High risk. Don't ship without significant remediation.
Scanned 5/18/2026, 7:50:37 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). The presence of ANSI escape injection vulnerabilities (9), prompt injection risks (7), and one command injection flaw creates meaningful security exposure, though no critical issues were detected. You should address the high-severity findings before deployment, particularly the server configuration problems that could affect how this MCP server operates in your 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, which while not directly vulnerable here, sets a dangerous pattern. The concrete fix is to remove `shell=True` from the `subprocess.run()` call โ change `subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)` to `subprocess.run([cmd, "--version"], check=True, capture_output=True)` since the command list is already safe. This eliminates shell injection by ensuring the subprocess module treats arguments as literal values rather than shell syntax. 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 environment variables and scenario names, enabling silent redefinition of handler logic.
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 HANDLERS dictionary allows runtime re-registration of tool behavior through environment variables and scenario names, enabling a malicious actor to silently redefine handler logic at runtime. The concrete fix is to make the HANDLERS dictionary immutable after initialization โ wrap it with `types.MappingProxyType(HANDLERS)` or use a frozen dataclass instead of a mutable dict, and validate all scenario names against a hardcoded allowlist before lookup. This prevents unauthorized redefinition of handler behavior at runtime. Verify by attempting to modify HANDLERS after initialization and confirming the operation raises an error or has no effect.
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 file documents MCP resource handlers without showing path-canonicalization guards, potentially leading developers to implement unsafe file-serving code. The concrete fix is to add documentation and code examples showing the use of `os.path.realpath()` combined with `os.path.commonpath()` to validate that resolved paths stay within an allowed root directory before opening files. This prevents directory traversal attacks via `..` sequences and symlinks. Verify by testing a resource handler with payloads like `../../../etc/passwd` and confirming access is denied.
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 the README.v2.md file documents MCP resource handlers without showing path-canonicalization guards, potentially leading developers to implement unsafe file-serving code. The concrete fix is to add documentation and code examples showing the use of `os.path.realpath()` combined with `os.path.commonpath()` to validate that resolved paths stay within an allowed root directory before opening files. This prevents directory traversal attacks via `..` sequences and symlinks. Verify by testing a resource handler with payloads like `../../../etc/passwd` and confirming access is denied.
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 uses an f-string to interpolate the untrusted `code` parameter directly into the prompt text returned to the LLM, allowing prompt injection attacks. The concrete fix is to wrap the parameter in XML tags: 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 safely serialize it. This signals to the LLM that the content is untrusted and prevents the attacker from injecting instructions. Verify by passing a prompt like `</untrusted>IGNORE PREVIOUS INSTRUCTIONS` and confirming the LLM treats it as literal text, not a command.
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 server example does not show prompt handlers with untrusted parameter interpolation, but the finding flags it as a pattern risk. The concrete fix is to ensure any prompt handler that accepts parameters wraps them in `<untrusted>...</untrusted>` tags or uses `json.dumps()` before returning โ for example, if a prompt handler were added, use `return f"Echo: <untrusted>{text}</untrusted>"`. This prevents prompt injection if user input flows into prompt text. Verify by adding a prompt handler with user input and confirming the LLM does not execute injected instructions.
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 file documents MCP prompt handlers without showing safe interpolation patterns, potentially leading developers to build vulnerable prompt injection code. The concrete fix is to add documentation and examples showing the use of `<untrusted>...</untrusted>` XML wrapping or `json.dumps()` serialization for any user-controlled parameters passed to prompt text. This prevents prompt injection attacks. Verify by reviewing the updated documentation and confirming it includes a code example with safe parameter handling.
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 quickstart example does not show prompt handlers with untrusted parameter interpolation, but the finding flags it as a pattern risk for future development. The concrete fix is to ensure any prompt handler that accepts parameters wraps them in `<untrusted>...</untrusted>` tags or uses `json.dumps()` before returning โ for example, if a prompt handler were added, use `return f"Prompt: <untrusted>{user_input}</untrusted>"`. This prevents prompt injection if user input flows into prompt text. Verify by adding a prompt handler with user input and confirming the LLM does not execute injected 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 | 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 example defines a resource handler with parameters (`owner`, `repo`) that could be interpolated into prompt text without sanitization, enabling prompt injection. The concrete fix is to ensure any prompt handler that uses these parameters wraps them in `<untrusted>...</untrusted>` tags or uses `json.dumps()` โ for example, change any prompt that includes these to `f"Repo: <untrusted>{owner}/{repo}</untrusted>"`. This prevents prompt injection attacks. Verify by passing malicious values like `owner="</untrusted>IGNORE"` and confirming the LLM treats it as literal text.
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 file documents MCP without showing safe prompt parameter handling patterns, potentially leading developers to build vulnerable prompt injection code. The concrete fix is to add documentation and examples showing the use of `<untrusted>...</untrusted>` XML wrapping or `json.dumps()` serialization for any user-controlled parameters passed to prompt text. This prevents prompt injection attacks. Verify by reviewing the updated documentation and confirming it includes a code example with safe parameter handling.
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 the types file does not show HTTP route definitions, but the finding flags a pattern risk where `tools/list` routes lack authentication. The concrete fix is to add authentication middleware to any HTTP route that handles MCP requests โ for FastAPI, use `Depends(verify_jwt)` or `Depends(get_current_user)` on the route; for Flask, use `@login_required` or a custom auth decorator. This prevents anonymous enumeration of tools. Verify by making an unauthenticated request to the route 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 the server module documentation does not show HTTP route definitions with authentication, potentially leading developers to expose `tools/list` without auth. The concrete fix is to add authentication middleware to any HTTP route that handles MCP requests โ for FastAPI, use `Depends(verify_jwt)` or `Depends(get_current_user)` on the route; for Flask, use `@login_required` or a custom auth decorator. This prevents anonymous enumeration of tools. Verify by making an unauthenticated request to the route and confirming it returns a 401 or 403 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 the streamable_config example binds an HTTP server without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page invokes tools as the user. The concrete fix is to either (1) switch to stdio transport by changing `mcp.run(transport="http")` to `mcp.run(transport="stdio")`, or (2) add an `Authorization` header check to every request via middleware. This eliminates the network attack surface or requires proof of authorization. Verify by attempting to call a tool from a different origin and confirming it is blocked or requires a valid token.
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 the quickstart example binds an HTTP server without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page invokes tools as the user. The concrete fix is to either (1) switch to stdio transport by changing `mcp.run(transport="http")` to `mcp.run(transport="stdio")`, or (2) add an `Authorization` header check to every request via middleware. This eliminates the network attack surface or requires proof of authorization. Verify by attempting to call a tool from a different origin and confirming it is blocked or requires a valid token.
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 the everything-server example binds an HTTP server without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page invokes tools as the user. The concrete fix is to either (1) switch to stdio transport by changing `mcp.run(transport="http")` to `mcp.run(transport="stdio")`, or (2) add an `Authorization` header check to every request via middleware. This eliminates the network attack surface or requires proof of authorization. Verify by attempting to call a tool from a different origin and confirming it is blocked or requires a valid token.
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 the oauth_server example binds an HTTP server to localhost without validating the `Host` header, allowing DNS-rebinding attacks to bypass same-origin checks even with authentication. The concrete fix is to add Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames (e.g., `localhost`, `127.0.0.1`) โ for FastAPI, create a middleware that raises a 400 error if the Host header is not in the allowlist. This prevents DNS-rebinding attacks. 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 the README.v2.md file documents MCP servers without showing Host header validation patterns, potentially leading developers to build servers vulnerable to DNS-rebinding attacks. The concrete fix is to add documentation and examples showing Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames. This prevents DNS-rebinding attacks. Verify by reviewing the updated documentation and confirming it includes a code example with Host header validation.
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 the README.md file documents MCP servers without showing Host header validation patterns, potentially leading developers to build servers vulnerable to DNS-rebinding attacks. The concrete fix is to add documentation and examples showing Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames. This prevents DNS-rebinding attacks. Verify by reviewing the updated documentation and confirming it includes a code example with Host header validation.
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 the quickstart example binds an HTTP server to localhost without validating the `Host` header, allowing DNS-rebinding attacks to bypass same-origin checks even with authentication. The concrete fix is to add Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames (e.g., `localhost`, `127.0.0.1`) โ for FastAPI, create a middleware that raises a 400 error if the Host header is not in the allowlist. This prevents DNS-rebinding attacks. 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 the streamable_config example binds an HTTP server to localhost without validating the `Host` header, allowing DNS-rebinding attacks to bypass same-origin checks even with authentication. The concrete fix is to add Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames (e.g., `localhost`, `127.0.0.1`) โ for FastAPI, create a middleware that raises a 400 error if the Host header is not in the allowlist. This prevents DNS-rebinding attacks. 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 the everything-server example binds an HTTP server to localhost without validating the `Host` header, allowing DNS-rebinding attacks to bypass same-origin checks even with authentication. The concrete fix is to add Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames (e.g., `localhost`, `127.0.0.1`) โ for FastAPI, create a middleware that raises a 400 error if the Host header is not in the allowlist. This prevents DNS-rebinding attacks. 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 the migration.md file documents MCP servers without showing Host header validation patterns, potentially leading developers to build servers vulnerable to DNS-rebinding attacks. The concrete fix is to add documentation and examples showing Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames. This prevents DNS-rebinding attacks. Verify by reviewing the updated documentation and confirming it includes a code example with Host header validation.
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 the docs/index.md file documents MCP servers without showing Host header validation patterns, potentially leading developers to build servers vulnerable to DNS-rebinding attacks. The concrete fix is to add documentation and examples showing Host header validation middleware that checks `req.headers.host` against an allowlist of expected hostnames. This prevents DNS-rebinding attacks. Verify by reviewing the updated documentation and confirming it includes a code example with Host header validation.
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 migration.md file shows an MCP server binding to `0.0.0.0`, exposing it to all network interfaces when it should only be accessible locally. The concrete fix is to change `host="0.0.0.0"` to `host="127.0.0.1"` in the `mcp.run()` call โ for example, change `mcp.run(transport="sse", host="0.0.0.0", port=9000)` to `mcp.run(transport="sse", host="127.0.0.1", port=9000)`. This restricts access to localhost only. Verify by attempting to connect from a remote machine and confirming the connection is refused.
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 migration.md file shows an MCP server binding to `0.0.0.0`, exposing it to all network interfaces when it should only be accessible locally. The concrete fix is to change `host="0.0.0.0"` to `host="127.0.0.1"` in the `mcp.run()` call โ for example, change `FastMCP("Server", host="0.0.0.0", port=9000)` to `FastMCP("Server", host="127.0.0.1", port=9000)`. This restricts access to localhost only. Verify by attempting to connect from a remote machine and confirming the connection is refused.
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 without ANSI escape sanitization, allowing a malicious server to inject cursor-control sequences that rewrite output or hide commands. The concrete fix is to sanitize the message before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', message)` before the `print()` call. This prevents terminal injection attacks. Verify by passing a message with ANSI codes like `\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 without ANSI escape sanitization, allowing a malicious server to inject cursor-control sequences. The concrete fix is to sanitize the values before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', str(params.progress))` before the `print()` call. This prevents terminal injection attacks. Verify by passing progress values with ANSI 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 without ANSI escape sanitization, allowing a malicious server to inject cursor-control sequences. The concrete fix is to sanitize the message before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', params.message)` before the `print()` call. This prevents terminal injection attacks. Verify by passing a message with ANSI codes like `\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
| 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 without ANSI escape sanitization, allowing a malicious LLM to inject cursor-control sequences. The concrete fix is to sanitize the messages before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))` before the `print()` call. This prevents terminal injection attacks. Verify by passing messages with ANSI 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 without ANSI escape sanitization, allowing a malicious LLM to inject cursor-control sequences. The concrete fix is to sanitize the messages before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))` before the `print()` call. This prevents terminal injection attacks. Verify by passing messages with ANSI 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 without ANSI escape sanitization, allowing a malicious LLM to inject cursor-control sequences. The concrete fix is to sanitize the messages before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))` before the `print()` call. This prevents terminal injection attacks. Verify by passing messages with ANSI 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 without ANSI escape sanitization, allowing a malicious server to inject cursor-control sequences. The concrete fix is to sanitize the message before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', params.message)` before the `print()` call. This prevents terminal injection attacks. Verify by passing a message with ANSI codes like `\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
| 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 without ANSI escape sanitization, allowing a malicious URL to inject cursor-control sequences. The concrete fix is to sanitize the URL before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', server_url)` before the `print()` call. This prevents terminal injection attacks. Verify by passing a URL with ANSI 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 without ANSI escape sanitization, allowing a malicious server to inject cursor-control sequences. The concrete fix is to sanitize the message before printing โ use a library like `bleach` or manually strip ANSI escape sequences with a regex like `re.sub(r'\x1b\[[0-9;]*m', '', params.message)` before the `print()` call. This prevents terminal injection attacks. Verify by passing a message with ANSI codes like `\x1b[2J` and confirming the terminal is not cleared.
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 the exception handler returns `str(e)` which may include full tracebacks, exposing internal paths, library versions, and code structure to the caller. The concrete fix is to catch the exception and return a generic error message instead โ change `return f"Elicitation not supported or error: {str(e)}"` to `return "Elicitation not supported or error"` or log the full exception server-side only. This prevents information disclosure. Verify by triggering an exception and confirming the response does not include file paths or stack traces.
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 the exception handler returns `str(e)` which may include full tracebacks, exposing internal paths, library versions, and code structure to the caller. The concrete fix is to catch the exception and return a generic error message instead โ change `return f"Sampling not supported or error: {str(e)}"` to `return "Sampling not supported or error"` or log the full exception server-side only. This prevents information disclosure. Verify by triggering an exception and confirming the response does not include file paths or stack traces.
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 the exception handler returns `str(e)` which may include full tracebacks, exposing internal paths, library versions, and code structure to the caller. The concrete fix is to catch the exception and return a generic error message instead โ change `return f"Elicitation not supported or error: {str(e)}"` to `return "Elicitation not supported or error"` or log the full exception server-side only. This prevents information disclosure. Verify by triggering an exception and confirming the response does not include file paths or stack traces.
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 the exception handler returns `str(e)` which may include full tracebacks, exposing internal paths, library versions, and code structure to the caller. The concrete fix is to catch the exception and return a generic error message instead โ change `return f"Elicitation not supported or error: {str(e)}"` to `return "Elicitation not supported or error"` or log the full exception server-side only. This prevents information disclosure. Verify by triggering an exception and confirming the response does not include file paths or stack traces.
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 the exception handler returns `str(e)` in the `CallToolResult`, which may include full tracebacks and expose internal paths, library versions, and code structure to the caller. The concrete fix is to catch the exception and return a generic error message instead โ change `return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)` to `return CallToolResult(content=[TextContent(type="text", text="Tool execution failed")], is_error=True)` and log the full exception server-side only. This prevents information disclosure. Verify by triggering a tool error and confirming the response does not include file paths or stack traces.
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 the `subprocess.run()` call lacks an explicit timeout, allowing a hung or malicious child process to pin the thread indefinitely. The concrete fix is to add a `timeout` parameter to the `subprocess.run()` call โ change `subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)` to `subprocess.run([cmd, "--version"], check=True, capture_output=True, timeout=5)`. This ensures the subprocess is killed if it exceeds 5 seconds. Verify by running the function and confirming it completes within the timeout even if the subprocess hangs.
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