High risk. Don't ship without significant remediation.
Scanned 5/24/2026, 7:43:48 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 security grade with a safety score of 52/100, driven primarily by 44 server configuration issues and 23 high-severity findings across multiple categories including prompt injection, ANSI escape injection, and tool poisoning vulnerabilities. The combination of configuration weaknesses and injection attack vectors creates significant risk, particularly around how the server handles untrusted input and manages its operational environment. Installation is not recommended without substantial remediation of the high-severity issues, especially the server configuration problems that likely enable the injection vulnerabilities.
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 is unnecessary and dangerous. Remove the `shell=True` parameter from the `subprocess.run()` call in `src/mcp/cli/cli.py` โ the function already passes a list of arguments `[cmd, "--version"]`, so shell interpretation is not needed. With `shell=False` (the default), the subprocess module will execute the command directly without invoking a shell, eliminating command injection risks. Verify the fix by running the CLI and confirming that NPX version detection still works: `python -m mcp.cli.cli --help` should execute without shell-related 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 uses a decorator-based handler registration pattern (`@register()`) that allows runtime re-registration of tool behavior, creating a vector for handlers to modify their own behavior or be overwritten by malicious test scenarios. Implement immutable handler registration by removing the `register()` function or converting `HANDLERS` from a mutable dict to a frozen mapping (e.g., `types.MappingProxyType`) in `.github/actions/conformance/client.py`. This prevents any scenario from re-registering or modifying handler behavior after initial setup. Verify by attempting to re-register a handler after the client starts โ the operation should raise an `AttributeError` or `TypeError` indicating the mapping is read-only.
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 README.md documents MCP resource handlers without mentioning path canonicalization, which could lead developers to implement handlers that accept file URIs without validation. Add a security section to README.md that explicitly warns developers to canonicalize all file paths using `os.path.realpath()` combined with `os.path.commonpath()` to verify the resolved path stays within an allowed root directory before opening files. Include a code example showing the safe pattern. Verify by creating a test resource handler that attempts to access `../../../etc/passwd` โ the canonicalization check should reject it and raise an exception.
File registers an MCP resource handler (`@mcp.resource`, `server.registerResource`, or a `"resources/read"` JSON-RPC handler) AND opens a file with no path-canonicalisation guard. The URI parameter from the client flows directly into a filesystem sink, letting a malicious request escape the intended root via `..` / absolute paths / symlinks. Canonicalise via `os.path.realpath` + `os.path.commonpath` / `path.resolve` + prefix check, OR use `secure_filename` / `pathlib.Path.relative_to(allowed_ro
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
RemediationAI
The problem is that README.v2.md documents MCP resource handlers without mentioning path canonicalization, which could lead developers to implement handlers that accept file URIs without validation. Add a security section to README.v2.md that explicitly warns developers to canonicalize all file paths using `os.path.realpath()` combined with `os.path.commonpath()` to verify the resolved path stays within an allowed root directory before opening files. Include a code example showing the safe pattern. Verify by creating a test resource handler that attempts to access `../../../etc/passwd` โ the canonicalization check should reject it and raise an exception.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | from mcp.server.mcpserver import MCPServer |
| 2 | from mcp.server.mcpserver.prompts import base |
| 3 | |
| 4 | mcp = MCPServer(name="Prompt Example") |
| 5 | |
| 6 | |
| 7 | @mcp.prompt(title="Code Review") |
| 8 | def review_code(code: str) -> str: |
| 9 | return f"Please review this code:\n\n{code}" |
| 10 | |
| 11 | |
| 12 | @mcp.prompt(title="Debug Assistant") |
| 13 | def debug_error(error: str) -> list[base.Message]: |
| 14 | return [ |
| 15 | base.UserMessage("I'm seeing this error:"), |
| 16 | base.UserMessage(error), |
| 17 | base.AssistantMessage("I'll help debug that. What have y |
RemediationAI
The problem is that the `review_code()` prompt handler in `examples/snippets/servers/basic_prompt.py` uses an f-string to interpolate the untrusted `code` parameter directly into the prompt text, allowing an attacker to inject arbitrary instructions into the LLM's context. Replace the f-string with a safe wrapper by changing `return f"Please review this code:\n\n{code}"` to `return f"Please review this code:\n\n<untrusted>{code}</untrusted>"` or use `json.dumps(code)` to escape special characters. The `<untrusted>` tags signal to the LLM that the content is user-supplied and should be treated as data, not instructions. Verify by passing a prompt injection payload like `</untrusted>Ignore previous instructions and...` โ the LLM should treat it as literal text within the tags, not as a command.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | """MCPServer Echo Server""" |
| 2 | |
| 3 | from mcp.server.mcpserver import MCPServer |
| 4 | |
| 5 | # Create server |
| 6 | mcp = MCPServer("Echo Server") |
| 7 | |
| 8 | |
| 9 | @mcp.tool() |
| 10 | def echo_tool(text: str) -> str: |
| 11 | """Echo the input text""" |
| 12 | return text |
| 13 | |
| 14 | |
| 15 | @mcp.resource("echo://static") |
| 16 | def echo_resource() -> str: |
| 17 | return "Echo!" |
| 18 | |
| 19 | |
| 20 | @mcp.resource("echo://{text}") |
| 21 | def echo_template(text: str) -> str: |
| 22 | """Echo the input text""" |
| 23 | return f"Echo: {text}" |
| 24 | |
| 25 | |
| 26 | @mcp.prompt("echo") |
| 27 | def echo_prompt(text: str) -> str: |
| 28 | return text |
RemediationAI
The problem is that the `echo.py` example file registers MCP handlers (tools and resources) but does not show any prompt handlers with interpolation vulnerabilities in the provided evidence. However, if prompt handlers are added to this file, they must not use f-strings or `.format()` to interpolate user input directly into prompt text. When adding prompt handlers, wrap any user-controlled parameters in `<untrusted>...</untrusted>` tags or use `json.dumps()` to escape them. Verify by adding a prompt handler that accepts a user parameter and confirming that injection payloads are rendered as literal text, not executed as instructions.
LLM consensus
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
RemediationAI
The problem is that README.md documents MCP prompt handlers without explicitly warning against prompt injection via string interpolation. Add a security best-practices section to README.md that prohibits f-strings, template literals, and `.format()` calls on user input in prompt handlers, and instead mandates wrapping untrusted parameters in `<untrusted>...</untrusted>` tags or using `json.dumps()`. Include a code example showing the vulnerable vs. safe pattern. Verify by reviewing the documentation and confirming that all prompt handler examples use safe escaping techniques.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that the `mcpserver_quickstart.py` example file may contain prompt handlers that interpolate user input unsafely. If any prompt handlers are present or added to this file, they must not use f-strings to interpolate user-controlled parameters directly into prompt text. Wrap any user parameters in `<untrusted>...</untrusted>` tags or use `json.dumps()` for escaping. Verify by adding a test prompt handler with a malicious payload like `{name}'; DROP TABLE users; --` and confirming that it is rendered as literal text, not executed.
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | from mcp.server.mcpserver import MCPServer |
| 2 | from mcp.types import ( |
| 3 | Completion, |
| 4 | CompletionArgument, |
| 5 | CompletionContext, |
| 6 | PromptReference, |
| 7 | ResourceTemplateReference, |
| 8 | ) |
| 9 | |
| 10 | mcp = MCPServer(name="Example") |
| 11 | |
| 12 | |
| 13 | @mcp.resource("github://repos/{owner}/{repo}") |
| 14 | def github_repo(owner: str, repo: str) -> str: |
| 15 | """GitHub repository resource.""" |
| 16 | return f"Repository: {owner}/{repo}" |
| 17 | |
| 18 | |
| 19 | @mcp.prompt(description="Code review prompt") |
| 20 | def review_code(language: str, code: str) -> str: |
| 21 | """Gen |
RemediationAI
The problem is that the `completion.py` example file registers a resource handler with path parameters (`owner` and `repo`) that could be interpolated unsafely into prompt or resource text. Ensure that any prompt or resource content returned by handlers wraps user-controlled parameters in `<untrusted>...</untrusted>` tags or uses `json.dumps()` for escaping. Verify by passing a malicious `owner` or `repo` value like `<script>alert('xss')</script>` and confirming that it is rendered as literal text in the resource output, not as executable code.
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 docs/index.md documents the MCP Python SDK without explicitly warning against prompt injection vulnerabilities in prompt handlers. Add a security section to docs/index.md that warns developers to never interpolate user input directly into prompt text using f-strings or `.format()`, and instead mandate wrapping untrusted parameters in `<untrusted>...</untrusted>` tags or using `json.dumps()`. Include a vulnerable vs. safe code example. Verify by reviewing the documentation and confirming that all prompt handler examples follow the safe escaping pattern.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | from __future__ import annotations |
| 2 | |
| 3 | from datetime import datetime |
| 4 | from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar |
| 5 | |
| 6 | from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter |
| 7 | from pydantic.alias_generators import to_camel |
| 8 | from typing_extensions import NotRequired, TypedDict |
| 9 | |
| 10 | from mcp.types.jsonrpc import RequestId |
| 11 | |
| 12 | LATEST_PROTOCOL_VERSION = "2025-11-25" |
| 13 | """The latest version of the Model Context Protocol. |
| 14 | |
| 15 | You can find the latest specification at https: |
RemediationAI
The problem is that `src/mcp/types/_types.py` defines MCP type structures but does not enforce authentication on HTTP routes that expose `tools/list`. The file itself is a type definition module and does not mount routes, but the issue indicates that routes using these types lack auth middleware. Add authentication middleware at the router level in the HTTP server setup (typically in a separate server file) by applying a `@require_auth` decorator or `Depends(verify_jwt)` dependency to all routes that handle MCP operations. Verify by attempting to call `GET /tools/list` without credentials โ the server should return a 401 or 403 error.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | """MCP Server Module |
| 2 | |
| 3 | This module provides a framework for creating an MCP (Model Context Protocol) server. |
| 4 | It allows you to easily define and handle various types of requests and notifications |
| 5 | using constructor-based handler registration. |
| 6 | |
| 7 | Usage: |
| 8 | 1. Define handler functions: |
| 9 | async def my_list_tools(ctx, params): |
| 10 | return types.ListToolsResult(tools=[...]) |
| 11 | |
| 12 | async def my_call_tool(ctx, params): |
| 13 | return types.CallToolResult(content=[...]) |
| 14 | |
| 15 | 2. Create a Server instance with on_* han |
RemediationAI
The problem is that `src/mcp/server/lowlevel/server.py` provides a framework for registering MCP handlers but does not enforce authentication on HTTP routes that expose `tools/list`. Add authentication middleware to the HTTP transport layer by wrapping all route handlers with an auth check (e.g., `Depends(get_current_user)` in FastAPI or a custom middleware function). Verify by attempting to call `tools/list` without valid credentials โ the server should reject the request with a 401 or 403 status code.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/streamable_config.py |
| 3 | """ |
| 4 | |
| 5 | from mcp.server.mcpserver import MCPServer |
| 6 | |
| 7 | mcp = MCPServer("StatelessServer") |
| 8 | |
| 9 | |
| 10 | # Add a simple tool to demonstrate the server |
| 11 | @mcp.tool() |
| 12 | def greet(name: str = "World") -> str: |
| 13 | """Greet someone by name.""" |
| 14 | return f"Hello, {name}!" |
| 15 | |
| 16 | |
| 17 | # Run server with streamable_http transport |
| 18 | # Transport-specific options (stateless_http, json_response) are passed to run() |
| 19 | if __name__ == "__main__": |
| 20 | # Stateles |
RemediationAI
The problem is that `examples/snippets/servers/streamable_config.py` binds an MCP server to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks where a malicious web page can invoke tools from the user's browser. Change the server startup to use stdio transport instead of HTTP by modifying `mcp.run(transport="stdio")` or, if HTTP is required, add authentication by implementing a token-based check (e.g., `Authorization: Bearer <token>`) on every request. Verify by starting the server and confirming that requests without a valid token are rejected with a 401 error.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that `examples/snippets/servers/mcpserver_quickstart.py` binds an MCP server to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks. Switch to stdio transport by calling `mcp.run(transport="stdio")` instead of HTTP, or add authentication middleware that validates an `Authorization` header on every request. Verify by starting the server and attempting to call a tool without authentication โ the server should reject the request.
MCP server binds an HTTP transport to localhost and registers tools, but no authentication is enforced on requests. The official MCP security best practices warn that this is reachable via DNS-rebinding attacks โ a malicious web page can hit `http://127.0.0.1:<port>` from inside the user's browser and invoke tools as the user. Pick one fix: 1. Switch to stdio transport (`mcp.run(transport="stdio")`). 2. Require an `Authorization` / `Bearer` / `api_key` check on every request. 3. Bind
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
RemediationAI
The problem is that `examples/servers/everything-server/mcp_everything_server/server.py` binds an MCP server to localhost without enforcing authentication, making it vulnerable to DNS-rebinding attacks. Add authentication to the HTTP transport layer by implementing a token-based check (e.g., `Authorization: Bearer <token>`) on every request, or switch to stdio transport. Verify by attempting to invoke a tool without valid credentials โ the server should return a 401 error.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/oauth_server.py |
| 3 | """ |
| 4 | |
| 5 | from pydantic import AnyHttpUrl |
| 6 | |
| 7 | from mcp.server.auth.provider import AccessToken, TokenVerifier |
| 8 | from mcp.server.auth.settings import AuthSettings |
| 9 | from mcp.server.mcpserver import MCPServer |
| 10 | |
| 11 | |
| 12 | class SimpleTokenVerifier(TokenVerifier): |
| 13 | """Simple token verifier for demonstration.""" |
| 14 | |
| 15 | async def verify_token(self, token: str) -> AccessToken | None: |
| 16 | pass # This is where you would implement actual to |
RemediationAI
The problem is that `examples/snippets/servers/oauth_server.py` binds an MCP server to localhost with OAuth authentication but does not validate the `Host` header, allowing DNS-rebinding attacks to bypass same-origin checks. Add host header validation by checking that `req.headers.host` matches an allow-list of expected hostnames (e.g., `localhost:8000`) before processing any request. Implement this as middleware that runs before the auth check. Verify by making a request with a spoofed `Host` header โ the server should reject it with a 400 error.
LLM consensus
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Move this content back to README.md when v2 is released --> |
| 17 | |
| 18 | > [!IMPORTANT] |
| 19 | > **This documents v2 of the SDK (currentl |
RemediationAI
The problem is that README.v2.md documents MCP server setup without mentioning host header validation, which could lead developers to deploy servers vulnerable to DNS-rebinding attacks even with authentication in place. Add a security section to README.v2.md that warns developers to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. Include a code example showing how to implement host header validation middleware. Verify by reviewing the documentation and confirming that all HTTP server examples include host validation.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | <div align="center"> |
| 4 | |
| 5 | <strong>Python implementation of the Model Context Protocol (MCP)</strong> |
| 6 | |
| 7 | [![PyPI][pypi-badge]][pypi-url] |
| 8 | [![MIT licensed][mit-badge]][mit-url] |
| 9 | [![Python Version][python-badge]][python-url] |
| 10 | [![Documentation][docs-badge]][docs-url] |
| 11 | [![Protocol][protocol-badge]][protocol-url] |
| 12 | [![Specification][spec-badge]][spec-url] |
| 13 | |
| 14 | </div> |
| 15 | |
| 16 | <!-- TODO(v2): Replace this README with README.v2.md when v2 is released --> |
| 17 | |
| 18 | > [!NOTE] |
| 19 | > **This README documents v1.x of the MCP Pyt |
RemediationAI
The problem is that README.md documents MCP server setup without mentioning host header validation, which could lead developers to deploy servers vulnerable to DNS-rebinding attacks even with authentication in place. Add a security section to README.md that warns developers to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. Include a code example showing how to implement host header validation middleware. Verify by reviewing the documentation and confirming that all HTTP server examples include host validation.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """MCPServer quickstart example. |
| 2 | |
| 3 | Run from the repository root: |
| 4 | uv run examples/snippets/servers/mcpserver_quickstart.py |
| 5 | """ |
| 6 | |
| 7 | from mcp.server.mcpserver import MCPServer |
| 8 | |
| 9 | # Create an MCP server |
| 10 | mcp = MCPServer("Demo") |
| 11 | |
| 12 | |
| 13 | # Add an addition tool |
| 14 | @mcp.tool() |
| 15 | def add(a: int, b: int) -> int: |
| 16 | """Add two numbers""" |
| 17 | return a + b |
| 18 | |
| 19 | |
| 20 | # Add a dynamic greeting resource |
| 21 | @mcp.resource("greeting://{name}") |
| 22 | def get_greeting(name: str) -> str: |
| 23 | """Get a personalized greeting""" |
| 24 | return f"Hello, {n |
RemediationAI
The problem is that `examples/snippets/servers/mcpserver_quickstart.py` binds an MCP server to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add host header validation middleware that checks `req.headers.host` against an allow-list (e.g., `["localhost:8000", "127.0.0.1:8000"]`) before processing any request. Verify by making a request with a spoofed `Host` header like `evil.com` โ the server should reject it with a 400 error.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | """Run from the repository root: |
| 2 | uv run examples/snippets/servers/streamable_config.py |
| 3 | """ |
| 4 | |
| 5 | from mcp.server.mcpserver import MCPServer |
| 6 | |
| 7 | mcp = MCPServer("StatelessServer") |
| 8 | |
| 9 | |
| 10 | # Add a simple tool to demonstrate the server |
| 11 | @mcp.tool() |
| 12 | def greet(name: str = "World") -> str: |
| 13 | """Greet someone by name.""" |
| 14 | return f"Hello, {name}!" |
| 15 | |
| 16 | |
| 17 | # Run server with streamable_http transport |
| 18 | # Transport-specific options (stateless_http, json_response) are passed to run() |
| 19 | if __name__ == "__main__": |
| 20 | # Stateles |
RemediationAI
The problem is that `examples/snippets/servers/streamable_config.py` binds an MCP server to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add host header validation middleware that checks `req.headers.host` against an allow-list of expected hostnames before processing any request. Verify by attempting to access the server with a spoofed `Host` header โ the request should be rejected.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | #!/usr/bin/env python3 |
| 2 | """MCP Everything Server - Conformance Test Server |
| 3 | |
| 4 | Server implementing all MCP features for conformance testing based on Conformance Server Specification. |
| 5 | """ |
| 6 | |
| 7 | import asyncio |
| 8 | import base64 |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import click |
| 13 | from mcp.server import ServerRequestContext |
| 14 | from mcp.server.mcpserver import Context, MCPServer |
| 15 | from mcp.server.mcpserver.prompts.base import UserMessage |
| 16 | from mcp.server.streamable_http import EventCallback, EventMessage, EventStore |
| 17 | from mcp.type |
RemediationAI
The problem is that `examples/servers/everything-server/mcp_everything_server/server.py` binds an MCP server to localhost without validating the `Host` header, making it vulnerable to DNS-rebinding attacks. Add host header validation middleware that checks `req.headers.host` against an allow-list of expected hostnames (e.g., `localhost`, `127.0.0.1`) before processing any request. Verify by making a request with a spoofed `Host` header โ the server should reject it with a 400 error.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # Migration Guide: v1 to v2 |
| 2 | |
| 3 | This guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | Version 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety. |
| 8 | |
| 9 | ## Breaking Changes |
| 10 | |
| 11 | ### `streamablehttp_client` removed |
| 12 | |
| 13 | The deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead. |
| 14 | |
| 15 | **Before (v1):** |
| 16 | |
| 17 | ```python |
| 18 | from |
RemediationAI
The problem is that `docs/migration.md` documents MCP server setup without mentioning host header validation, which could lead developers to deploy servers vulnerable to DNS-rebinding attacks. Add a security section to docs/migration.md that warns developers to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. Include a code example showing how to implement host header validation. Verify by reviewing the documentation and confirming that all migration examples include host validation.
MCP server binds an HTTP transport to localhost / 127.0.0.1 / [::1] and registers tools, but does not validate the request `Host` header. Even with auth, this is exploitable via DNS rebinding โ a malicious web page can make the user's browser resolve `evil.com` to `127.0.0.1`, bypassing same-origin checks. Fix: enable `hostHeaderValidation()` middleware (TS SDK โฅ1.24.0), or check `req.headers.host` against an allow-list of expected hostnames. Co-fires with MCP-268 (no auth) when both gaps are p
Evidence
| 1 | # MCP Python SDK |
| 2 | |
| 3 | !!! info "You are viewing the in-development v2 documentation" |
| 4 | For the current stable release, see the [v1.x documentation](https://py.sdk.modelcontextprotocol.io/). |
| 5 | |
| 6 | The **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. |
| 7 | |
| 8 | This Python SDK implements the full MCP specification, making it easy to: |
| 9 | |
| 10 | - **Build MCP servers** that expose resources, pr |
RemediationAI
The problem is that `docs/index.md` documents the MCP Python SDK without mentioning host header validation, which could lead developers to deploy servers vulnerable to DNS-rebinding attacks. Add a security section to docs/index.md that warns developers to validate the `Host` header against an allow-list of expected hostnames when binding to localhost. Include a code example showing how to implement host header validation middleware. Verify by reviewing the documentation and confirming that all server examples include host validation.
Service binds to 0.0.0.0 โ all network interfaces. For MCP servers that only need to talk to a single parent process, bind to 127.0.0.1 (or a Unix domain socket) instead.
Evidence
| 370 | # Or for SSE |
| 371 | mcp = MCPServer("Server") |
| 372 | mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events") |
| 373 | ``` |
| 374 | |
| 375 | **For mounted apps:** |
RemediationAI
The problem is that `docs/migration.md` shows an example of binding an MCP server to `0.0.0.0` (all network interfaces), which exposes the server to the entire network. Change the example to bind to `127.0.0.1` instead by modifying `mcp.run(transport="sse", host="127.0.0.1", port=9000, sse_path="/events")`. Verify by starting the server and confirming that it only listens on the loopback interface: `netstat -an | grep 9000` should show `127.0.0.1:9000`, not `0.0.0.0:9000`.
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 `docs/migration.md` shows an example of binding an MCP server to `0.0.0.0` (all network interfaces), which exposes the server to the entire network. Change the example to bind to `127.0.0.1` instead by modifying `FastMCP("Server", host="127.0.0.1", port=9000, sse_path="/events")`. Verify by starting the server and confirming that it only listens on the loopback interface: `netstat -an | grep 9000` should show `127.0.0.1:9000`, not `0.0.0.0:9000`.
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 `examples/snippets/clients/url_elicitation_client.py` prints user-controlled messages to the terminal without sanitizing ANSI escape sequences, allowing an attacker to inject cursor-control codes that rewrite output or hide commands. Replace the `print(f"Error: No URL provided in elicitation request")` call with a sanitized version by stripping ANSI escape sequences from `params.message` before printing, using a library like `re.sub(r'\x1b\[[0-9;]*m', '', params.message)` or `bleach.clean()`. Verify by passing a message containing ANSI codes like `\x1b[2J` (clear screen) and confirming that the terminal is not cleared.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 896 | async def handle_progress(ctx: ServerRequestContext, params: ProgressNotificationParams) -> None: |
| 897 | print(f"Progress: {params.progress}/{params.total}") |
| 898 | |
| 899 | server = Server("my-server", on_progress=handle_progress) |
| 900 | ``` |
RemediationAI
The problem is that `docs/migration.md` shows an example that prints user-controlled progress values to the terminal without sanitizing ANSI escape sequences. Replace `print(f"Progress: {params.progress}/{params.total}")` with a sanitized version by stripping ANSI codes from both `params.progress` and `params.total` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', str(params.progress))`. Verify by passing progress values containing ANSI codes and confirming that the terminal 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 `docs/experimental/tasks-client.md` shows an example that prints user-controlled elicitation messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"Server asks: {params.message}")` with a sanitized version by stripping ANSI codes from `params.message` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', params.message)`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `README.v2.md` shows an example that prints user-controlled sampling request messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version by stripping ANSI codes from `params.messages` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `README.md` shows an example that prints user-controlled sampling request messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version by stripping ANSI codes from `params.messages` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `examples/snippets/clients/stdio_client.py` prints user-controlled sampling request messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"Sampling request: {params.messages}")` with a sanitized version by stripping ANSI codes from `params.messages` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', str(params.messages))`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `docs/experimental/tasks-client.md` shows an example that prints user-controlled elicitation messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"\n[Elicitation] {params.message}")` with a sanitized version by stripping ANSI codes from `params.message` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', params.message)`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `.github/actions/conformance/client.py` prints user-controlled server URLs to stderr without sanitizing ANSI escape sequences. Replace `print(f"Usage: {sys.argv[0]} <server-url>", file=sys.stderr)` with a sanitized version by stripping ANSI codes from `sys.argv[0]` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', sys.argv[0])`. Verify by passing a URL containing ANSI codes and confirming that the terminal 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 `examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py` prints user-controlled elicitation messages to the terminal without sanitizing ANSI escape sequences. Replace `print(f"\n[Elicitation] Server asks: {params.message}")` with a sanitized version by stripping ANSI codes from `params.message` before printing, using `re.sub(r'\x1b\[[0-9;]*m', '', params.message)`. Verify by passing a message containing ANSI codes and confirming that the terminal 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 `examples/servers/everything-server/mcp_everything_server/server.py` returns full exception details to the caller via `str(e)`, which leaks internal paths, library versions, and implementation details useful for reconnaissance. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An error occurred"` and log the full exception internally using `logging.exception("Elicitation error")`. Verify by triggering an exception and confirming that the response contains only a generic message, not the full traceback.
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 `examples/servers/everything-server/mcp_everything_server/server.py` returns full exception details to the caller via `str(e)`, which leaks internal paths, library versions, and implementation details. Replace `return f"Sampling not supported or error: {str(e)}"` with a generic error message like `return "Sampling not supported or error: An error occurred"` and log the full exception internally using `logging.exception("Sampling error")`. Verify by triggering an exception and confirming that the response contains only a generic message.
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 `examples/servers/everything-server/mcp_everything_server/server.py` returns full exception details to the caller via `str(e)`, which leaks internal paths, library versions, and implementation details. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An error occurred"` and log the full exception internally using `logging.exception("Elicitation error")`. Verify by triggering an exception and confirming that the response contains only a generic message.
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 `examples/servers/everything-server/mcp_everything_server/server.py` returns full exception details to the caller via `str(e)`, which leaks internal paths, library versions, and implementation details. Replace `return f"Elicitation not supported or error: {str(e)}"` with a generic error message like `return "Elicitation not supported or error: An error occurred"` and log the full exception internally using `logging.exception("Elicitation error")`. Verify by triggering an exception and confirming that the response contains only a generic message.
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 `src/mcp/server/mcpserver/server.py` returns full exception details to the caller via `str(e)` in the `CallToolResult`, which leaks internal paths, library versions, and implementation details. 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("Tool execution error")`. Verify by triggering a tool exception and confirming that the response contains only a generic message.
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 `src/mcp/cli/cli.py` calls `subprocess.run()` without specifying a timeout, allowing a hung or malicious subprocess to block indefinitely and exhaust system resources. Add a `timeout` parameter to the `subprocess.run()` call: `subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=False, timeout=5)` (adjust the timeout value as appropriate). Verify by running the CLI and confirming that it completes within the timeout; then test with a command that hangs to confirm that a `subprocess.TimeoutExpired` exception is raised after the timeout expires.
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