diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 858ab7db..62334a4a 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -317,12 +317,12 @@ def run( ) -> None: # pragma: no cover """Run an MCP server. - The server can be specified in two ways:\n - 1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n - 2. Import approach: server.py:app - imports and runs the specified server object.\n\n + The server can be specified in two ways: + 1. Module approach: server.py - runs the module directly, expecting a server.run() call. + 2. Import approach: server.py:app - imports and runs the specified server object. Note: This command runs the server directly. You are responsible for ensuring - all dependencies are available.\n + all dependencies are available. For dependency management, use `mcp install` or `mcp dev` instead. """ # noqa: E501 file, server_object = _parse_file_path(file_spec) diff --git a/src/mcp/client/auth/extensions/client_credentials.py b/src/mcp/client/auth/extensions/client_credentials.py index 07f6180b..cb6dafb4 100644 --- a/src/mcp/client/auth/extensions/client_credentials.py +++ b/src/mcp/client/auth/extensions/client_credentials.py @@ -450,7 +450,7 @@ class RFC7523OAuthClientProvider(OAuthClientProvider): # When using private_key_jwt, in a client_credentials flow, we use RFC 7523 Section 2.2 token_data["client_assertion"] = assertion token_data["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - # We need to set the audience to the resource server, the audience is difference from the one in claims + # We need to set the audience to the resource server, the audience is different from the one in claims # it represents the resource server that will validate the token token_data["audience"] = self.context.get_resource_url() diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index f4640775..7f5af518 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -215,6 +215,7 @@ class OAuthContext: class OAuthClientProvider(httpx.Auth): """OAuth2 authentication for httpx. + Handles OAuth flow with automatic client registration and token storage. """ @@ -241,7 +242,7 @@ class OAuthClientProvider(httpx.Auth): callback_handler: Handler for authorization callbacks. timeout: Timeout for the OAuth flow. client_metadata_url: URL-based client ID. When provided and the server - advertises client_id_metadata_document_supported=true, this URL will be + advertises client_id_metadata_document_supported=True, this URL will be used as the client_id instead of performing dynamic client registration. Must be a valid HTTPS URL with a non-root pathname. validate_resource_url: Optional callback to override resource URL validation. diff --git a/src/mcp/client/auth/utils.py b/src/mcp/client/auth/utils.py index 1aa960b9..0ca36b98 100644 --- a/src/mcp/client/auth/utils.py +++ b/src/mcp/client/auth/utils.py @@ -38,7 +38,7 @@ def extract_field_from_www_auth(response: Response, field_name: str) -> str | No def extract_scope_from_www_auth(response: Response) -> str | None: - """Extract scope parameter from WWW-Authenticate header as per RFC6750. + """Extract scope parameter from WWW-Authenticate header as per RFC 6750. Returns: Scope string if found in WWW-Authenticate header, None otherwise @@ -47,7 +47,7 @@ def extract_scope_from_www_auth(response: Response) -> str | None: def extract_resource_metadata_from_www_auth(response: Response) -> str | None: - """Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728. + """Extract protected resource metadata URL from WWW-Authenticate header as per RFC 9728. Returns: Resource metadata URL if found in WWW-Authenticate header, None otherwise @@ -67,8 +67,8 @@ def build_protected_resource_metadata_discovery_urls(www_auth_url: str | None, s 3. Fall back to root-based well-known URI: /.well-known/oauth-protected-resource Args: - www_auth_url: optional resource_metadata url extracted from the WWW-Authenticate header - server_url: server url + www_auth_url: Optional resource_metadata URL extracted from the WWW-Authenticate header + server_url: Server URL Returns: Ordered list of URLs to try for discovery @@ -120,10 +120,10 @@ def get_client_metadata_scopes( def build_oauth_authorization_server_metadata_discovery_urls(auth_server_url: str | None, server_url: str) -> list[str]: - """Generate ordered list of (url, type) tuples for discovery attempts. + """Generate an ordered list of URLs for authorization server metadata discovery. Args: - auth_server_url: URL for the OAuth Authorization Metadata URL if found, otherwise None + auth_server_url: OAuth Authorization Server Metadata URL if found, otherwise None server_url: URL for the MCP server, used as a fallback if auth_server_url is None """ @@ -170,7 +170,7 @@ async def handle_protected_resource_response( Per SEP-985, supports fallback when discovery fails at one URL. Returns: - True if metadata was successfully discovered, False if we should try next URL + ProtectedResourceMetadata if successfully discovered, None if we should try next URL """ if response.status_code == 200: try: @@ -206,7 +206,7 @@ def create_oauth_metadata_request(url: str) -> Request: def create_client_registration_request( auth_server_metadata: OAuthMetadata | None, client_metadata: OAuthClientMetadata, auth_base_url: str ) -> Request: - """Build registration request or skip if already registered.""" + """Build a client registration request.""" if auth_server_metadata and auth_server_metadata.registration_endpoint: registration_url = str(auth_server_metadata.registration_endpoint) @@ -261,7 +261,7 @@ def should_use_client_metadata_url( """Determine if URL-based client ID (CIMD) should be used instead of DCR. URL-based client IDs should be used when: - 1. The server advertises client_id_metadata_document_supported=true + 1. The server advertises client_id_metadata_document_supported=True 2. The client has a valid client_metadata_url configured Args: @@ -306,7 +306,7 @@ def create_client_info_from_metadata_url( async def handle_token_response_scopes( response: Response, ) -> OAuthToken: - """Parse and validate token response with optional scope validation. + """Parse and validate a token response. Parses token response JSON. Callers should check response.status_code before calling. diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index 29d4a703..7dc67c58 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -37,8 +37,8 @@ from mcp.types import ( class Client: """A high-level MCP client for connecting to MCP servers. - Currently supports in-memory transport for testing. Pass a Server or - MCPServer instance directly to the constructor. + Supports in-memory transport for testing (pass a Server or MCPServer instance), + Streamable HTTP transport (pass a URL string), or a custom Transport instance. Example: ```python @@ -205,7 +205,7 @@ class Client: Args: uri: The URI of the resource to read. - meta: Additional metadata for the request + meta: Additional metadata for the request. Returns: The resource content. @@ -239,7 +239,7 @@ class Client: meta: Additional metadata for the request Returns: - The tool result + The tool result. """ return await self.session.call_tool( name=name, diff --git a/src/mcp/client/experimental/tasks.py b/src/mcp/client/experimental/tasks.py index 8ddc4fac..2e2fdf73 100644 --- a/src/mcp/client/experimental/tasks.py +++ b/src/mcp/client/experimental/tasks.py @@ -83,7 +83,7 @@ class ExperimentalClientFeatures: status = await session.experimental.get_task(task_id) if status.status == "completed": break - await asyncio.sleep(0.5) + await anyio.sleep(0.5) # Get result final = await session.experimental.get_task_result(task_id, CallToolResult) @@ -177,7 +177,7 @@ class ExperimentalClientFeatures: """Poll a task until it reaches a terminal status. Yields GetTaskResult for each poll, allowing the caller to react to - status changes (e.g., handle input_required). Exits when task reaches + status changes (e.g., handle input_required). Exits when the task reaches a terminal status (completed, failed, cancelled). Respects the pollInterval hint from the server. diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index f4e6293b..17f41025 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -3,7 +3,7 @@ Tools, resources, and prompts are aggregated across servers. Servers may be connected to or disconnected from at any point after initialization. -This abstractions can handle naming collisions using a custom user-provided hook. +This abstraction can handle naming collisions using a custom user-provided hook. """ import contextlib @@ -30,7 +30,7 @@ from mcp.shared.session import ProgressFnT class SseServerParameters(BaseModel): - """Parameters for initializing a sse_client.""" + """Parameters for initializing an sse_client.""" # The endpoint URL. url: str @@ -67,8 +67,8 @@ class StreamableHttpParameters(BaseModel): ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters -# Use dataclass instead of pydantic BaseModel -# because pydantic BaseModel cannot handle Protocol fields. +# Use dataclass instead of Pydantic BaseModel +# because Pydantic BaseModel cannot handle Protocol fields. @dataclass class ClientSessionParameters: """Parameters for establishing a client session to an MCP server.""" @@ -119,7 +119,7 @@ class ClientSessionGroup: _session_exit_stacks: dict[mcp.ClientSession, contextlib.AsyncExitStack] # Optional fn consuming (component_name, server_info) for custom names. - # This is provide a means to mitigate naming conflicts across servers. + # This is to provide a means to mitigate naming conflicts across servers. # Example: (tool_name, server_info) => "{result.server_info.name}.{tool_name}" _ComponentNameHook: TypeAlias = Callable[[str, types.Implementation], str] _component_name_hook: _ComponentNameHook | None diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 7c309ecb..61026aa0 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -47,6 +47,7 @@ async def sse_client( headers: Optional headers to include in requests. timeout: HTTP timeout for regular operations (in seconds). sse_read_timeout: Timeout for SSE read operations (in seconds). + httpx_client_factory: Factory function for creating the HTTPX client. auth: Optional HTTPX authentication handler. on_session_created: Optional callback invoked with the session ID when received. """ diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index 5b8209ee..902dc857 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -87,9 +87,9 @@ class StdioServerParameters(BaseModel): encoding: str = "utf-8" """ - The text encoding used when sending/receiving messages to the server + The text encoding used when sending/receiving messages to the server. - defaults to utf-8 + Defaults to utf-8. """ encoding_error_handler: Literal["strict", "ignore", "replace"] = "strict" @@ -97,7 +97,7 @@ class StdioServerParameters(BaseModel): The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for - explanations of possible values + explanations of possible values. """ diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index d161e3c2..9f3dd5e0 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -358,7 +358,7 @@ class StreamableHTTPTransport: resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None), is_initialization=is_initialization, ) - # If the SSE event indicates completion, like returning respose/error + # If the SSE event indicates completion, like returning response/error # break the loop if is_complete: await response.aclose() diff --git a/src/mcp/client/websocket.py b/src/mcp/client/websocket.py index bda199f3..79e75fad 100644 --- a/src/mcp/client/websocket.py +++ b/src/mcp/client/websocket.py @@ -25,8 +25,8 @@ async def websocket_client( (read_stream, write_stream) - read_stream: As you read from this stream, you'll receive either valid - JSONRPCMessage objects or Exception objects (when validation fails). - - write_stream: Write JSONRPCMessage objects to this stream to send them + SessionMessage objects or Exception objects (when validation fails). + - write_stream: Write SessionMessage objects to this stream to send them over the WebSocket to the server. """ diff --git a/src/mcp/os/win32/utilities.py b/src/mcp/os/win32/utilities.py index fa4e4b39..0e188691 100644 --- a/src/mcp/os/win32/utilities.py +++ b/src/mcp/os/win32/utilities.py @@ -138,9 +138,9 @@ async def create_windows_process( ) -> Process | FallbackProcess: """Creates a subprocess in a Windows-compatible way with Job Object support. - Attempt to use anyio's open_process for async subprocess creation. - In some cases this will throw NotImplementedError on Windows, e.g. - when using the SelectorEventLoop which does not support async subprocesses. + Attempts to use anyio's open_process for async subprocess creation. + In some cases this will throw NotImplementedError on Windows, e.g., + when using the SelectorEventLoop, which does not support async subprocesses. In that case, we fall back to using subprocess.Popen. The process is automatically added to a Job Object to ensure all child @@ -242,8 +242,9 @@ def _create_job_object() -> int | None: def _maybe_assign_process_to_job(process: Process | FallbackProcess, job: JobHandle | None) -> None: - """Try to assign a process to a job object. If assignment fails - for any reason, the job handle is closed. + """Try to assign a process to a job object. + + If assignment fails for any reason, the job handle is closed. """ if not job: return @@ -312,8 +313,8 @@ async def terminate_windows_process(process: Process | FallbackProcess): Note: On Windows, terminating a process with process.terminate() doesn't always guarantee immediate process termination. - So we give it 2s to exit, or we call process.kill() - which sends a SIGKILL equivalent signal. + If the process does not exit within 2 seconds, process.kill() is called + to send a SIGKILL-equivalent signal. Args: process: The process to terminate diff --git a/src/mcp/server/auth/handlers/revoke.py b/src/mcp/server/auth/handlers/revoke.py index 68a3392b..4efd1540 100644 --- a/src/mcp/server/auth/handlers/revoke.py +++ b/src/mcp/server/auth/handlers/revoke.py @@ -15,7 +15,7 @@ from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvid class RevocationRequest(BaseModel): - """# See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1""" + """See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1""" token: str token_type_hint: Literal["access_token", "refresh_token"] | None = None diff --git a/src/mcp/server/auth/middleware/client_auth.py b/src/mcp/server/auth/middleware/client_auth.py index 8a6a1b51..2832f835 100644 --- a/src/mcp/server/auth/middleware/client_auth.py +++ b/src/mcp/server/auth/middleware/client_auth.py @@ -19,15 +19,17 @@ class AuthenticationError(Exception): class ClientAuthenticator: """ClientAuthenticator is a callable which validates requests from a client application, used to verify /token calls. + If, during registration, the client requested to be issued a secret, the authenticator asserts that /token calls must be authenticated with - that same token. + that same secret. + NOTE: clients can opt for no authentication during registration, in which case this logic is skipped. """ def __init__(self, provider: OAuthAuthorizationServerProvider[Any, Any, Any]): - """Initialize the dependency. + """Initialize the authenticator. Args: provider: Provider to look up client information @@ -83,7 +85,7 @@ class ClientAuthenticator: elif client.token_endpoint_auth_method == "client_secret_post": raw_form_data = form_data.get("client_secret") - # form_data.get() can return a UploadFile or None, so we need to check if it's a string + # form_data.get() can return an UploadFile or None, so we need to check if it's a string if isinstance(raw_form_data, str): request_client_secret = str(raw_form_data) diff --git a/src/mcp/server/auth/provider.py b/src/mcp/server/auth/provider.py index 5eb577fd..957082a8 100644 --- a/src/mcp/server/auth/provider.py +++ b/src/mcp/server/auth/provider.py @@ -131,8 +131,9 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref """ async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str: - """Called as part of the /authorize endpoint, and returns a URL that the client + """Handle the /authorize endpoint and return a URL that the client will be redirected to. + Many MCP implementations will redirect to a third-party provider to perform a second OAuth exchange with that provider. In this sort of setup, the client has an OAuth connection with the MCP server, and the MCP server has an OAuth @@ -151,7 +152,7 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref | | +------------+ - Implementations will need to define another handler on the MCP server return + Implementations will need to define another handler on the MCP server's return flow to perform the second redirect, and generate and store an authorization code as part of completing the OAuth authorization step. @@ -182,7 +183,7 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref authorization_code: The authorization code to get the challenge for. Returns: - The AuthorizationCode, or None if not found + The AuthorizationCode, or None if not found. """ ... @@ -199,7 +200,7 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref The OAuth token, containing access and refresh tokens. Raises: - TokenError: If the request is invalid + TokenError: If the request is invalid. """ ... @@ -234,18 +235,18 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref The OAuth token, containing access and refresh tokens. Raises: - TokenError: If the request is invalid + TokenError: If the request is invalid. """ ... async def load_access_token(self, token: str) -> AccessTokenT | None: - """Loads an access token by its token. + """Loads an access token by its token string. Args: token: The access token to verify. Returns: - The AuthInfo, or None if the token is invalid. + The access token, or None if the token is invalid. """ async def revoke_token( @@ -261,7 +262,7 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref provided. Args: - token: the token to revoke + token: The token to revoke. """ diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index 9a10ac57..a72e8194 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -25,10 +25,10 @@ def validate_issuer_url(url: AnyHttpUrl): """Validate that the issuer URL meets OAuth 2.0 requirements. Args: - url: The issuer URL to validate + url: The issuer URL to validate. Raises: - ValueError: If the issuer URL is invalid + ValueError: If the issuer URL is invalid. """ # RFC 8414 requires HTTPS, but we allow loopback/localhost HTTP for testing @@ -213,6 +213,8 @@ def create_protected_resource_routes( resource_url: The URL of this resource server authorization_servers: List of authorization servers that can issue tokens scopes_supported: Optional list of scopes supported by this resource + resource_name: Optional human-readable name for this resource + resource_documentation: Optional URL to documentation for this resource Returns: List of Starlette routes for protected resource metadata diff --git a/src/mcp/server/elicitation.py b/src/mcp/server/elicitation.py index 58e9fe44..731c914e 100644 --- a/src/mcp/server/elicitation.py +++ b/src/mcp/server/elicitation.py @@ -112,8 +112,8 @@ async def elicit_with_validation( This method can be used to interactively ask for additional information from the client within a tool's execution. The client might display the message to the - user and collect a response according to the provided schema. Or in case a - client is an agent, it might decide how to handle the elicitation -- either by asking + user and collect a response according to the provided schema. If the client + is an agent, it might decide how to handle the elicitation -- either by asking the user or automatically generating a response. For sensitive data like credentials or OAuth flows, use elicit_url() instead. diff --git a/src/mcp/server/experimental/request_context.py b/src/mcp/server/experimental/request_context.py index 91aa9a64..138c0211 100644 --- a/src/mcp/server/experimental/request_context.py +++ b/src/mcp/server/experimental/request_context.py @@ -62,8 +62,8 @@ class Experimental: """Validate that the request is compatible with the tool's task execution mode. Per MCP spec: - - "required": Clients MUST invoke as task. Server returns -32601 if not. - - "forbidden" (or None): Clients MUST NOT invoke as task. Server returns -32601 if they do. + - "required": Clients MUST invoke as a task. Server returns -32601 if not. + - "forbidden" (or None): Clients MUST NOT invoke as a task. Server returns -32601 if they do. - "optional": Either is acceptable. Args: @@ -111,7 +111,7 @@ class Experimental: """Check if this client can use a tool with the given task mode. Useful for filtering tool lists or providing warnings. - Returns False if tool requires "required" but client doesn't support tasks. + Returns False if the tool's task mode is "required" but the client doesn't support tasks. Args: tool_task_mode: The tool's execution.taskSupport value @@ -164,7 +164,7 @@ class Experimental: async def work(task: ServerTaskContext) -> CallToolResult: result = await task.elicit( message="Are you sure?", - requestedSchema={"type": "object", ...} + requested_schema={"type": "object", ...} ) confirmed = result.content.get("confirm", False) return CallToolResult(content=[TextContent(text="Done" if confirmed else "Cancelled")]) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 9ca5ac4f..aee64404 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -88,9 +88,6 @@ class NotificationOptions: async def lifespan(_: Server[LifespanResultT]) -> AsyncIterator[dict[str, Any]]: """Default lifespan context manager that does nothing. - Args: - server: The server instance this lifespan is managing - Returns: An empty context object """ diff --git a/src/mcp/server/mcpserver/resources/types.py b/src/mcp/server/mcpserver/resources/types.py index 64e63380..42aecd6e 100644 --- a/src/mcp/server/mcpserver/resources/types.py +++ b/src/mcp/server/mcpserver/resources/types.py @@ -109,7 +109,7 @@ class FunctionResource(Resource): class FileResource(Resource): """A resource that reads from a file. - Set is_binary=True to read file as binary data instead of text. + Set is_binary=True to read the file as binary data instead of text. """ path: Path = Field(description="Path to the file") diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index cd459589..21b6af7b 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -108,7 +108,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]): warn_on_duplicate_prompts: bool lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None - """A async context manager that will be called when the server is started.""" + """An async context manager that will be called when the server is started.""" auth: AuthSettings | None @@ -388,8 +388,10 @@ class MCPServer(Generic[LifespanResultT]): ] def get_context(self) -> Context[LifespanResultT, Request]: - """Returns a Context object. Note that the context will only be valid - during a request; outside a request, most methods will error. + """Return a Context object. + + Note that the context will only be valid during a request; outside a + request, most methods will error. """ try: request_context = request_ctx.get() @@ -475,6 +477,8 @@ class MCPServer(Generic[LifespanResultT]): title: Optional human-readable title for the tool description: Optional description of what the tool does annotations: Optional ToolAnnotations providing additional tool information + icons: Optional list of icons for the tool + meta: Optional metadata dictionary for the tool structured_output: Controls whether the tool's output is structured or unstructured - If None, auto-detects based on the function's return type annotation - If True, creates a structured tool (return type annotation permitting) @@ -523,6 +527,8 @@ class MCPServer(Generic[LifespanResultT]): title: Optional human-readable title for the tool description: Optional description of what the tool does annotations: Optional ToolAnnotations providing additional tool information + icons: Optional list of icons for the tool + meta: Optional metadata dictionary for the tool structured_output: Controls whether the tool's output is structured or unstructured - If None, auto-detects based on the function's return type annotation - If True, creates a structured tool (return type annotation permitting) @@ -534,8 +540,8 @@ class MCPServer(Generic[LifespanResultT]): return str(x) @server.tool() - def tool_with_context(x: int, ctx: Context) -> str: - ctx.info(f"Processing {x}") + async def tool_with_context(x: int, ctx: Context) -> str: + await ctx.info(f"Processing {x}") return str(x) @server.tool() @@ -636,6 +642,8 @@ class MCPServer(Generic[LifespanResultT]): title: Optional human-readable title for the resource description: Optional description of the resource mime_type: Optional MIME type for the resource + icons: Optional list of icons for the resource + annotations: Optional annotations for the resource meta: Optional metadata dictionary for the resource Example: @@ -644,7 +652,7 @@ class MCPServer(Generic[LifespanResultT]): return "Hello, world!" @server.resource("resource://my-resource") - async get_data() -> str: + async def get_data() -> str: data = await fetch_data() return f"Hello, world! {data}" @@ -736,6 +744,7 @@ class MCPServer(Generic[LifespanResultT]): name: Optional name for the prompt (defaults to function name) title: Optional human-readable title for the prompt description: Optional description of what the prompt does + icons: Optional list of icons for the prompt Example: @server.prompt() @@ -1092,18 +1101,18 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): ```python @server.tool() - def my_tool(x: int, ctx: Context) -> str: + async def my_tool(x: int, ctx: Context) -> str: # Log messages to the client - ctx.info(f"Processing {x}") - ctx.debug("Debug info") - ctx.warning("Warning message") - ctx.error("Error message") + await ctx.info(f"Processing {x}") + await ctx.debug("Debug info") + await ctx.warning("Warning message") + await ctx.error("Error message") # Report progress - ctx.report_progress(50, 100) + await ctx.report_progress(50, 100) # Access resources - data = ctx.read_resource("resource://data") + data = await ctx.read_resource("resource://data") # Get request info request_id = ctx.request_id @@ -1149,9 +1158,9 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): """Report progress for the current operation. Args: - progress: Current progress value e.g. 24 - total: Optional total value e.g. 100 - message: Optional message e.g. Starting render... + progress: Current progress value (e.g., 24) + total: Optional total value (e.g., 100) + message: Optional message (e.g., "Starting render...") """ progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None @@ -1187,15 +1196,14 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): This method can be used to interactively ask for additional information from the client within a tool's execution. The client might display the message to the - user and collect a response according to the provided schema. Or in case a - client is an agent, it might decide how to handle the elicitation -- either by asking + user and collect a response according to the provided schema. If the client + is an agent, it might decide how to handle the elicitation -- either by asking the user or automatically generating a response. Args: - schema: A Pydantic model class defining the expected response structure, according to the specification, - only primitive types are allowed. - message: Optional message to present to the user. If not provided, will use - a default message based on the schema + message: Message to present to the user + schema: A Pydantic model class defining the expected response structure. + According to the specification, only primitive types are allowed. Returns: An ElicitationResult containing the action taken and the data if accepted @@ -1229,7 +1237,7 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): The response indicates whether the user consented to navigate to the URL. The actual interaction happens out-of-band. When the elicitation completes, - call `self.session.send_elicit_complete(elicitation_id)` to notify the client. + call `ctx.session.send_elicit_complete(elicitation_id)` to notify the client. Args: message: Human-readable explanation of why the interaction is needed @@ -1299,7 +1307,7 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): be replayed when the client reconnects with Last-Event-ID. Use this to implement polling behavior during long-running operations - - client will reconnect after the retry interval specified in the priming event. + the client will reconnect after the retry interval specified in the priming event. Note: This is a no-op if not using StreamableHTTP transport with event_store. diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 4b539ce1..062b47d0 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -46,7 +46,7 @@ class ArgModelBase(BaseModel): def model_dump_one_level(self) -> dict[str, Any]: """Return a dict of the model's fields, one level deep. - That is, sub-models etc are not dumped - they are kept as pydantic models. + That is, sub-models etc are not dumped - they are kept as Pydantic models. """ kwargs: dict[str, Any] = {} for field_name, field_info in self.__class__.model_fields.items(): @@ -89,8 +89,7 @@ class FuncMetadata(BaseModel): return await anyio.to_thread.run_sync(functools.partial(fn, **arguments_parsed_dict)) def convert_result(self, result: Any) -> Any: - """Convert the result of a function call to the appropriate format for - the lowlevel server tool call handler: + """Convert a function call result to the format for the lowlevel tool call handler. - If output_model is None, return the unstructured content directly. - If output_model is not None, convert the result to structured output format @@ -126,11 +125,11 @@ class FuncMetadata(BaseModel): def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]: """Pre-parse data from JSON. - Return a dict with same keys as input but with values parsed from JSON + Return a dict with the same keys as input but with values parsed from JSON if appropriate. This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside - a string rather than an actual list. Claude desktop is prone to this - in fact + a string rather than an actual list. Claude Desktop is prone to this - in fact it seems incapable of NOT doing this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings, which can be pre-parsed here. """ @@ -173,8 +172,7 @@ def func_metadata( skip_names: Sequence[str] = (), structured_output: bool | None = None, ) -> FuncMetadata: - """Given a function, return metadata including a pydantic model representing its - signature. + """Given a function, return metadata including a Pydantic model representing its signature. The use case for this is ``` @@ -183,11 +181,11 @@ def func_metadata( return func(**validated_args.model_dump_one_level()) ``` - **critically** it also provides pre-parse helper to attempt to parse things from + **critically** it also provides a pre-parse helper to attempt to parse things from JSON. Args: - func: The function to convert to a pydantic model + func: The function to convert to a Pydantic model skip_names: A list of parameter names to skip. These will not be included in the model. structured_output: Controls whether the tool's output is structured or unstructured @@ -195,8 +193,8 @@ def func_metadata( - If True, creates a structured tool (return type annotation permitting) - If False, unconditionally creates an unstructured tool - If structured, creates a Pydantic model for the function's result based on its annotation. - Supports various return types: + If structured, creates a Pydantic model for the function's result based on its annotation. + Supports various return types: - BaseModel subclasses (used directly) - Primitive types (str, int, float, bool, bytes, None) - wrapped in a model with a 'result' field @@ -206,9 +204,9 @@ def func_metadata( Returns: A FuncMetadata object containing: - - arg_model: A pydantic model representing the function's arguments - - output_model: A pydantic model for the return type if output is structured - - output_conversion: Records how function output should be converted before returning. + - arg_model: A Pydantic model representing the function's arguments + - output_model: A Pydantic model for the return type if the output is structured + - wrap_output: Whether the function result needs to be wrapped in `{"result": ...}` for structured output. """ try: sig = inspect.signature(func, eval_str=True) @@ -296,7 +294,7 @@ def func_metadata( ] # pragma: no cover else: # We only had `Annotated[CallToolResult, ReturnType]`, treat the original annotation - # as beging `ReturnType`: + # as being `ReturnType`: original_annotation = return_type_expr else: return FuncMetadata(arg_model=arguments_model) @@ -355,7 +353,7 @@ def _try_create_model_and_schema( if origin is dict: args = get_args(type_expr) if len(args) == 2 and args[0] is str: - # TODO: should we use the original annotation? We are loosing any potential `Annotated` + # TODO: should we use the original annotation? We are losing any potential `Annotated` # metadata for Pydantic here: model = _create_dict_model(func_name, type_expr) else: diff --git a/src/mcp/server/mcpserver/utilities/logging.py b/src/mcp/server/mcpserver/utilities/logging.py index c394f2bf..04ca3885 100644 --- a/src/mcp/server/mcpserver/utilities/logging.py +++ b/src/mcp/server/mcpserver/utilities/logging.py @@ -8,10 +8,10 @@ def get_logger(name: str) -> logging.Logger: """Get a logger nested under MCP namespace. Args: - name: the name of the logger + name: The name of the logger. Returns: - a configured logger instance + A configured logger instance. """ return logging.getLogger(name) @@ -22,7 +22,7 @@ def configure_logging( """Configure logging for MCP. Args: - level: the log level to use + level: The log level to use. """ handlers: list[logging.Handler] = [] try: diff --git a/src/mcp/server/models.py b/src/mcp/server/models.py index 41b9224c..3861f42a 100644 --- a/src/mcp/server/models.py +++ b/src/mcp/server/models.py @@ -1,4 +1,4 @@ -"""This module provides simpler types to use with the server for managing prompts +"""This module provides simplified types to use with the server for managing prompts and tools. """ diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 6925aa55..759d2131 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -363,12 +363,12 @@ class ServerSession( """Send a form mode elicitation/create request. Args: - message: The message to present to the user - requested_schema: Schema defining the expected response structure - related_request_id: Optional ID of the request that triggered this elicitation + message: The message to present to the user. + requested_schema: Schema defining the expected response structure. + related_request_id: Optional ID of the request that triggered this elicitation. Returns: - The client's response + The client's response. Note: This method is deprecated in favor of elicit_form(). It remains for @@ -385,12 +385,12 @@ class ServerSession( """Send a form mode elicitation/create request. Args: - message: The message to present to the user - requested_schema: Schema defining the expected response structure - related_request_id: Optional ID of the request that triggered this elicitation + message: The message to present to the user. + requested_schema: Schema defining the expected response structure. + related_request_id: Optional ID of the request that triggered this elicitation. Returns: - The client's response with form data + The client's response with form data. Raises: StatelessModeNotSupported: If called in stateless HTTP mode. @@ -421,13 +421,13 @@ class ServerSession( like OAuth flows, credential collection, or payment processing. Args: - message: Human-readable explanation of why the interaction is needed - url: The URL the user should navigate to - elicitation_id: Unique identifier for tracking this elicitation - related_request_id: Optional ID of the request that triggered this elicitation + message: Human-readable explanation of why the interaction is needed. + url: The URL the user should navigate to. + elicitation_id: Unique identifier for tracking this elicitation. + related_request_id: Optional ID of the request that triggered this elicitation. Returns: - The client's response indicating acceptance, decline, or cancellation + The client's response indicating acceptance, decline, or cancellation. Raises: StatelessModeNotSupported: If called in stateless HTTP mode. @@ -499,7 +499,7 @@ class ServerSession( Args: elicitation_id: The unique identifier of the completed elicitation - related_request_id: Optional ID of the request that triggered this + related_request_id: Optional ID of the request that triggered this notification """ await self.send_notification( types.ElicitCompleteNotification( diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 674294c5..827ec359 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -29,8 +29,8 @@ Example usage: uvicorn.run(starlette_app, host="127.0.0.1", port=port) ``` -Note: The handle_sse function must return a Response to avoid a "TypeError: 'NoneType' -object is not callable" error when client disconnects. The example above returns +Note: The handle_sse function must return a Response to avoid a +"TypeError: 'NoneType' object is not callable" error when client disconnects. The example above returns an empty Response() after the SSE connection ends to fix this. See SseServerTransport class documentation for more details. @@ -61,8 +61,8 @@ logger = logging.getLogger(__name__) class SseServerTransport: - """SSE server transport for MCP. This class provides _two_ ASGI applications, - suitable to be used with a framework like Starlette and a server like Hypercorn: + """SSE server transport for MCP. This class provides two ASGI applications, + suitable for use with a framework like Starlette and a server like Hypercorn: 1. connect_sse() is an ASGI application which receives incoming GET requests, and sets up a new SSE stream to send server messages to the client. diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index bcee3a47..04aed345 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -89,7 +89,7 @@ class EventStore(ABC): message: The JSON-RPC message to store, or None for priming events Returns: - The generated event ID for the stored event + The generated event ID for the stored event. """ pass # pragma: no cover @@ -106,7 +106,7 @@ class EventStore(ABC): send_callback: A callback function to send events to the client Returns: - The stream ID of the replayed events + The stream ID of the replayed events, or None if no events were found. """ pass # pragma: no cover @@ -185,7 +185,7 @@ class StreamableHTTPServerTransport: be replayed when the client reconnects with Last-Event-ID. Use this to implement polling behavior during long-running operations - - client will reconnect after the retry interval specified in the priming event. + the client will reconnect after the retry interval specified in the priming event. Args: request_id: The request ID whose SSE stream should be closed. @@ -213,7 +213,7 @@ class StreamableHTTPServerTransport: with Last-Event-ID to resume receiving notifications. Use this to implement polling behavior for the notification stream - - client will reconnect after the retry interval specified in the priming event. + the client will reconnect after the retry interval specified in the priming event. Note: This is a no-op if there is no active standalone SSE stream. @@ -316,7 +316,7 @@ class StreamableHTTPServerTransport: status_code: HTTPStatus = HTTPStatus.OK, headers: dict[str, str] | None = None, ) -> Response: - """Create a JSON response from a JSONRPCMessage""" + """Create a JSON response from a JSONRPCMessage.""" response_headers = {"Content-Type": CONTENT_TYPE_JSON} if headers: # pragma: lax no cover response_headers.update(headers) @@ -362,7 +362,7 @@ class StreamableHTTPServerTransport: self._request_streams.pop(request_id, None) async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None: - """Application entry point that handles all HTTP requests""" + """Application entry point that handles all HTTP requests.""" request = Request(scope, receive) # Validate request headers for DNS rebinding protection @@ -536,7 +536,7 @@ class StreamableHTTPServerTransport: if isinstance(event_message.message, JSONRPCResponse | JSONRPCError): response_message = event_message.message break - # For notifications and request, keep waiting + # For notifications and requests, keep waiting else: # pragma: no cover logger.debug(f"received: {event_message.message.method}") @@ -860,6 +860,7 @@ class StreamableHTTPServerTransport: async def _replay_events(self, last_event_id: str, request: Request, send: Send) -> None: # pragma: no cover """Replays events that would have been sent after the specified event ID. + Only used when resumability is enabled. """ event_store = self._event_store diff --git a/src/mcp/server/websocket.py b/src/mcp/server/websocket.py index 7b00f790..3e675da5 100644 --- a/src/mcp/server/websocket.py +++ b/src/mcp/server/websocket.py @@ -12,8 +12,8 @@ from mcp.shared.message import SessionMessage @asynccontextmanager # pragma: no cover async def websocket_server(scope: Scope, receive: Receive, send: Send): - """WebSocket server transport for MCP. This is an ASGI application, suitable to be - used with a framework like Starlette and a server like Hypercorn. + """WebSocket server transport for MCP. This is an ASGI application, suitable for use + with a framework like Starlette and a server like Hypercorn. """ websocket = WebSocket(scope, receive, send) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index bf03a8b8..ca5b7b45 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -33,9 +33,8 @@ class InvalidRedirectUriError(Exception): class OAuthClientMetadata(BaseModel): - """RFC 7591 OAuth 2.0 Dynamic Client Registration metadata. + """RFC 7591 OAuth 2.0 Dynamic Client Registration Metadata. See https://datatracker.ietf.org/doc/html/rfc7591#section-2 - for the full specification. """ redirect_uris: list[AnyUrl] | None = Field(..., min_length=1) @@ -145,9 +144,9 @@ class ProtectedResourceMetadata(BaseModel): 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 default is False, but omitted 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 default is False, but omitted here for clarity dpop_bound_access_tokens_required: bool | None = None diff --git a/src/mcp/shared/memory.py b/src/mcp/shared/memory.py index d01d28b8..f2d5e2b9 100644 --- a/src/mcp/shared/memory.py +++ b/src/mcp/shared/memory.py @@ -17,7 +17,7 @@ MessageStream = tuple[MemoryObjectReceiveStream[SessionMessage | Exception], Mem async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageStream, MessageStream], None]: """Creates a pair of bidirectional memory streams for client-server communication. - Returns: + Yields: A tuple of (client_streams, server_streams) where each is a tuple of (read_stream, write_stream) """ diff --git a/src/mcp/shared/metadata_utils.py b/src/mcp/shared/metadata_utils.py index 2b66996b..3b6b27df 100644 --- a/src/mcp/shared/metadata_utils.py +++ b/src/mcp/shared/metadata_utils.py @@ -1,7 +1,7 @@ """Utility functions for working with metadata in MCP types. These utilities are primarily intended for client-side usage to properly display -human-readable names in user interfaces in a spec compliant way. +human-readable names in user interfaces in a spec-compliant way. """ from mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index 5ee8f3ba..d3f36ded 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -115,6 +115,7 @@ class RequestResponder(Generic[ReceiveRequestT, SendResultT]): """Send a response for this request. Must be called within a context manager block. + Raises: RuntimeError: If not used within a context manager AssertionError: If request was already responded to @@ -235,7 +236,7 @@ class BaseSession( metadata: MessageMetadata = None, progress_callback: ProgressFnT | None = None, ) -> ReceiveResultT: - """Sends a request and wait for a response. + """Sends a request and waits for a response. Raises an MCPError if the response contains an error. If a request read timeout is provided, it will take precedence over the session read timeout. @@ -512,4 +513,4 @@ class BaseSession( async def _handle_incoming( self, req: RequestResponder[ReceiveRequestT, SendResultT] | ReceiveNotificationT | Exception ) -> None: - """A generic handler for incoming messages. Overwritten by subclasses.""" + """A generic handler for incoming messages. Overridden by subclasses.""" diff --git a/src/mcp/types/_types.py b/src/mcp/types/_types.py index 32042263..9005d253 100644 --- a/src/mcp/types/_types.py +++ b/src/mcp/types/_types.py @@ -22,7 +22,7 @@ We need this to satisfy the MCP specification, which requires the server to assu provided by the client. See the "Protocol Version Header" at -https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header). +https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header. """ ProgressToken = str | int @@ -108,8 +108,7 @@ class Request(MCPModel, Generic[RequestParamsT, MethodT]): class PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[MethodT]): - """Base class for paginated requests, - matching the schema's PaginatedRequest interface.""" + """Base class for paginated requests, matching the schema's PaginatedRequest interface.""" params: PaginatedRequestParams | None = None @@ -174,10 +173,10 @@ class Icon(MCPModel): theme: IconTheme | None = None """Optional theme specifier. - - `"light"` indicates the icon is designed for a light background, `"dark"` indicates the icon + + `"light"` indicates the icon is designed for a light background, `"dark"` indicates the icon is designed for a dark background. - + See https://modelcontextprotocol.io/specification/2025-11-25/schema#icon for more details. """ @@ -536,7 +535,7 @@ class TaskStatusNotificationParams(NotificationParams, Task): class TaskStatusNotification(Notification[TaskStatusNotificationParams, Literal["notifications/tasks/status"]]): """An optional notification from the receiver to the requestor, informing them that a task's status has changed. - Receivers are not required to send these notifications + Receivers are not required to send these notifications. """ method: Literal["notifications/tasks/status"] = "notifications/tasks/status" @@ -608,7 +607,7 @@ class ProgressNotificationParams(NotificationParams): message: str | None = None """Message related to progress. - This should provide relevant human readable progress information. + This should provide relevant human-readable progress information. """ @@ -999,7 +998,9 @@ SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioConte SamplingContent: TypeAlias = TextContent | ImageContent | AudioContent """Basic content types for sampling responses (without tool use). -Used for backwards-compatible CreateMessageResult when tools are not used.""" + +Used for backwards-compatible CreateMessageResult when tools are not used. +""" class SamplingMessage(MCPModel): @@ -1117,7 +1118,7 @@ class ToolAnnotations(MCPModel): idempotent_hint: bool | None = None """ If true, calling the tool repeatedly with the same arguments - will have no additional effect on the its environment. + will have no additional effect on its environment. (This property is meaningful only when `read_only_hint == false`) Default: false """ @@ -1265,7 +1266,7 @@ class ModelPreferences(MCPModel): sampling. Because LLMs can vary along multiple dimensions, choosing the "best" model is - rarely straightforward. Different models excel in different areas—some are + rarely straightforward. Different models excel in different areas—some are faster but less capable, others are more capable but more expensive, and so on. This interface allows servers to express their priorities across multiple dimensions to help clients make an appropriate selection for their use case. @@ -1369,7 +1370,7 @@ StopReason = Literal["endTurn", "stopSequence", "maxTokens", "toolUse"] | str class CreateMessageResult(Result): - """The client's response to a sampling/create_message request from the server. + """The client's response to a sampling/createMessage request from the server. This is the backwards-compatible version that returns single content (no arrays). Used when the request does not include tools. @@ -1386,7 +1387,7 @@ class CreateMessageResult(Result): class CreateMessageResultWithTools(Result): - """The client's response to a sampling/create_message request when tools were provided. + """The client's response to a sampling/createMessage request when tools were provided. This version supports array content for tool use flows. """ @@ -1426,14 +1427,14 @@ class PromptReference(MCPModel): type: Literal["ref/prompt"] = "ref/prompt" name: str - """The name of the prompt or prompt template""" + """The name of the prompt or prompt template.""" class CompletionArgument(MCPModel): """The argument's information for completion requests.""" name: str - """The name of the argument""" + """The name of the argument.""" value: str """The value of the argument to use for completion matching.""" @@ -1451,7 +1452,7 @@ class CompleteRequestParams(RequestParams): ref: ResourceTemplateReference | PromptReference argument: CompletionArgument context: CompletionContext | None = None - """Additional, optional context for completions""" + """Additional, optional context for completions.""" class CompleteRequest(Request[CompleteRequestParams, Literal["completion/complete"]]): @@ -1479,7 +1480,7 @@ class Completion(MCPModel): class CompleteResult(Result): - """The server's response to a completion/complete request""" + """The server's response to a completion/complete request.""" completion: Completion @@ -1522,6 +1523,7 @@ class Root(MCPModel): class ListRootsResult(Result): """The client's response to a roots/list request from the server. + This result contains an array of Root objects, each representing a root directory or file that the server can operate on. """ @@ -1643,7 +1645,7 @@ class ElicitRequestFormParams(RequestParams): requested_schema: ElicitRequestedSchema """ - A restricted subset of JSON Schema defining the structure of expected response. + A restricted subset of JSON Schema defining the structure of the expected response. Only top-level properties are allowed, without nesting. """ @@ -1697,8 +1699,8 @@ class ElicitResult(Result): content: dict[str, str | int | float | bool | list[str] | None] | None = None """ The submitted form data, only present when action is "accept" in form mode. - Contains values matching the requested schema. Values can be strings, integers, - booleans, or arrays of strings. + Contains values matching the requested schema. Values can be strings, integers, floats, + booleans, arrays of strings, or null. For URL mode, this field is omitted. """