diff --git a/README.md b/README.md index 993b6006b..94a5eab01 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,20 @@ - [Sampling](#sampling) - [Logging and Notifications](#logging-and-notifications) - [Authentication](#authentication) + - [FastMCP Properties](#fastmcp-properties) + - [Session Properties](#session-properties-and-methods) + - [Request Context Properties](#request-context-properties) - [Running Your Server](#running-your-server) - [Development Mode](#development-mode) - [Claude Desktop Integration](#claude-desktop-integration) - [Direct Execution](#direct-execution) + - [Streamable HTTP Transport](#streamable-http-transport) - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server) - [Advanced Usage](#advanced-usage) - [Low-Level Server](#low-level-server) - [Writing MCP Clients](#writing-mcp-clients) + - [Client Display Utilities](#client-display-utilities) + - [OAuth Authentication for Clients](#oauth-authentication-for-clients) - [Parsing Tool Results](#parsing-tool-results) - [MCP Primitives](#mcp-primitives) - [Server Capabilities](#server-capabilities) @@ -303,6 +309,35 @@ def get_weather(city: str, unit: str = "celsius") -> str: _Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_ +Tools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities: + + +```python +from mcp.server.fastmcp import Context, FastMCP + +mcp = FastMCP(name="Progress Example") + + +@mcp.tool() +async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: + """Execute a task with progress updates.""" + await ctx.info(f"Starting: {task_name}") + + for i in range(steps): + progress = (i + 1) / steps + await ctx.report_progress( + progress=progress, + total=1.0, + message=f"Step {i + 1}/{steps}", + ) + await ctx.debug(f"Completed step {i + 1}") + + return f"Task '{task_name}' completed" +``` + +_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_ + + #### Structured Output Tools will return structured results by default, if their return type @@ -496,7 +531,42 @@ _Full example: [examples/snippets/servers/images.py](https://github.com/modelcon ### Context -The Context object gives your tools and resources access to MCP capabilities: +The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata. + +#### Getting Context in Functions + +To use context in a tool or resource function, add a parameter with the `Context` type annotation: + +```python +from mcp.server.fastmcp import Context, FastMCP + +mcp = FastMCP(name="Context Example") + + +@mcp.tool() +async def my_tool(x: int, ctx: Context) -> str: + """Tool that uses context capabilities.""" + # The context parameter can have any name as long as it's type-annotated + return await process_with_context(x, ctx) +``` + +#### Context Properties and Methods + +The Context object provides the following capabilities: + +- `ctx.request_id` - Unique ID for the current request +- `ctx.client_id` - Client ID if available +- `ctx.fastmcp` - Access to the FastMCP server instance (see [FastMCP Properties](#fastmcp-properties)) +- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods)) +- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties)) +- `await ctx.debug(message)` - Send debug log message +- `await ctx.info(message)` - Send info log message +- `await ctx.warning(message)` - Send warning log message +- `await ctx.error(message)` - Send error log message +- `await ctx.log(level, message, logger_name=None)` - Send log with custom level +- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress +- `await ctx.read_resource(uri)` - Read a resource by URI +- `await ctx.elicit(message, schema)` - Request additional information from user with validation ```python @@ -808,6 +878,99 @@ For a complete example with separate Authorization Server and Resource Server im See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation. +### FastMCP Properties + +The FastMCP server instance accessible via `ctx.fastmcp` provides access to server configuration and metadata: + +- `ctx.fastmcp.name` - The server's name as defined during initialization +- `ctx.fastmcp.instructions` - Server instructions/description provided to clients +- `ctx.fastmcp.settings` - Complete server configuration object containing: + - `debug` - Debug mode flag + - `log_level` - Current logging level + - `host` and `port` - Server network configuration + - `mount_path`, `sse_path`, `streamable_http_path` - Transport paths + - `stateless_http` - Whether the server operates in stateless mode + - And other configuration options + +```python +@mcp.tool() +def server_info(ctx: Context) -> dict: + """Get information about the current server.""" + return { + "name": ctx.fastmcp.name, + "instructions": ctx.fastmcp.instructions, + "debug_mode": ctx.fastmcp.settings.debug, + "log_level": ctx.fastmcp.settings.log_level, + "host": ctx.fastmcp.settings.host, + "port": ctx.fastmcp.settings.port, + } +``` + +### Session Properties and Methods + +The session object accessible via `ctx.session` provides advanced control over client communication: + +- `ctx.session.client_params` - Client initialization parameters and declared capabilities +- `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control +- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion +- `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates +- `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed +- `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed +- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed +- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed + +```python +@mcp.tool() +async def notify_data_update(resource_uri: str, ctx: Context) -> str: + """Update data and notify clients of the change.""" + # Perform data update logic here + + # Notify clients that this specific resource changed + await ctx.session.send_resource_updated(AnyUrl(resource_uri)) + + # If this affects the overall resource list, notify about that too + await ctx.session.send_resource_list_changed() + + return f"Updated {resource_uri} and notified clients" +``` + +### Request Context Properties + +The request context accessible via `ctx.request_context` contains request-specific information and resources: + +- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup + - Database connections, configuration objects, shared services + - Type-safe access to resources defined in your server's lifespan function +- `ctx.request_context.meta` - Request metadata from the client including: + - `progressToken` - Token for progress notifications + - Other client-provided metadata +- `ctx.request_context.request` - The original MCP request object for advanced processing +- `ctx.request_context.request_id` - Unique identifier for this request + +```python +# Example with typed lifespan context +@dataclass +class AppContext: + db: Database + config: AppConfig + +@mcp.tool() +def query_with_config(query: str, ctx: Context) -> str: + """Execute a query using shared database and configuration.""" + # Access typed lifespan context + app_ctx: AppContext = ctx.request_context.lifespan_context + + # Use shared resources + connection = app_ctx.db + settings = app_ctx.config + + # Execute query with configuration + result = connection.execute(query, timeout=settings.query_timeout) + return str(result) +``` + +_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_ + ## Running Your Server ### Development Mode diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index e4db806e7..bce32df52 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -190,6 +190,8 @@ def create_protected_resource_routes( resource_url: AnyHttpUrl, authorization_servers: list[AnyHttpUrl], scopes_supported: list[str] | None = None, + resource_name: str | None = None, + resource_documentation: AnyHttpUrl | None = None, ) -> list[Route]: """ Create routes for OAuth 2.0 Protected Resource Metadata (RFC 9728). @@ -209,6 +211,8 @@ def create_protected_resource_routes( resource=resource_url, authorization_servers=authorization_servers, scopes_supported=scopes_supported, + resource_name=resource_name, + resource_documentation=resource_documentation, # bearer_methods_supported defaults to ["header"] in the model ) diff --git a/src/mcp/server/fastmcp/prompts/prompt_manager.py b/src/mcp/server/fastmcp/prompts/prompt_manager.py deleted file mode 100644 index 389e89624..000000000 --- a/src/mcp/server/fastmcp/prompts/prompt_manager.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Prompt management functionality.""" - -from mcp.server.fastmcp.prompts.base import Prompt -from mcp.server.fastmcp.utilities.logging import get_logger - -logger = get_logger(__name__) - - -class PromptManager: - """Manages FastMCP prompts.""" - - def __init__(self, warn_on_duplicate_prompts: bool = True): - self._prompts: dict[str, Prompt] = {} - self.warn_on_duplicate_prompts = warn_on_duplicate_prompts - - def add_prompt(self, prompt: Prompt) -> Prompt: - """Add a prompt to the manager.""" - logger.debug(f"Adding prompt: {prompt.name}") - existing = self._prompts.get(prompt.name) - if existing: - if self.warn_on_duplicate_prompts: - logger.warning(f"Prompt already exists: {prompt.name}") - return existing - self._prompts[prompt.name] = prompt - return prompt - - def get_prompt(self, name: str) -> Prompt | None: - """Get prompt by name.""" - return self._prompts.get(name) - - def list_prompts(self) -> list[Prompt]: - """List all registered prompts.""" - return list(self._prompts.values()) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index ab6a3d15c..8c459383c 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -149,7 +149,6 @@ def __init__( types.PingRequest: _ping_handler, } self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {} - self.notification_options = NotificationOptions() self._tool_cache: dict[str, types.Tool] = {} logger.debug("Initializing server %r", name) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index 33878ee15..6bf15b531 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -114,20 +114,20 @@ class OAuthMetadata(BaseModel): registration_endpoint: AnyHttpUrl | None = None scopes_supported: list[str] | None = None response_types_supported: list[str] = ["code"] - response_modes_supported: list[Literal["query", "fragment", "form_post"]] | None = None + response_modes_supported: list[str] | None = None grant_types_supported: list[str] | None = None token_endpoint_auth_methods_supported: list[str] | None = None - token_endpoint_auth_signing_alg_values_supported: None = None + token_endpoint_auth_signing_alg_values_supported: list[str] | None = None service_documentation: AnyHttpUrl | None = None ui_locales_supported: list[str] | None = None op_policy_uri: AnyHttpUrl | None = None op_tos_uri: AnyHttpUrl | None = None revocation_endpoint: AnyHttpUrl | None = None revocation_endpoint_auth_methods_supported: list[str] | None = None - revocation_endpoint_auth_signing_alg_values_supported: None = None + revocation_endpoint_auth_signing_alg_values_supported: list[str] | None = None introspection_endpoint: AnyHttpUrl | None = None introspection_endpoint_auth_methods_supported: list[str] | None = None - introspection_endpoint_auth_signing_alg_values_supported: None = None + introspection_endpoint_auth_signing_alg_values_supported: list[str] | None = None code_challenge_methods_supported: list[str] | None = None @@ -139,6 +139,17 @@ class ProtectedResourceMetadata(BaseModel): resource: AnyHttpUrl authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1) + jwks_uri: AnyHttpUrl | None = None scopes_supported: list[str] | None = None bearer_methods_supported: list[str] | None = Field(default=["header"]) # MCP only supports header method + resource_signing_alg_values_supported: list[str] | None = None + resource_name: str | None = None resource_documentation: AnyHttpUrl | None = None + resource_policy_uri: AnyHttpUrl | None = None + resource_tos_uri: AnyHttpUrl | None = None + # tls_client_certificate_bound_access_tokens default is False, but ommited here for clarity + tls_client_certificate_bound_access_tokens: bool | None = None + authorization_details_types_supported: list[str] | None = None + dpop_signing_alg_values_supported: list[str] | None = None + # dpop_bound_access_tokens_required default is False, but ommited here for clarity + dpop_bound_access_tokens_required: bool | None = None diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index fd39eb255..bd9f5a934 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -37,3 +37,25 @@ def test_oidc(self): "userinfo_endpoint": "https://example.com/oauth2/userInfo", } ) + + def test_oauth_with_jarm(self): + """Should not throw when parsing OAuth metadata that includes JARM response modes.""" + OAuthMetadata.model_validate( + { + "issuer": "https://example.com", + "authorization_endpoint": "https://example.com/oauth2/authorize", + "token_endpoint": "https://example.com/oauth2/token", + "scopes_supported": ["read", "write"], + "response_types_supported": ["code", "token"], + "response_modes_supported": [ + "query", + "fragment", + "form_post", + "query.jwt", + "fragment.jwt", + "form_post.jwt", + "jwt", + ], + "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], + } + )