High risk. Don't ship without significant remediation.
Scanned 5/24/2026, 7:55:56 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 scan also identified critical vulnerabilities including command injection, tool poisoning, prompt injection, and ANSI escape injection risks that could allow attackers to manipulate server behavior or extract sensitive information. Installation is not recommended without significant remediation of these configuration and injection-based security gaps.
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` while executing a command list, which defeats the purpose of using list arguments and creates a command injection risk. Remove the `shell=True` parameter from the `subprocess.run()` call in `src/mcp/cli/cli.py` โ change `subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)` to `subprocess.run([cmd, "--version"], check=True, capture_output=True)`. With `shell=False` (the default), the list arguments are passed directly to the OS without shell interpretation, eliminating injection. Verify the fix by running the CLI and confirming that `npx --version` detection still works without errors.
Conformance test client uses dynamic scenario handler registration via decorator pattern (HANDLERS dict) allowing runtime re-registration of tool behavior through register() function, enabling self-re-registration of handlers with different behavior at runtime.
Evidence
| 1 | """MCP unified conformance test client. |
| 2 | |
| 3 | This client is designed to work with the @modelcontextprotocol/conformance npm package. |
| 4 | It handles all conformance test scenarios via environment variables and CLI arguments. |
| 5 | |
| 6 | Contract: |
| 7 | - MCP_CONFORMANCE_SCENARIO env var -> scenario name |
| 8 | - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios) |
| 9 | - Server URL as last CLI argument (sys.argv[1]) |
| 10 | - Must exit 0 within 30 seconds |
| 11 | |
| 12 | Scenarios: |
| 13 | initialize |
RemediationAI
The problem is that the conformance test client allows dynamic re-registration of tool handlers at runtime via a decorator pattern, which could allow a handler to replace itself with different behavior during execution. Implement immutable handler registration by removing the `register()` function or making the `HANDLERS` dictionary read-only after initialization (e.g., using `types.MappingProxyType` in Python). This prevents runtime re-registration and ensures handler behavior remains consistent. Verify by attempting to re-register a handler after server startup and confirming it raises an error or is silently ignored.
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 MCP resource handlers that read files do not canonicalize the URI path before opening it, allowing attackers to use `../` sequences or absolute paths to escape the intended root directory. Add path canonicalization in the resource handler by calling `os.path.realpath()` on the URI and then validating it is within the allowed root using `os.path.commonpath([allowed_root, canonical_path])` or `pathlib.Path.relative_to(allowed_root)`. This ensures only files within the intended directory can be accessed. Verify by attempting to access a file outside the root (e.g., `../../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 MCP resource handlers that read files do not canonicalize the URI path before opening it, allowing attackers to use `../` sequences or absolute paths to escape the intended root directory. Add path canonicalization in the resource handler by calling `os.path.realpath()` on the URI and then validating it is within the allowed root using `os.path.commonpath([allowed_root, canonical_path])` or `pathlib.Path.relative_to(allowed_root)`. This ensures only files within the intended directory can be accessed. Verify by attempting to access a file outside the root (e.g., `../../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, allowing an attacker to inject arbitrary instructions into the LLM's context. Wrap the parameter in XML tags or use a sanitization function: change `return f"Please review this code:\n\n{code}"` to `return f"Please review this code:\n\n<untrusted>{code}</untrusted>"` or use a dedicated `escape_for_prompt()` function. This signals to the LLM that the content is untrusted and prevents prompt injection. Verify by passing a prompt with LLM instructions (e.g., `Ignore previous instructions and...`) and confirming the LLM treats it as data, not commands.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | """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 proper sanitization of user-controlled input before returning it in prompts or resources, which could allow prompt injection attacks. Wrap any user-controlled parameters in `<untrusted>...</untrusted>` tags or use a sanitization function before returning them in prompt handlers. This ensures the LLM treats the content as data rather than instructions. Verify by testing with prompt injection payloads and confirming they are not executed.
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 documentation examples may show prompt handlers that interpolate untrusted input without sanitization, which could mislead developers into creating vulnerable code. Update all documentation examples to show proper sanitization by wrapping parameters in `<untrusted>...</untrusted>` tags or using `escape_for_prompt()`. This ensures developers follow secure patterns by default. Verify by reviewing all prompt handler examples and confirming none use raw f-string interpolation of user input.
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 demonstrate secure prompt handling patterns, potentially leading developers to create vulnerable code. Update the example to show proper sanitization if any prompt handlers are added โ wrap user-controlled parameters in `<untrusted>...</untrusted>` tags or use a sanitization function. This ensures developers learn secure patterns from the start. Verify by adding a prompt handler to the example and confirming it uses proper sanitization.
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 may show resource handlers that interpolate untrusted parameters (e.g., `owner` and `repo`) without sanitization, which could allow prompt injection or path traversal attacks. Wrap any user-controlled parameters in `<untrusted>...</untrusted>` tags before returning them, or use a sanitization function. This prevents injection attacks. Verify by testing with injection payloads in the `owner` and `repo` parameters and confirming they are treated as data.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | # 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 documentation may show prompt handlers that interpolate untrusted input without sanitization, which could mislead developers into creating vulnerable code. Update all documentation to show proper sanitization by wrapping parameters in `<untrusted>...</untrusted>` tags or using `escape_for_prompt()`. This ensures developers follow secure patterns. Verify by reviewing all prompt handler examples and confirming none use raw interpolation of user input.
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 module does not enforce authentication on HTTP routes that expose MCP `tools/list`, allowing anonymous enumeration of available tools. Add authentication middleware to the HTTP server configuration โ for FastAPI, use `Depends(verify_jwt)` or `Depends(get_current_user)` on all tool-related routes; for Flask, use `@login_required` or a custom auth decorator. This ensures only authenticated clients can enumerate tools. Verify by making an unauthenticated request to the `tools/list` endpoint and confirming it returns a 401 or 403 error.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | """MCP Server Module |
| 2 | |
| 3 | This module provides a framework for creating an MCP (Model Context Protocol) server. |
| 4 | It allows you to easily define and handle various types of requests and notifications |
| 5 | using constructor-based handler registration. |
| 6 | |
| 7 | Usage: |
| 8 | 1. Define handler functions: |
| 9 | async def my_list_tools(ctx, params): |
| 10 | return types.ListToolsResult(tools=[...]) |
| 11 | |
| 12 | async def my_call_tool(ctx, params): |
| 13 | return types.CallToolResult(content=[...]) |
| 14 | |
| 15 | 2. Create a Server instance with on_* han |
RemediationAI
The problem is that the low-level server module does not enforce authentication on HTTP routes that expose MCP `tools/list`, allowing anonymous enumeration of available tools. Add authentication middleware to all tool-related routes โ use FastAPI's `Depends()` with a verification function, or add a custom middleware that checks for valid credentials before processing requests. This ensures only authenticated clients can access tools. Verify by making an unauthenticated request and confirming it is rejected.
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 server binds to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page can invoke tools as the user. Either switch to stdio transport by changing `mcp.run(transport="http")` to `mcp.run(transport="stdio")`, or add authentication by implementing a token verification system and checking it on every request. Stdio transport eliminates the HTTP attack surface entirely. Verify by confirming the server uses stdio transport or by testing that unauthenticated HTTP requests are rejected.
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 to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks. Either switch to stdio transport by changing `mcp.run()` to use `transport="stdio"`, or add authentication middleware that validates a Bearer token or API key on every request. Stdio transport is the simplest fix for local-only servers. Verify by confirming the server uses stdio transport or by testing that unauthenticated requests are rejected.
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 binds to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page can invoke tools as the user. Either switch to stdio transport by changing the transport configuration, or add authentication by implementing token verification and checking it on every request. Stdio transport eliminates the HTTP attack surface. Verify by confirming the server uses stdio transport or by testing that unauthenticated HTTP requests are 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/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 to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks even with authentication in place. Add Host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests, or use a middleware that enforces this check. This prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
LLM consensus
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
RemediationAI
The problem is that the documentation does not mention Host header validation for localhost-bound servers, which could mislead developers into creating DNS-rebinding-vulnerable code. Update the documentation to recommend Host header validation by checking `req.headers.host` against an allow-list of expected hostnames. This ensures developers understand the risk and implement proper mitigations. Verify by reviewing the documentation and confirming it includes Host header validation guidance.
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 does not mention Host header validation for localhost-bound servers, which could mislead developers into creating DNS-rebinding-vulnerable code. Update the README to recommend Host header validation by checking `req.headers.host` against an allow-list of expected hostnames. This ensures developers understand the risk and implement proper mitigations. Verify by reviewing the README and confirming it includes Host header validation guidance.
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 to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add Host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/streamable_config.py |
| 3 | """ |
| 4 | |
| 5 | from mcp.server.mcpserver import MCPServer |
| 6 | |
| 7 | mcp = MCPServer("StatelessServer") |
| 8 | |
| 9 | |
| 10 | # Add a simple tool to demonstrate the server |
| 11 | @mcp.tool() |
| 12 | def greet(name: str = "World") -> str: |
| 13 | """Greet someone by name.""" |
| 14 | return f"Hello, {name}!" |
| 15 | |
| 16 | |
| 17 | # Run server with streamable_http transport |
| 18 | # Transport-specific options (stateless_http, json_response) are passed to run() |
| 19 | if __name__ == "__main__": |
| 20 | # Stateles |
RemediationAI
The problem is that the streamable_config example binds to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add Host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
RemediationAI
The problem is that the everything-server binds to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add Host header validation by checking `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing requests. This prevents attackers from using DNS rebinding to bypass same-origin checks. Verify by making a request with a spoofed `Host` header and confirming it is rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # Migration Guide: v1 to v2 |
| 2 | |
| 3 | This guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | Version 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety. |
| 8 | |
| 9 | ## Breaking Changes |
| 10 | |
| 11 | ### `streamablehttp_client` removed |
| 12 | |
| 13 | The deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead. |
| 14 | |
| 15 | **Before (v1):** |
| 16 | |
| 17 | ```python |
| 18 | from |
RemediationAI
The problem is that the migration guide does not mention Host header validation for localhost-bound servers, which could mislead developers into creating DNS-rebinding-vulnerable code. Update the migration guide to recommend Host header validation by checking `req.headers.host` against an allow-list of expected hostnames. This ensures developers understand the risk and implement proper mitigations. Verify by reviewing the migration guide and confirming it includes Host header validation guidance.
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 documentation does not mention Host header validation for localhost-bound servers, which could mislead developers into creating DNS-rebinding-vulnerable code. Update the documentation to recommend Host header validation by checking `req.headers.host` against an allow-list of expected hostnames. This ensures developers understand the risk and implement proper mitigations. Verify by reviewing the documentation and confirming it includes Host header validation guidance.
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 guide shows an example binding to `0.0.0.0`, which exposes the server to all network interfaces and increases the attack surface unnecessarily. Change `host="0.0.0.0"` to `host="127.0.0.1"` in the example code. This restricts the server to localhost only, which is appropriate for MCP servers that only communicate with a single parent process. Verify by running the example and confirming the server only listens on 127.0.0.1.
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 guide shows an example binding to `0.0.0.0`, which exposes the server to all network interfaces and increases the attack surface unnecessarily. Change `host="0.0.0.0"` to `host="127.0.0.1"` in the example code. This restricts the server to localhost only, which is appropriate for MCP servers that only communicate with a single parent process. Verify by running the example and confirming the server only listens on 127.0.0.1.
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 the `message` parameter is printed directly to the terminal without sanitizing ANSI escape sequences, allowing an attacker to inject cursor-control sequences that rewrite output or hide commands. Replace `print("Error: No URL provided in elicitation request")` with a sanitized version that strips ANSI codes, such as `print(re.sub(r'\x1b\[[0-9;]*m', '', message))` or use a library like `bleach` to sanitize the output. This prevents terminal injection attacks. Verify by passing a message with ANSI escape codes (e.g., `\x1b[2J`) and confirming the output is not manipulated.
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 the `params.progress` and `params.total` values are printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Progress: {params.progress}/{params.total}")` with a sanitized version that strips ANSI codes, such as `print(f"Progress: {re.sub(r'\x1b\[[0-9;]*m', '', str(params.progress))}/{re.sub(r'\x1b\[[0-9;]*m', '', str(params.total))}")`. This prevents terminal injection. Verify by passing progress values with ANSI codes and confirming the output is not manipulated.
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 the `params.message` is printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Server asks: {params.message}")` with a sanitized version that strips ANSI codes, such as `print(f"Server asks: {re.sub(r'\x1b\[[0-9;]*m', '', params.message)}")`. This prevents terminal injection. Verify by passing a message with ANSI escape codes and confirming the output is not manipulated.
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 the `params.messages` are printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version that strips ANSI codes, such as `print(f"Sampling request: {re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))}")`. This prevents terminal injection. Verify by passing messages with ANSI escape codes and confirming the output is not manipulated.
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 the `params.messages` are printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version that strips ANSI codes, such as `print(f"Sampling request: {re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))}")`. This prevents terminal injection. Verify by passing messages with ANSI escape codes and confirming the output is not manipulated.
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 the `params.messages` are printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version that strips ANSI codes, such as `print(f"Sampling request: {re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))}")`. This prevents terminal injection. Verify by passing messages with ANSI escape codes and confirming the output is not manipulated.
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 the `params.message` is printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"\n[Elicitation] {params.message}")` with a sanitized version that strips ANSI codes, such as `print(f"\n[Elicitation] {re.sub(r'\x1b\[[0-9;]*m', '', params.message)}")`. This prevents terminal injection. Verify by passing a message with ANSI escape codes and confirming the output is not manipulated.
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 the `server_url` from `sys.argv[1]` is printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"Usage: {sys.argv[0]} <server-url>", file=sys.stderr)` with a sanitized version that strips ANSI codes, such as `print(f"Usage: {re.sub(r'\x1b\[[0-9;]*m', '', sys.argv[0])} <server-url>", file=sys.stderr)`. This prevents terminal injection. Verify by passing a URL with ANSI escape codes and confirming the output is not manipulated.
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 the `params.message` is printed directly to the terminal without sanitizing ANSI escape sequences, allowing terminal injection attacks. Replace `print(f"\n[Elicitation] Server asks: {params.message}")` with a sanitized version that strips ANSI codes, such as `print(f"\n[Elicitation] Server asks: {re.sub(r'\x1b\[[0-9;]*m', '', params.message)}")`. This prevents terminal injection. Verify by passing a message with ANSI escape codes and confirming the output is not manipulated.
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 contain sensitive information like file paths, library versions, and internal structure. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An unexpected error occurred"` and log the full exception internally using `logging.exception()`. This prevents information disclosure. Verify by triggering an exception and confirming the response does not contain sensitive details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 189 | return f"LLM response: {model_response}" |
| 190 | except Exception as e: |
| 191 | return f"Sampling not supported or error: {str(e)}" |
| 192 | |
| 193 | |
| 194 | class UserResponse(BaseModel): |
RemediationAI
The problem is that the exception handler returns `str(e)` which may contain sensitive information like file paths, library versions, and internal structure. Replace `return f"Sampling not supported or error: {str(e)}"` with a generic error message like `return "Sampling not supported or error: An unexpected error occurred"` and log the full exception internally using `logging.exception()`. This prevents information disclosure. Verify by triggering an exception and confirming the response does not contain sensitive details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 211 | return f"User response: action={result.action}, content={content}" |
| 212 | except Exception as e: |
| 213 | return f"Elicitation not supported or error: {str(e)}" |
| 214 | |
| 215 | |
| 216 | class SEP1034DefaultsSchema(BaseModel): |
RemediationAI
The problem is that the exception handler returns `str(e)` which may contain sensitive information like file paths, library versions, and internal structure. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An unexpected error occurred"` and log the full exception internally using `logging.exception()`. This prevents information disclosure. Verify by triggering an exception and confirming the response does not contain sensitive details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 302 | return f"Elicitation completed: action={result.action}, content={content}" |
| 303 | except Exception as e: |
| 304 | return f"Elicitation not supported or error: {str(e)}" |
| 305 | |
| 306 | |
| 307 | @mcp.tool() |
RemediationAI
The problem is that the exception handler returns `str(e)` which may contain sensitive information like file paths, library versions, and internal structure. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An unexpected error occurred"` and log the full exception internally using `logging.exception()`. This prevents information disclosure. Verify by triggering an exception and confirming the response does not contain sensitive details.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 313 | except MCPError: |
| 314 | raise |
| 315 | except Exception as e: |
| 316 | return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True) |
| 317 | if isinstance(result, CallToolResult): |
| 318 | return result |
| 319 | if isinstance(result, tuple) and len(result) == 2: |
RemediationAI
The problem is that the exception handler returns `str(e)` which may contain sensitive information like file paths, library versions, and internal structure. Replace `return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)` with a generic error message like `return CallToolResult(content=[TextContent(type="text", text="Tool execution failed")], is_error=True)` and log the full exception internally using `logging.exception()`. This prevents information disclosure. Verify by triggering a tool error and confirming the response does not contain sensitive details.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 45 | # Try both npx.cmd and npx.exe on Windows |
| 46 | for cmd in ["npx.cmd", "npx.exe", "npx"]: |
| 47 | try: |
| 48 | subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True) |
| 49 | return cmd |
| 50 | except subprocess.CalledProcessError: |
| 51 | continue |
RemediationAI
The problem is that the `subprocess.run()` call does not specify a timeout, allowing a hung or malicious child process to pin the thread indefinitely. 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, shell=True, timeout=5)`. This ensures the subprocess is terminated if it takes too long. Verify by testing with a command that hangs and confirming it is terminated after the timeout.
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