High risk. Don't ship without significant remediation.
Scanned 5/24/2026, 8:22:17 PMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
High
Severity Breakdown
0
critical
23
high
52
medium
5
low
MCP Server Information
Findings
This package receives a D grade with a safety score of 52/100 due to 23 high-severity issues, primarily concentrated in server configuration (44 findings) and readiness concerns (9 findings), along with multiple instances of ANSI escape injection and prompt injection vulnerabilities. The combination of these weaknesses, particularly the server configuration problems and injection vectors, creates significant security risks that would require substantial remediation before deployment in a production environment.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 40 of 80 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
DNS Rebinding Protection Disabled by Default in Model Context Protocol Python SDK for Servers Running on Localhost
MCP SDK FastMCP Server Validation Error Leading to Denial of Service
Unhandled Exception in Streamable HTTP Transport Leading to Denial of Service
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
Showing 1โ30 of 80 findings
80 findings
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 45 | # Try both npx.cmd and npx.exe on Windows |
| 46 | for cmd in ["npx.cmd", "npx.exe", "npx"]: |
| 47 | try: |
| 48 | subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True) |
| 49 | return cmd |
| 50 | except subprocess.CalledProcessError: |
| 51 | continue |
RemediationAI
The problem is that `subprocess.run()` is called with `shell=True` and a hardcoded command list, but the `shell=True` flag is unnecessary and dangerous when using list-form arguments. Remove the `shell=True` parameter from the `subprocess.run()` call in `src/mcp/cli/cli.py` โ change `subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)` to `subprocess.run([cmd, "--version"], check=True, capture_output=True)`. This fix eliminates the command injection risk because list-form subprocess calls with `shell=False` (the default) do not interpret shell metacharacters. Verify by running the function and confirming it still detects npx correctly without shell interpretation.
Conformance test client uses dynamic scenario handler registration via decorator pattern (HANDLERS dict) allowing runtime re-registration of tool behavior through register() function, enabling self-re-registration of handlers with different behavior at runtime.
Evidence
| 1 | """MCP unified conformance test client. |
| 2 | |
| 3 | This client is designed to work with the @modelcontextprotocol/conformance npm package. |
| 4 | It handles all conformance test scenarios via environment variables and CLI arguments. |
| 5 | |
| 6 | Contract: |
| 7 | - MCP_CONFORMANCE_SCENARIO env var -> scenario name |
| 8 | - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios) |
| 9 | - Server URL as last CLI argument (sys.argv[1]) |
| 10 | - Must exit 0 within 30 seconds |
| 11 | |
| 12 | Scenarios: |
| 13 | initialize |
RemediationAI
The problem is that the conformance test client uses a decorator-based `HANDLERS` dictionary that allows runtime re-registration of tool behavior, creating a vector for self-re-registration attacks where handlers can modify their own behavior at runtime. Implement a frozen or immutable handler registry by converting `HANDLERS` from a mutable dict to a `types.MappingProxyType(HANDLERS)` or by removing the `register()` function and using only static decorator registration at module load time. This fix prevents handlers from re-registering themselves with different behavior after initialization. Verify by attempting to call `register()` after the server starts and confirming it raises an `AttributeError` or `TypeError`.
File registers an MCP resource handler (`@mcp.resource`, `server.registerResource`, or a `"resources/read"` JSON-RPC handler) AND opens a file with no path-canonicalisation guard. The URI parameter from the client flows directly into a filesystem sink, letting a malicious request escape the intended root via `..` / absolute paths / symlinks. Canonicalise via `os.path.realpath` + `os.path.commonpath` / `path.resolve` + prefix check, OR use `secure_filename` / `pathlib.Path.relative_to(allowed_ro
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
RemediationAI
The problem is that the README documents MCP resource handlers but does not provide path-canonicalization guidance, leaving developers vulnerable to directory traversal attacks when implementing file-serving resources. Add a security best-practices section to README.md that explicitly documents the requirement to canonicalize file paths using `os.path.realpath()` combined with `os.path.commonpath()` or `pathlib.Path.relative_to()` before opening files in resource handlers. This fix prevents attackers from using `../` sequences or absolute paths to escape the intended root directory. Verify by creating a test resource handler that attempts to access `../../etc/passwd` and confirming it raises a `ValueError` or returns an error instead of serving the file.
File registers an MCP resource handler (`@mcp.resource`, `server.registerResource`, or a `"resources/read"` JSON-RPC handler) AND opens a file with no path-canonicalisation guard. The URI parameter from the client flows directly into a filesystem sink, letting a malicious request escape the intended root via `..` / absolute paths / symlinks. Canonicalise via `os.path.realpath` + `os.path.commonpath` / `path.resolve` + prefix check, OR use `secure_filename` / `pathlib.Path.relative_to(allowed_ro
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
RemediationAI
The problem is that README.v2.md documents MCP resource handlers without providing path-canonicalization security guidance, leaving developers vulnerable to directory traversal attacks. Add a security best-practices section to README.v2.md that explicitly documents the requirement to canonicalize file paths using `os.path.realpath()` combined with `os.path.commonpath()` or `pathlib.Path.relative_to()` before opening files in resource handlers. This fix prevents attackers from using `../` sequences or absolute paths to escape the intended root directory. Verify by creating a test resource handler that attempts to access `../../etc/passwd` and confirming it raises a `ValueError` or returns an error instead of serving the file.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | from mcp.server.mcpserver import MCPServer |
| 2 | from mcp.server.mcpserver.prompts import base |
| 3 | |
| 4 | mcp = MCPServer(name="Prompt Example") |
| 5 | |
| 6 | |
| 7 | @mcp.prompt(title="Code Review") |
| 8 | def review_code(code: str) -> str: |
| 9 | return f"Please review this code:\n\n{code}" |
| 10 | |
| 11 | |
| 12 | @mcp.prompt(title="Debug Assistant") |
| 13 | def debug_error(error: str) -> list[base.Message]: |
| 14 | return [ |
| 15 | base.UserMessage("I'm seeing this error:"), |
| 16 | base.UserMessage(error), |
| 17 | base.AssistantMessage("I'll help debug that. What have y |
RemediationAI
The problem is that the `review_code()` prompt handler in `examples/snippets/servers/basic_prompt.py` uses an f-string to interpolate the untrusted `code` parameter directly into the prompt text, allowing an attacker to inject arbitrary instructions into the LLM's context. Replace the f-string with a safe wrapper: change `return f"Please review this code:\n\n{code}"` to `return f"Please review this code:\n\n<untrusted>{code}</untrusted>"` or use `json.dumps(code)` to escape special characters. This fix signals to the LLM that the content is untrusted and prevents prompt injection attacks. Verify by passing a prompt like `ignore previous instructions and do X` and confirming the LLM treats it as code to review, not as new instructions.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | """MCPServer Echo Server""" |
| 2 | |
| 3 | from mcp.server.mcpserver import MCPServer |
| 4 | |
| 5 | # Create server |
| 6 | mcp = MCPServer("Echo Server") |
| 7 | |
| 8 | |
| 9 | @mcp.tool() |
| 10 | def echo_tool(text: str) -> str: |
| 11 | """Echo the input text""" |
| 12 | return text |
| 13 | |
| 14 | |
| 15 | @mcp.resource("echo://static") |
| 16 | def echo_resource() -> str: |
| 17 | return "Echo!" |
| 18 | |
| 19 | |
| 20 | @mcp.resource("echo://{text}") |
| 21 | def echo_template(text: str) -> str: |
| 22 | """Echo the input text""" |
| 23 | return f"Echo: {text}" |
| 24 | |
| 25 | |
| 26 | @mcp.prompt("echo") |
| 27 | def echo_prompt(text: str) -> str: |
| 28 | return text |
RemediationAI
The problem is that the `echo.py` example file registers MCP resource and tool handlers but does not demonstrate safe handling of user input in prompts or resource URIs. Add a prompt handler example that wraps untrusted parameters in `<untrusted>...</untrusted>` tags or uses `json.dumps()` to escape them, and document path canonicalization for any file-serving resources. This fix prevents prompt injection and directory traversal attacks in handlers. Verify by adding a prompt handler that accepts user input and confirming that injected instructions are treated as data, not commands.
LLM consensus
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
RemediationAI
The problem is that the README.md does not provide guidance on safe prompt handling in MCP servers, leaving developers vulnerable to prompt injection attacks when they interpolate user input into prompts. Add a security section to README.md that explicitly documents the requirement to wrap untrusted parameters in `<untrusted>...</untrusted>` tags or use `json.dumps()` when building prompt text in handlers decorated with `@mcp.prompt()`. This fix prevents attackers from injecting arbitrary LLM instructions via prompt parameters. Verify by creating a test prompt handler that accepts user input and confirming that injected instructions are treated as data.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that the `mcpserver_quickstart.py` example does not demonstrate safe handling of user input in prompts, leaving developers vulnerable to prompt injection attacks when they copy this pattern. Add a prompt handler example that wraps untrusted parameters in `<untrusted>...</untrusted>` tags or uses `json.dumps()` to escape them. This fix prevents prompt injection attacks in handlers. Verify by adding a prompt handler that accepts user input and confirming that injected instructions are treated as data, not commands.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | from mcp.server.mcpserver import MCPServer |
| 2 | from mcp.types import ( |
| 3 | Completion, |
| 4 | CompletionArgument, |
| 5 | CompletionContext, |
| 6 | PromptReference, |
| 7 | ResourceTemplateReference, |
| 8 | ) |
| 9 | |
| 10 | mcp = MCPServer(name="Example") |
| 11 | |
| 12 | |
| 13 | @mcp.resource("github://repos/{owner}/{repo}") |
| 14 | def github_repo(owner: str, repo: str) -> str: |
| 15 | """GitHub repository resource.""" |
| 16 | return f"Repository: {owner}/{repo}" |
| 17 | |
| 18 | |
| 19 | @mcp.prompt(description="Code review prompt") |
| 20 | def review_code(language: str, code: str) -> str: |
| 21 | """Gen |
RemediationAI
The problem is that the `completion.py` example registers resource handlers with URI parameters (`owner`, `repo`) but does not demonstrate safe path canonicalization or prompt injection prevention. Add documentation and example code showing how to canonicalize file paths using `os.path.realpath()` and `os.path.commonpath()`, and wrap any user-controlled text in prompts with `<untrusted>...</untrusted>` tags or `json.dumps()`. This fix prevents directory traversal and prompt injection attacks. Verify by attempting to access `../../../etc/passwd` via the resource URI and confirming it raises an error.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | !!! info "You are viewing the in-development v2 documentation" |
| 4 | For the current stable release, see the [v1.x documentation](https://py.sdk.modelcontextprotocol.io/). |
| 5 | |
| 6 | The **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. |
| 7 | |
| 8 | This Python SDK implements the full MCP specification, making it easy to: |
| 9 | |
| 10 | - **Build MCP servers** that expose resources, pr |
RemediationAI
The problem is that the docs/index.md does not provide security guidance on safe prompt handling in MCP servers, leaving developers vulnerable to prompt injection attacks. Add a security best-practices section to docs/index.md that explicitly documents the requirement to wrap untrusted parameters in `<untrusted>...</untrusted>` tags or use `json.dumps()` when building prompt text in handlers. This fix prevents attackers from injecting arbitrary LLM instructions via prompt parameters. Verify by creating a test prompt handler that accepts user input and confirming that injected instructions are treated as data.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | from __future__ import annotations |
| 2 | |
| 3 | from datetime import datetime |
| 4 | from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar |
| 5 | |
| 6 | from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter |
| 7 | from pydantic.alias_generators import to_camel |
| 8 | from typing_extensions import NotRequired, TypedDict |
| 9 | |
| 10 | from mcp.types.jsonrpc import RequestId |
| 11 | |
| 12 | LATEST_PROTOCOL_VERSION = "2025-11-25" |
| 13 | """The latest version of the Model Context Protocol. |
| 14 | |
| 15 | You can find the latest specification at https: |
RemediationAI
The problem is that `src/mcp/types/_types.py` is a type definitions file with no HTTP routes, so this finding appears to be a false positive or misclassification. If there are HTTP routes elsewhere that handle `tools/list` without authentication, apply auth middleware at the route or router level: for FastAPI, add `Depends(verify_jwt)` or `Depends(get_current_user)` to the route; for Flask, use `@login_required` or a custom decorator; for Express, use `passport.authenticate()`. This fix prevents anonymous clients from enumerating tools. Verify by making an unauthenticated request to the `tools/list` endpoint and confirming it returns a 401 or 403 error.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | """MCP Server Module |
| 2 | |
| 3 | This module provides a framework for creating an MCP (Model Context Protocol) server. |
| 4 | It allows you to easily define and handle various types of requests and notifications |
| 5 | using constructor-based handler registration. |
| 6 | |
| 7 | Usage: |
| 8 | 1. Define handler functions: |
| 9 | async def my_list_tools(ctx, params): |
| 10 | return types.ListToolsResult(tools=[...]) |
| 11 | |
| 12 | async def my_call_tool(ctx, params): |
| 13 | return types.CallToolResult(content=[...]) |
| 14 | |
| 15 | 2. Create a Server instance with on_* han |
RemediationAI
The problem is that `src/mcp/server/lowlevel/server.py` provides a low-level server framework but does not enforce authentication on HTTP routes by default, leaving tools/list and tools/call endpoints accessible to unauthenticated clients. Add authentication middleware to the server initialization: implement a `require_auth` decorator or middleware that checks for a valid `Authorization` header or API key on every request, and apply it to all route handlers. This fix prevents anonymous clients from enumerating and invoking tools. Verify by making an unauthenticated request to a protected endpoint and confirming it returns a 401 error.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/streamable_config.py |
| 3 | """ |
| 4 | |
| 5 | from mcp.server.mcpserver import MCPServer |
| 6 | |
| 7 | mcp = MCPServer("StatelessServer") |
| 8 | |
| 9 | |
| 10 | # Add a simple tool to demonstrate the server |
| 11 | @mcp.tool() |
| 12 | def greet(name: str = "World") -> str: |
| 13 | """Greet someone by name.""" |
| 14 | return f"Hello, {name}!" |
| 15 | |
| 16 | |
| 17 | # Run server with streamable_http transport |
| 18 | # Transport-specific options (stateless_http, json_response) are passed to run() |
| 19 | if __name__ == "__main__": |
| 20 | # Stateles |
RemediationAI
The problem is that `examples/snippets/servers/streamable_config.py` uses `mcp.run()` without specifying a transport or binding address, which may default to binding on all interfaces or localhost without authentication. Change the server startup to explicitly use stdio transport: `mcp.run(transport="stdio")`, or if HTTP is required, add authentication by implementing a token verifier and passing it to the server. This fix prevents DNS-rebinding attacks from malicious web pages. Verify by confirming the server only accepts connections from stdio or requires a valid Authorization header.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that `examples/snippets/servers/mcpserver_quickstart.py` does not specify a transport or authentication, which may default to binding on localhost without auth, leaving it vulnerable to DNS-rebinding attacks. Change the server startup to use stdio transport: `mcp.run(transport="stdio")`, or if HTTP is required, add authentication by implementing token verification. This fix prevents malicious web pages from invoking tools via DNS rebinding. Verify by confirming the server only accepts connections from stdio or requires a valid Authorization header.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
RemediationAI
The problem is that `examples/servers/everything-server/mcp_everything_server/server.py` binds an HTTP transport to localhost and registers tools without enforcing authentication, leaving it vulnerable to DNS-rebinding attacks. Add authentication to the server by implementing a token verifier and requiring an `Authorization` header on all requests, or switch to stdio transport: `mcp.run(transport="stdio")`. This fix prevents malicious web pages from invoking tools. Verify by making an unauthenticated request to a tool endpoint and confirming it returns a 401 error.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/oauth_server.py |
| 3 | """ |
| 4 | |
| 5 | from pydantic import AnyHttpUrl |
| 6 | |
| 7 | from mcp.server.auth.provider import AccessToken, TokenVerifier |
| 8 | from mcp.server.auth.settings import AuthSettings |
| 9 | from mcp.server.mcpserver import MCPServer |
| 10 | |
| 11 | |
| 12 | class SimpleTokenVerifier(TokenVerifier): |
| 13 | """Simple token verifier for demonstration.""" |
| 14 | |
| 15 | async def verify_token(self, token: str) -> AccessToken | None: |
| 16 | pass # This is where you would implement actual to |
RemediationAI
The problem is that `examples/snippets/servers/oauth_server.py` demonstrates OAuth token verification but does not validate the `Host` header on HTTP requests, leaving it vulnerable to DNS-rebinding attacks even with authentication. Add host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests, or use a middleware that enforces this check. This fix prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
LLM consensus
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
RemediationAI
The problem is that README.v2.md does not document host header validation requirements for HTTP-bound MCP servers, leaving developers vulnerable to DNS-rebinding attacks. Add a security section to README.v2.md that explicitly documents the requirement to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. This fix prevents DNS-rebinding attacks. Verify by creating a test server and making a request with a spoofed `Host` header, confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
RemediationAI
The problem is that README.md does not document host header validation requirements for HTTP-bound MCP servers, leaving developers vulnerable to DNS-rebinding attacks. Add a security section to README.md that explicitly documents the requirement to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. This fix prevents DNS-rebinding attacks. Verify by creating a test server and making a request with a spoofed `Host` header, confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that `examples/snippets/servers/mcpserver_quickstart.py` does not validate the `Host` header on HTTP requests, leaving it vulnerable to DNS-rebinding attacks. Add host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This fix prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/streamable_config.py |
| 3 | """ |
| 4 | |
| 5 | from mcp.server.mcpserver import MCPServer |
| 6 | |
| 7 | mcp = MCPServer("StatelessServer") |
| 8 | |
| 9 | |
| 10 | # Add a simple tool to demonstrate the server |
| 11 | @mcp.tool() |
| 12 | def greet(name: str = "World") -> str: |
| 13 | """Greet someone by name.""" |
| 14 | return f"Hello, {name}!" |
| 15 | |
| 16 | |
| 17 | # Run server with streamable_http transport |
| 18 | # Transport-specific options (stateless_http, json_response) are passed to run() |
| 19 | if __name__ == "__main__": |
| 20 | # Stateles |
RemediationAI
The problem is that `examples/snippets/servers/streamable_config.py` does not validate the `Host` header on HTTP requests, leaving it vulnerable to DNS-rebinding attacks. Add host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This fix prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
RemediationAI
The problem is that `examples/servers/everything-server/mcp_everything_server/server.py` does not validate the `Host` header on HTTP requests, leaving it vulnerable to DNS-rebinding attacks. Add host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This fix prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # Migration Guide: v1 to v2 |
| 2 | |
| 3 | This guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | Version 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety. |
| 8 | |
| 9 | ## Breaking Changes |
| 10 | |
| 11 | ### `streamablehttp_client` removed |
| 12 | |
| 13 | The deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead. |
| 14 | |
| 15 | **Before (v1):** |
| 16 | |
| 17 | ```python |
| 18 | from |
RemediationAI
The problem is that `docs/migration.md` does not document host header validation requirements when migrating to v2, leaving developers vulnerable to DNS-rebinding attacks. Add a security section to the migration guide that explicitly documents the requirement to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. This fix prevents DNS-rebinding attacks. Verify by creating a test server and making a request with a spoofed `Host` header, confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | !!! info "You are viewing the in-development v2 documentation" |
| 4 | For the current stable release, see the [v1.x documentation](https://py.sdk.modelcontextprotocol.io/). |
| 5 | |
| 6 | The **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. |
| 7 | |
| 8 | This Python SDK implements the full MCP specification, making it easy to: |
| 9 | |
| 10 | - **Build MCP servers** that expose resources, pr |
RemediationAI
The problem is that `docs/index.md` does not document host header validation requirements for HTTP-bound MCP servers, leaving developers vulnerable to DNS-rebinding attacks. Add a security section to docs/index.md that explicitly documents the requirement to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. This fix prevents DNS-rebinding attacks. Verify by creating a test server and making a request with a spoofed `Host` header, confirming it is rejected.
Service binds to 0.0.0.0 โ all network interfaces. For MCP servers that only need to talk to a single parent process, bind to 127.0.0.1 (or a Unix domain socket) instead.
Evidence
| 370 | # Or for SSE |
| 371 | mcp = MCPServer("Server") |
| 372 | mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events") |
| 373 | ``` |
| 374 | |
| 375 | **For mounted apps:** |
RemediationAI
The problem is that the example in `docs/migration.md` shows binding to `0.0.0.0` (all network interfaces), which exposes the MCP server to the entire network instead of just the local machine. Change `host="0.0.0.0"` to `host="127.0.0.1"` in the example: `mcp.run(transport="sse", host="127.0.0.1", port=9000, sse_path="/events")`. This fix restricts access to localhost only. Verify by confirming the server is not reachable from other machines on the network.
Service binds to 0.0.0.0 โ all network interfaces. For MCP servers that only need to talk to a single parent process, bind to 127.0.0.1 (or a Unix domain socket) instead.
Evidence
| 355 | mcp.run(transport="streamable-http") |
| 356 | |
| 357 | # Or for SSE |
| 358 | mcp = FastMCP("Server", host="0.0.0.0", port=9000, sse_path="/events") |
| 359 | mcp.run(transport="sse") |
| 360 | ``` |
RemediationAI
The problem is that the example in `docs/migration.md` shows binding to `0.0.0.0` (all network interfaces), which exposes the MCP server to the entire network instead of just the local machine. Change `host="0.0.0.0"` to `host="127.0.0.1"` in the example: `FastMCP("Server", host="127.0.0.1", port=9000, sse_path="/events")`. This fix restricts access to localhost only. Verify by confirming the server is not reachable from other machines on the network.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 74 | message = params.message |
| 75 | |
| 76 | if not url: |
| 77 | print("Error: No URL provided in elicitation request") |
| 78 | return types.ElicitResult(action="cancel") |
| 79 | |
| 80 | # Reject dangerous URL schemes before prompting the user |
RemediationAI
The problem is that user-controlled `params.message` is printed directly to the terminal in `examples/snippets/clients/url_elicitation_client.py` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes that rewrite output or hide commands. Replace `print("Error: No URL provided in elicitation request")` with a sanitized version using `print(repr(message))` or a library like `bleach` to strip ANSI codes before printing. This fix prevents terminal injection attacks. Verify by passing a message with ANSI escape codes (e.g., `\x1b[2J`) and confirming the terminal is not cleared.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 896 | async def handle_progress(ctx: ServerRequestContext, params: ProgressNotificationParams) -> None: |
| 897 | print(f"Progress: {params.progress}/{params.total}") |
| 898 | |
| 899 | server = Server("my-server", on_progress=handle_progress) |
| 900 | ``` |
RemediationAI
The problem is that `params.progress` and `params.total` are printed directly to the terminal in `docs/migration.md` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Progress: {params.progress}/{params.total}")` with `print(f"Progress: {repr(params.progress)}/{repr(params.total)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing progress values with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 100 | async def handle_elicitation(context, params: ElicitRequestParams) -> ElicitResult: |
| 101 | # Display the message to the user |
| 102 | print(f"Server asks: {params.message}") |
| 103 | |
| 104 | # Collect user input (this is a simplified example) |
| 105 | response = input("Your response (y/n): ") |
RemediationAI
The problem is that `params.message` is printed directly to the terminal in `docs/experimental/tasks-client.md` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Server asks: {params.message}")` with `print(f"Server asks: {repr(params.message)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing a message with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 2115 | async def handle_sampling_message( |
| 2116 | context: ClientRequestContext, params: types.CreateMessageRequestParams |
| 2117 | ) -> types.CreateMessageResult: |
| 2118 | print(f"Sampling request: {params.messages}") |
| 2119 | return types.CreateMessageResult( |
| 2120 | role="assistant", |
| 2121 | content=types.TextContent( |
RemediationAI
The problem is that `params.messages` is printed directly to the terminal in `README.v2.md` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Sampling request: {params.messages}")` with `print(f"Sampling request: {repr(params.messages)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing messages with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 2175 | async def handle_sampling_message( |
| 2176 | context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams |
| 2177 | ) -> types.CreateMessageResult: |
| 2178 | print(f"Sampling request: {params.messages}") |
| 2179 | return types.CreateMessageResult( |
| 2180 | role="assistant", |
| 2181 | content=types.TextContent( |
RemediationAI
The problem is that `params.messages` is printed directly to the terminal in `README.md` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Sampling request: {params.messages}")` with `print(f"Sampling request: {repr(params.messages)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing messages with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 21 | async def handle_sampling_message( |
| 22 | context: ClientRequestContext, params: types.CreateMessageRequestParams |
| 23 | ) -> types.CreateMessageResult: |
| 24 | print(f"Sampling request: {params.messages}") |
| 25 | return types.CreateMessageResult( |
| 26 | role="assistant", |
| 27 | content=types.TextContent( |
RemediationAI
The problem is that `params.messages` is printed directly to the terminal in `examples/snippets/clients/stdio_client.py` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Sampling request: {params.messages}")` with `print(f"Sampling request: {repr(params.messages)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing messages with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 287 | async def elicitation_callback(context, params: ElicitRequestParams) -> ElicitResult: |
| 288 | print(f"\n[Elicitation] {params.message}") |
| 289 | response = input("Confirm? (y/n): ") |
| 290 | return ElicitResult(action="accept", content={"confirm": response.lower() == "y"}) |
RemediationAI
The problem is that `params.message` is printed directly to the terminal in `docs/experimental/tasks-client.md` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"\n[Elicitation] {params.message}")` with `print(f"\n[Elicitation] {repr(params.message)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing a message with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 342 | def main() -> None: |
| 343 | """Main entry point for the conformance client.""" |
| 344 | if len(sys.argv) < 2: |
| 345 | print(f"Usage: {sys.argv[0]} <server-url>", file=sys.stderr) |
| 346 | sys.exit(1) |
| 347 | |
| 348 | server_url = sys.argv[1] |
RemediationAI
The problem is that `sys.argv[1]` (the server URL) is printed directly to stderr in `.github/actions/conformance/client.py` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"Usage: {sys.argv[0]} <server-url>", file=sys.stderr)` with `print(f"Usage: {repr(sys.argv[0])} <server-url>", file=sys.stderr)` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing a URL with ANSI escape codes and confirming the terminal is not affected.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 27 | params: ElicitRequestParams, |
| 28 | ) -> ElicitResult: |
| 29 | """Handle elicitation requests from the server.""" |
| 30 | print(f"\n[Elicitation] Server asks: {params.message}") |
| 31 | |
| 32 | # Simple terminal prompt |
| 33 | response = input("Your response (y/n): ").strip().lower() |
RemediationAI
The problem is that `params.message` is printed directly to the terminal in `examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py` without sanitizing ANSI escape sequences, allowing attackers to inject cursor-control codes. Replace `print(f"\n[Elicitation] Server asks: {params.message}")` with `print(f"\n[Elicitation] Server asks: {repr(params.message)}")` or use a sanitization library. This fix prevents terminal injection attacks. Verify by passing a message with ANSI escape codes and confirming the terminal is not affected.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 243 | return f"Elicitation result: action={result.action}, content={content}" |
| 244 | except Exception as e: |
| 245 | return f"Elicitation not supported or error: {str(e)}" |
| 246 | |
| 247 | |
| 248 | class EnumSchemasTestSchema(BaseModel): |
RemediationAI
The problem is that exception details are returned directly to the caller in `examples/servers/everything-server/mcp_everything_server/server.py` via `str(e)`, exposing internal paths and library versions. Replace `return f"Elicitation not supported or error: {str(e)}"` with `return "Elicitation not supported or error: An internal error occurred"` and log the full exception internally using `logging.exception()`. This fix prevents information disclosure. Verify by triggering an exception and confirming the response does not contain stack traces or internal details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 189 | return f"LLM response: {model_response}" |
| 190 | except Exception as e: |
| 191 | return f"Sampling not supported or error: {str(e)}" |
| 192 | |
| 193 | |
| 194 | class UserResponse(BaseModel): |
RemediationAI
The problem is that exception details are returned directly to the caller in `examples/servers/everything-server/mcp_everything_server/server.py` via `str(e)`, exposing internal paths and library versions. Replace `return f"Sampling not supported or error: {str(e)}"` with `return "Sampling not supported or error: An internal error occurred"` and log the full exception internally using `logging.exception()`. This fix prevents information disclosure. Verify by triggering an exception and confirming the response does not contain stack traces or internal details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 211 | return f"User response: action={result.action}, content={content}" |
| 212 | except Exception as e: |
| 213 | return f"Elicitation not supported or error: {str(e)}" |
| 214 | |
| 215 | |
| 216 | class SEP1034DefaultsSchema(BaseModel): |
RemediationAI
The problem is that exception details are returned directly to the caller in `examples/servers/everything-server/mcp_everything_server/server.py` via `str(e)`, exposing internal paths and library versions. Replace `return f"Elicitation not supported or error: {str(e)}"` with `return "Elicitation not supported or error: An internal error occurred"` and log the full exception internally using `logging.exception()`. This fix prevents information disclosure. Verify by triggering an exception and confirming the response does not contain stack traces or internal details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 302 | return f"Elicitation completed: action={result.action}, content={content}" |
| 303 | except Exception as e: |
| 304 | return f"Elicitation not supported or error: {str(e)}" |
| 305 | |
| 306 | |
| 307 | @mcp.tool() |
RemediationAI
The problem is that exception details are returned directly to the caller in `examples/servers/everything-server/mcp_everything_server/server.py` via `str(e)`, exposing internal paths and library versions. Replace `return f"Elicitation not supported or error: {str(e)}"` with `return "Elicitation not supported or error: An internal error occurred"` and log the full exception internally using `logging.exception()`. This fix prevents information disclosure. Verify by triggering an exception and confirming the response does not contain stack traces or internal details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 313 | except MCPError: |
| 314 | raise |
| 315 | except Exception as e: |
| 316 | return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True) |
| 317 | if isinstance(result, CallToolResult): |
| 318 | return result |
| 319 | if isinstance(result, tuple) and len(result) == 2: |
RemediationAI
The problem is that exception details are returned directly to the caller in `src/mcp/server/mcpserver/server.py` via `str(e)`, exposing internal paths and library versions. Replace `return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)` with `return CallToolResult(content=[TextContent(type="text", text="Tool execution failed")], is_error=True)` and log the full exception internally using `logging.exception()`. This fix prevents information disclosure. Verify by triggering an exception and confirming the response does not contain stack traces or internal details.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 45 | # Try both npx.cmd and npx.exe on Windows |
| 46 | for cmd in ["npx.cmd", "npx.exe", "npx"]: |
| 47 | try: |
| 48 | subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True) |
| 49 | return cmd |
| 50 | except subprocess.CalledProcessError: |
| 51 | continue |
RemediationAI
The problem is that `subprocess.run()` in `src/mcp/cli/cli.py` is called without a timeout parameter, allowing a hung npx process to block indefinitely and exhaust resources. Add a `timeout` parameter: `subprocess.run([cmd, "--version"], check=True, capture_output=True, timeout=5)`. This fix prevents resource exhaustion from hung subprocesses. Verify by creating a test that hangs and confirming it raises `subprocess.TimeoutExpired` after 5 seconds.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 206 | async def update_importance(user_embedding: list[float], deps: Deps): |
| 207 | async with deps.pool.acquire() as conn: |
| 208 | rows = await conn.fetch("SELECT id, importance, access_count, embedding FROM memories") |
| 209 | for row in rows: |
| 210 | memory_embedding = row["embedding"] |
| 211 | similarity = cosine_similarity(user_embedding, memory_embedding) |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
LLM consensus
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | # /// script |
| 2 | # dependencies = ["pydantic-ai-slim[openai]", "asyncpg", "numpy", "pgvector"] |
| 3 | # /// |
| 4 | |
| 5 | # uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector |
| 6 | |
| 7 | """Recursive memory system inspired by the human brain's clustering of memories. |
| 8 | Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient |
| 9 | similarity search. |
| 10 | """ |
| 11 | |
| 12 | import asyncio |
| 13 | import math |
| 14 | import os |
| 15 | from dataclasses import dataclass |
| 16 | from datetime import datetime, timezone |
| 17 | from pathlib import Path |
| 18 | from typing import An |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 1722 | description="Query the database", |
| 1723 | inputSchema={ |
| 1724 | "type": "object", |
| 1725 | "properties": {"query": {"type": "string", "description": "SQL query to execute"}}, |
| 1726 | "required": ["query"], |
| 1727 | }, |
| 1728 | ) |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 29 | # see OAuthClientMetadata; we only support `code` |
| 30 | response_type: Literal["code"] = Field(..., description="Must be 'code' for authorization code flow") |
| 31 | code_challenge: str = Field(..., description="PKCE code challenge") |
| 32 | code_challenge_method: Literal["S256"] = Field("S256", description="PKCE code challenge method, must be S256") |
| 33 | state: str | None = Field(None, description="Optional state parameter") |
| 34 | scope: str | None = Field( |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 33 | """Parameters for initializing an sse_client.""" |
| 34 | |
| 35 | # The endpoint URL. |
| 36 | url: str |
| 37 | |
| 38 | # Optional headers to include in requests. |
| 39 | headers: dict[str, Any] | None = None |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 49 | """Parameters for initializing a streamable_http_client.""" |
| 50 | |
| 51 | # The endpoint URL. |
| 52 | url: str |
| 53 | |
| 54 | # Optional headers to include in requests. |
| 55 | headers: dict[str, Any] | None = None |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 1689 | description="Query the database", |
| 1690 | input_schema={ |
| 1691 | "type": "object", |
| 1692 | "properties": {"query": {"type": "string", "description": "SQL query to execute"}}, |
| 1693 | "required": ["query"], |
| 1694 | }, |
| 1695 | ) |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 28 | "type": "object", |
| 29 | "required": ["url"], |
| 30 | "properties": { |
| 31 | "url": { |
| 32 | "type": "string", |
| 33 | "description": "URL to fetch", |
| 34 | } |
| 35 | }, |
| 36 | }, |
| 37 | ) |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 153 | class HttpResource(Resource): |
| 154 | """A resource that reads from an HTTP endpoint.""" |
| 155 | |
| 156 | url: str = Field(description="URL to fetch content from") |
| 157 | mime_type: str = Field(default="application/json", description="MIME type of the resource content") |
| 158 | |
| 159 | async def read(self) -> str | bytes: |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 10 | class AuthorizationParams(BaseModel): |
| 11 | state: str | None |
| 12 | scopes: list[str] | None |
| 13 | code_challenge: str |
| 14 | redirect_uri: AnyUrl |
| 15 | redirect_uri_provided_explicitly: bool |
| 16 | resource: str | None = None # RFC 8707 resource indicator |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 1662 | message: str |
| 1663 | """The message to present to the user explaining why the interaction is needed.""" |
| 1664 | |
| 1665 | url: str |
| 1666 | """The URL that the user should navigate to.""" |
| 1667 | |
| 1668 | elicitation_id: str |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 17 | class AuthorizationCode(BaseModel): |
| 18 | code: str |
| 19 | scopes: list[str] |
| 20 | expires_at: float |
| 21 | client_id: str |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 21 | scopes: list[str] |
| 22 | expires_at: float |
| 23 | client_id: str |
| 24 | code_challenge: str |
| 25 | redirect_uri: AnyUrl |
| 26 | redirect_uri_provided_explicitly: bool |
| 27 | resource: str | None = None # RFC 8707 resource indicator |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 23 | # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 |
| 24 | client_secret: str | None = None |
| 25 | # See https://datatracker.ietf.org/doc/html/rfc7636#section-4.5 |
| 26 | code_verifier: str = Field(..., description="PKCE code verifier") |
| 27 | # RFC 8707 resource indicator |
| 28 | resource: str | None = Field(None, description="Resource indicator for the token") |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 69 | class StdioServerParameters(BaseModel): |
| 70 | command: str |
| 71 | """The executable to run to start the server.""" |
| 72 | |
| 73 | args: list[str] = Field(default_factory=list) |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 17 | class AuthorizationCodeRequest(BaseModel): |
| 18 | # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 |
| 19 | grant_type: Literal["authorization_code"] |
| 20 | code: str = Field(..., description="The authorization code") |
| 21 | redirect_uri: AnyUrl | None = Field(None, description="Must be the same as redirect URI provided in /authorize") |
| 22 | client_id: str |
| 23 | # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 56 | description="Query the database", |
| 57 | input_schema={ |
| 58 | "type": "object", |
| 59 | "properties": {"query": {"type": "string", "description": "SQL query to execute"}}, |
| 60 | "required": ["query"], |
| 61 | }, |
| 62 | ) |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | { |
| 2 | "mcpServers": { |
| 3 | "sqlite": { |
| 4 | "command": "uvx", |
| 5 | "args": ["mcp-server-sqlite", "--db-path", "./test.db"] |
| 6 | }, |
| 7 | "puppeteer": { |
| 8 | "command": "npx", |
| 9 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"] |
| 10 | } |
| 11 | } |
| 12 | } |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | """URL Elicitation Client Example. |
| 2 | |
| 3 | Demonstrates how clients handle URL elicitation requests from servers. |
| 4 | This is the Python equivalent of TypeScript SDK's elicitationUrlExample.ts, |
| 5 | focused on URL elicitation patterns without OAuth complexity. |
| 6 | |
| 7 | Features demonstrated: |
| 8 | 1. Client elicitation capability declaration |
| 9 | 2. Handling elicitation requests from servers via callback |
| 10 | 3. Catching UrlElicitationRequiredError from tool calls |
| 11 | 4. Browser interaction with security warnings |
| 12 | 5. Interactive CLI for te |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | from __future__ import annotations |
| 2 | |
| 3 | from datetime import datetime |
| 4 | from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar |
| 5 | |
| 6 | from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter |
| 7 | from pydantic.alias_generators import to_camel |
| 8 | from typing_extensions import NotRequired, TypedDict |
| 9 | |
| 10 | from mcp.types.jsonrpc import RequestId |
| 11 | |
| 12 | LATEST_PROTOCOL_VERSION = "2025-11-25" |
| 13 | """The latest version of the Model Context Protocol. |
| 14 | |
| 15 | You can find the latest specification at https: |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | """Unified MCP Client that wraps ClientSession with transport management.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from contextlib import AsyncExitStack |
| 6 | from dataclasses import KW_ONLY, dataclass, field |
| 7 | from typing import Any |
| 8 | |
| 9 | from mcp.client._memory import InMemoryTransport |
| 10 | from mcp.client._transport import Transport |
| 11 | from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT |
| 12 | from mcp.client.streamable_http import streamable_http_client |
| 13 |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """Simple MCP client example with OAuth authentication support. |
| 3 | |
| 4 | This client connects to an MCP server using streamable HTTP transport with OAuth. |
| 5 | |
| 6 | """ |
| 7 | |
| 8 | from __future__ import annotations as _annotations |
| 9 | |
| 10 | import asyncio |
| 11 | import os |
| 12 | import socketserver |
| 13 | import threading |
| 14 | import time |
| 15 | import webbrowser |
| 16 | from http.server import BaseHTTPRequestHandler, HTTPServer |
| 17 | from typing import Any |
| 18 | from urllib.parse import parse_qs, urlparse |
| 19 | |
| 20 | import httpx |
| 21 | from mcp.client._transport import ReadSt |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | """Experimental client-side task support. |
| 2 | |
| 3 | This module provides client methods for interacting with MCP tasks. |
| 4 | |
| 5 | WARNING: These APIs are experimental and may change without notice. |
| 6 | |
| 7 | Example: |
| 8 | ```python |
| 9 | # Call a tool as a task |
| 10 | result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) |
| 11 | task_id = result.task.task_id |
| 12 | |
| 13 | # Get task status |
| 14 | status = await session.experimental.get_task(task_id) |
| 15 | |
| 16 | # Get task result when complete |
| 17 | if status.status == "co |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | from __future__ import annotations |
| 2 | |
| 3 | from collections.abc import Callable |
| 4 | from typing import TYPE_CHECKING, Any |
| 5 | |
| 6 | from mcp.server.mcpserver.exceptions import ToolError |
| 7 | from mcp.server.mcpserver.tools.base import Tool |
| 8 | from mcp.server.mcpserver.utilities.logging import get_logger |
| 9 | from mcp.types import Icon, ToolAnnotations |
| 10 | |
| 11 | if TYPE_CHECKING: |
| 12 | from mcp.server.context import LifespanContextT, RequestT |
| 13 | from mcp.server.mcpserver.context import Context |
| 14 | |
| 15 | logger = get_logger(__name__) |
| 16 | |
| 17 | |
| 18 | class ToolMa |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | # Client Task Usage |
| 2 | |
| 3 | !!! warning "Experimental" |
| 4 | |
| 5 | Tasks are an experimental feature. The API may change without notice. |
| 6 | |
| 7 | This guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers. |
| 8 | |
| 9 | ## Quick Start |
| 10 | |
| 11 | Call a tool as a task and poll for the result: |
| 12 | |
| 13 | ```python |
| 14 | from mcp.client.session import ClientSession |
| 15 | from mcp.types import CallToolResult |
| 16 | |
| 17 | async with ClientSession(read, write) as session: |
| 18 | |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct
MCP server registers tool handlers that call `elicit()` / `elicitInput()`, but no rate-limit middleware is wired into this file. The MCP elicitation spec SHOULD require clients AND servers to rate-limit elicitation calls โ without it, a compromised client can flood the user with prompts (denial- of-attention) or grind through enumeration attacks against multi-step elicitation flows. Add `slowapi` (Python), `express-rate-limit` (Node), or an equivalent per-session limiter on the elicitation hand
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
Remediation
```python from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @mcp.tool() @limiter.limit("5/minute") async def confirm_action(ctx) -> str: return await ctx.elicit("Continue?", {"yes": "boolean"}) ```
MCP server registers tool handlers that call `elicit()` / `elicitInput()`, but no rate-limit middleware is wired into this file. The MCP elicitation spec SHOULD require clients AND servers to rate-limit elicitation calls โ without it, a compromised client can flood the user with prompts (denial- of-attention) or grind through enumeration attacks against multi-step elicitation flows. Add `slowapi` (Python), `express-rate-limit` (Node), or an equivalent per-session limiter on the elicitation hand
Evidence
| 1 | """Elicitation examples demonstrating form and URL mode elicitation. |
| 2 | |
| 3 | Form mode elicitation collects structured, non-sensitive data through a schema. |
| 4 | URL mode elicitation directs users to external URLs for sensitive operations |
| 5 | like OAuth flows, credential collection, or payment processing. |
| 6 | """ |
| 7 | |
| 8 | import uuid |
| 9 | |
| 10 | from pydantic import BaseModel, Field |
| 11 | |
| 12 | from mcp.server.mcpserver import Context, MCPServer |
| 13 | from mcp.shared.exceptions import UrlElicitationRequiredError |
| 14 | from mcp.types import ElicitRequestURL |
Remediation
```python from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @mcp.tool() @limiter.limit("5/minute") async def confirm_action(ctx) -> str: return await ctx.elicit("Continue?", {"yes": "boolean"}) ```
MCP server registers tool handlers that call `elicit()` / `elicitInput()`, but no rate-limit middleware is wired into this file. The MCP elicitation spec SHOULD require clients AND servers to rate-limit elicitation calls โ without it, a compromised client can flood the user with prompts (denial- of-attention) or grind through enumeration attacks against multi-step elicitation flows. Add `slowapi` (Python), `express-rate-limit` (Node), or an equivalent per-session limiter on the elicitation hand
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
Remediation
```python from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @mcp.tool() @limiter.limit("5/minute") async def confirm_action(ctx) -> str: return await ctx.elicit("Continue?", {"yes": "boolean"}) ```
MCP server registers tool handlers that call `elicit()` / `elicitInput()`, but no rate-limit middleware is wired into this file. The MCP elicitation spec SHOULD require clients AND servers to rate-limit elicitation calls โ without it, a compromised client can flood the user with prompts (denial- of-attention) or grind through enumeration attacks against multi-step elicitation flows. Add `slowapi` (Python), `express-rate-limit` (Node), or an equivalent per-session limiter on the elicitation hand
Evidence
| 1 | from __future__ import annotations |
| 2 | |
| 3 | from collections.abc import Iterable |
| 4 | from typing import TYPE_CHECKING, Any, Generic |
| 5 | |
| 6 | from pydantic import AnyUrl, BaseModel |
| 7 | |
| 8 | from mcp.server.context import LifespanContextT, RequestT, ServerRequestContext |
| 9 | from mcp.server.elicitation import ( |
| 10 | ElicitationResult, |
| 11 | ElicitSchemaModelT, |
| 12 | UrlElicitationResult, |
| 13 | elicit_url, |
| 14 | elicit_with_validation, |
| 15 | ) |
| 16 | from mcp.server.lowlevel.helper_types import ReadResourceContents |
| 17 | from mcp.types import LoggingLevel |
| 18 | |
| 19 | i |
Remediation
```python from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @mcp.tool() @limiter.limit("5/minute") async def confirm_action(ctx) -> str: return await ctx.elicit("Continue?", {"yes": "boolean"}) ```
MCP server registers tool handlers that call `elicit()` / `elicitInput()`, but no rate-limit middleware is wired into this file. The MCP elicitation spec SHOULD require clients AND servers to rate-limit elicitation calls โ without it, a compromised client can flood the user with prompts (denial- of-attention) or grind through enumeration attacks against multi-step elicitation flows. Add `slowapi` (Python), `express-rate-limit` (Node), or an equivalent per-session limiter on the elicitation hand
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
Remediation
```python from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @mcp.tool() @limiter.limit("5/minute") async def confirm_action(ctx) -> str: return await ctx.elicit("Continue?", {"yes": "boolean"}) ```
MCP server has authentication wired but no invocation log. An authenticated server that never logs is a forensics dead-end: unauthorized actions cannot be detected, attributed, or reconstructed after the fact. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Fix: add a structured `logger.info("tool.invoke", ...)` call to every authenticated tool handler with at minimum the tool name, caller identity, and request id. Ship invocation events to a retention sink (CloudWatc
Evidence
| 1 | import json |
| 2 | import time |
| 3 | from typing import Any |
| 4 | |
| 5 | from pydantic import AnyHttpUrl |
| 6 | from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser |
| 7 | from starlette.requests import HTTPConnection |
| 8 | from starlette.types import Receive, Scope, Send |
| 9 | |
| 10 | from mcp.server.auth.provider import AccessToken, TokenVerifier |
| 11 | |
| 12 | |
| 13 | class AuthenticatedUser(SimpleUser): |
| 14 | """User with authentication info.""" |
| 15 | |
| 16 | def __init__(self, auth_info: AccessToken): |
| 17 | super().__init__(auth_info.client_i |
Remediation
Add a structured invocation log to every authenticated MCP tool handler. A minimum example: Python (FastMCP): import logging logger = logging.getLogger(__name__) @mcp.tool() def my_tool(args, ctx): logger.info( "tool.invoke", extra={ "tool": "my_tool", "user_id": ctx.user_id, "request_id": ctx.request_id, }, ) ... TypeScript (@modelcontextprotocol/sdk): import pi
MCP server has authentication wired but no invocation log. An authenticated server that never logs is a forensics dead-end: unauthorized actions cannot be detected, attributed, or reconstructed after the fact. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Fix: add a structured `logger.info("tool.invoke", ...)` call to every authenticated tool handler with at minimum the tool name, caller identity, and request id. Ship invocation events to a retention sink (CloudWatc
Evidence
| 1 | # /// script |
| 2 | # dependencies = [] |
| 3 | # /// |
| 4 | |
| 5 | """MCPServer Text Me Server |
| 6 | -------------------------------- |
| 7 | This defines a simple MCPServer server that sends a text message to a phone number via https://surgemsg.com/. |
| 8 | |
| 9 | To run this example, create a `.env` file with the following values: |
| 10 | |
| 11 | SURGE_API_KEY=... |
| 12 | SURGE_ACCOUNT_ID=... |
| 13 | SURGE_MY_PHONE_NUMBER=... |
| 14 | SURGE_MY_FIRST_NAME=... |
| 15 | SURGE_MY_LAST_NAME=... |
| 16 | |
| 17 | Visit https://surgemsg.com/ and click "Get Started" to obtain these values. |
| 18 | """ |
| 19 | |
| 20 | from typing import Annot |
Remediation
Add a structured invocation log to every authenticated MCP tool handler. A minimum example: Python (FastMCP): import logging logger = logging.getLogger(__name__) @mcp.tool() def my_tool(args, ctx): logger.info( "tool.invoke", extra={ "tool": "my_tool", "user_id": ctx.user_id, "request_id": ctx.request_id, }, ) ... TypeScript (@modelcontextprotocol/sdk): import pi
LLM consensus
MCP server has authentication wired but no invocation log. An authenticated server that never logs is a forensics dead-end: unauthorized actions cannot be detected, attributed, or reconstructed after the fact. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Fix: add a structured `logger.info("tool.invoke", ...)` call to every authenticated tool handler with at minimum the tool name, caller identity, and request id. Ship invocation events to a retention sink (CloudWatc
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/oauth_server.py |
| 3 | """ |
| 4 | |
| 5 | from pydantic import AnyHttpUrl |
| 6 | |
| 7 | from mcp.server.auth.provider import AccessToken, TokenVerifier |
| 8 | from mcp.server.auth.settings import AuthSettings |
| 9 | from mcp.server.mcpserver import MCPServer |
| 10 | |
| 11 | |
| 12 | class SimpleTokenVerifier(TokenVerifier): |
| 13 | """Simple token verifier for demonstration.""" |
| 14 | |
| 15 | async def verify_token(self, token: str) -> AccessToken | None: |
| 16 | pass # This is where you would implement actual to |
Remediation
Add a structured invocation log to every authenticated MCP tool handler. A minimum example: Python (FastMCP): import logging logger = logging.getLogger(__name__) @mcp.tool() def my_tool(args, ctx): logger.info( "tool.invoke", extra={ "tool": "my_tool", "user_id": ctx.user_id, "request_id": ctx.request_id, }, ) ... TypeScript (@modelcontextprotocol/sdk): import pi
LLM consensus
MCP server has authentication wired but no invocation log. An authenticated server that never logs is a forensics dead-end: unauthorized actions cannot be detected, attributed, or reconstructed after the fact. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Fix: add a structured `logger.info("tool.invoke", ...)` call to every authenticated tool handler with at minimum the tool name, caller identity, and request id. Ship invocation events to a retention sink (CloudWatc
Evidence
| 1 | from dataclasses import dataclass |
| 2 | from typing import Generic, Literal, Protocol, TypeVar |
| 3 | from urllib.parse import parse_qs, urlencode, urlparse, urlunparse |
| 4 | |
| 5 | from pydantic import AnyUrl, BaseModel |
| 6 | |
| 7 | from mcp.shared.auth import OAuthClientInformationFull, OAuthToken |
| 8 | |
| 9 | |
| 10 | class AuthorizationParams(BaseModel): |
| 11 | state: str | None |
| 12 | scopes: list[str] | None |
| 13 | code_challenge: str |
| 14 | redirect_uri: AnyUrl |
| 15 | redirect_uri_provided_explicitly: bool |
| 16 | resource: str | None = None # RFC 8707 resource |
Remediation
Add a structured invocation log to every authenticated MCP tool handler. A minimum example: Python (FastMCP): import logging logger = logging.getLogger(__name__) @mcp.tool() def my_tool(args, ctx): logger.info( "tool.invoke", extra={ "tool": "my_tool", "user_id": ctx.user_id, "request_id": ctx.request_id, }, ) ... TypeScript (@modelcontextprotocol/sdk): import pi
MCP tool file registers a tool, performs a destructive sink (fs.unlink / shutil.rmtree / DROP TABLE / DELETE FROM / TRUNCATE / UPDATE ... SET / HTTP DELETE|PUT|PATCH / subprocess / exec / spawn), and emits no audit event anywhere in the file. Without an audit event, an investigator cannot answer "who deleted record X on day Y?" โ the irreversible action leaves no trail. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Distinct from MCP-201 (no confirmation) and MCP-283
Evidence
| 1 | # /// script |
| 2 | # dependencies = ["pydantic-ai-slim[openai]", "asyncpg", "numpy", "pgvector"] |
| 3 | # /// |
| 4 | |
| 5 | # uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector |
| 6 | |
| 7 | """Recursive memory system inspired by the human brain's clustering of memories. |
| 8 | Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient |
| 9 | similarity search. |
| 10 | """ |
| 11 | |
| 12 | import asyncio |
| 13 | import math |
| 14 | import os |
| 15 | from dataclasses import dataclass |
| 16 | from datetime import datetime, timezone |
| 17 | from pathlib import Path |
| 18 | from typing import An |
Remediation
Add a structured audit-event emit immediately after every destructive sink. Minimum schema: actor, action, target, outcome, request_id. Python: from acme.audit import audit_log @mcp.tool() def delete_record(token: str, record_id: str) -> dict: actor = verify_token(token) db.execute("DELETE FROM records WHERE id = %s", (record_id,)) audit_log( actor=actor.sub, action="delete_record", target=record_id, outcome=
LLM consensus
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 304 | # Always try to terminate the process itself as well |
| 305 | try: |
| 306 | process.terminate() |
| 307 | except Exception: |
| 308 | pass |
| 309 | |
| 310 | |
| 311 | @deprecated( |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 166 | async def wait_for_store() -> None: |
| 167 | try: |
| 168 | await self._store.wait_for_update(task_id) |
| 169 | except Exception: |
| 170 | pass |
| 171 | finally: |
| 172 | tg.cancel_scope.cancel() |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 298 | finally: |
| 299 | if win32api: |
| 300 | try: |
| 301 | win32api.CloseHandle(job) |
| 302 | except Exception: |
| 303 | pass |
| 304 | |
| 305 | # Always try to terminate the process itself as well |
| 306 | try: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 39 | return |
| 40 | |
| 41 | try: |
| 42 | os.killpg(pgid, signal.SIGKILL) |
| 43 | except ProcessLookupError: |
| 44 | pass |
| 45 | |
| 46 | except (ProcessLookupError, PermissionError, OSError) as e: |
| 47 | logger.warning(f"Process group termination failed for PID {pid}: {e}, falling back to simple terminate") |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 174 | async def wait_for_queue() -> None: |
| 175 | try: |
| 176 | await self._queue.wait_for_message(task_id) |
| 177 | except Exception: |
| 178 | pass |
| 179 | finally: |
| 180 | tg.cancel_scope.cancel() |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
echo_tool
add
+3 more โ click to filter
Example
greet
mcp-conformance-test-server
get_weather
book_table
my_tool
textme