diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 72e328b54..26982c586 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -91,5 +91,5 @@ jobs: - name: Install dependencies run: uv sync --frozen --all-extras --python 3.10 - - name: Check README snippets are up to date - run: uv run --frozen scripts/update_readme_snippets.py --check --readme README.v2.md + - name: Check snippets are up to date + run: uv run --frozen python scripts/sync_snippets.py --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42c12fded..40aa36756 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,8 +62,8 @@ repos: language: fail files: ^README\.md$ - id: readme-snippets - name: Check README snippets are up to date - entry: uv run --frozen python scripts/update_readme_snippets.py --check + name: Check snippets are up to date + entry: uv run --frozen python scripts/sync_snippets.py --check language: system - files: ^(README\.v2\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$ + files: ^(README\.v2\.md|examples/.*\.py|src/mcp/.*\.py|docs/.*\.md|scripts/sync_snippets\.py)$ pass_filenames: false diff --git a/CLAUDE.md b/CLAUDE.md index e48ce6e70..7dc1d712c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,72 @@ rather than adding new standalone sections. - Update config rev - Commit config first +## Code Snippet System + +`scripts/sync_snippets.py` replaces the content between +`` / `` markers with code +from the referenced source file. The source file is the source of truth — +never edit synced content directly in the target. + +To sync only part of a file, append `#RegionName` to the path. Regions are +delimited in source files by `# region Name` / `# endregion Name` markers. +Each example lives in a named function (returning `-> None`) wrapping a region. +Names follow `ClassName_methodName_variant` for methods, `functionName_variant` +for standalone functions, or `module_overview` for module docstrings. Pick a +descriptive variant suffix (`_basic`, `_sync`/`_async`, `_with_context`, etc.): + +````python +def MyClass_do_thing_basic(obj: MyClass) -> None: + # region MyClass_do_thing_basic + result = obj.do_thing("arg") + print(result) + # endregion MyClass_do_thing_basic +```` + +Function parameters supply typed dependencies the example needs but does not create +(e.g., `server: MCPServer`); module-level stubs are only for truly undefined references +(e.g., `async def fetch_data() -> str: ...`). + +NEVER put `# type: ignore`, `# pyright: ignore`, or `# noqa` inside a region — these +sync verbatim into the target. Restructure the code to address the errors instead. + +After editing an example file, run `uv run --frozen pyright` to verify types, then +`uv run python scripts/sync_snippets.py` to sync. Use `--check` to verify without +modifying files. + +### Markdown Code Examples + +Code examples in `README.v2.md` and `docs/**/*.md` use explicit paths relative +to the repo root: + +````markdown + +```python +# replaced by sync script +``` + +```` + +### Docstring Code Examples + +Code examples in `src/` docstrings use companion files in +`examples/snippets/docstrings/`, mirroring the source tree +(`src/mcp/foo/bar.py` → `examples/snippets/docstrings/mcp/foo/bar.py`). +Companion files are standalone scripts (not packages) starting with +`from __future__ import annotations`. + +Docstrings use path-less `#Region` markers (only supported in `src/` files): + +````text + Example: + + ```python + result = obj.do_thing("arg") + print(result) + ``` + +```` + ## Error Resolution 1. CI Failures diff --git a/README.v2.md b/README.v2.md index bd6927bf9..ffbb54170 100644 --- a/README.v2.md +++ b/README.v2.md @@ -181,9 +181,9 @@ def greet_user(name: str, style: str = "friendly") -> str: if __name__ == "__main__": mcp.run(transport="streamable-http", json_response=True) ``` + _Full example: [examples/snippets/servers/mcpserver_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/mcpserver_quickstart.py)_ - You can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server: @@ -279,9 +279,9 @@ def query_db(ctx: Context[AppContext]) -> str: db = ctx.request_context.lifespan_context.db return db.query() ``` + _Full example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_ - ### Resources @@ -310,9 +310,9 @@ def get_settings() -> str: "debug": false }""" ``` + _Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_ - ### Tools @@ -337,9 +337,9 @@ def get_weather(city: str, unit: str = "celsius") -> str: # This would normally call a weather API return f"Weather in {city}: 22degrees{unit[0].upper()}" ``` + _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 MCPServer framework and provides access to MCP capabilities: @@ -367,9 +367,9 @@ async def long_running_task(task_name: str, ctx: Context[ServerSession, None], s 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 @@ -452,9 +452,9 @@ def empty_result_tool() -> CallToolResult: """For empty results, return CallToolResult with empty content.""" return CallToolResult(content=[]) ``` + _Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_call_tool_result.py)_ - **Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`. @@ -558,9 +558,9 @@ def get_temperature(city: str) -> float: return 22.5 # Returns: {"result": 22.5} ``` + _Full example: [examples/snippets/servers/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/structured_output.py)_ - ### Prompts @@ -587,9 +587,9 @@ def debug_error(error: str) -> list[base.Message]: base.AssistantMessage("I'll help debug that. What have you tried so far?"), ] ``` + _Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_ - ### Icons @@ -648,9 +648,9 @@ def create_thumbnail(image_path: str) -> Image: img.thumbnail((100, 100)) return Image(data=img.tobytes(), format="png") ``` + _Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/images.py)_ - ### Context @@ -715,9 +715,9 @@ async def long_running_task(task_name: str, ctx: Context[ServerSession, None], s 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)_ - ### Completions @@ -805,9 +805,10 @@ def main(): if __name__ == "__main__": main() ``` + _Full example: [examples/snippets/clients/completion_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/completion_client.py)_ - + ### Elicitation Request additional information from users. This example shows an Elicitation during a Tool Call: @@ -914,9 +915,9 @@ async def connect_service(service_name: str, ctx: Context[ServerSession, None]) ] ) ``` + _Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_ - Elicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms. @@ -959,9 +960,9 @@ async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str: return result.content.text return str(result.content) ``` + _Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_ - ### Logging and Notifications @@ -989,9 +990,9 @@ async def process_data(data: str, ctx: Context[ServerSession, None]) -> str: return f"Processed: {data}" ``` + _Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_ - ### Authentication @@ -1049,9 +1050,9 @@ async def get_weather(city: str = "London") -> dict[str, str]: if __name__ == "__main__": mcp.run(transport="streamable-http", json_response=True) ``` + _Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_ - For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/). @@ -1223,9 +1224,9 @@ def main(): if __name__ == "__main__": main() ``` + _Full example: [examples/snippets/servers/direct_execution.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_execution.py)_ - Run it with: @@ -1272,9 +1273,9 @@ if __name__ == "__main__": # Stateful server with session persistence # mcp.run(transport="streamable-http") ``` + _Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_config.py)_ - You can mount multiple MCPServer servers in a Starlette application: @@ -1334,9 +1335,9 @@ app = Starlette( # echo_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True) # math_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True) ``` + _Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_ - For low level server with Streamable HTTP implementations, see: @@ -1429,9 +1430,9 @@ app = Starlette( lifespan=lifespan, ) ``` + _Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_basic_mounting.py)_ - ##### Host-based routing @@ -1476,9 +1477,9 @@ app = Starlette( lifespan=lifespan, ) ``` + _Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_host_mounting.py)_ - ##### Multiple servers with path configuration @@ -1533,9 +1534,9 @@ app = Starlette( lifespan=lifespan, ) ``` + _Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_multiple_servers.py)_ - ##### Path configuration at initialization @@ -1573,9 +1574,9 @@ app = Starlette( ] ) ``` + _Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_path_config.py)_ - #### SSE servers @@ -1738,9 +1739,9 @@ if __name__ == "__main__": asyncio.run(run()) ``` + _Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_ - The lifespan API provides: @@ -1814,9 +1815,9 @@ async def run(): if __name__ == "__main__": asyncio.run(run()) ``` + _Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_ - Caution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server. @@ -1907,9 +1908,9 @@ async def run(): if __name__ == "__main__": asyncio.run(run()) ``` + _Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_ - With the low-level server, handlers always return `CallToolResult` directly. You construct both the human-readable `content` and the machine-readable `structured_content` yourself, giving you full control over the response. @@ -1982,9 +1983,9 @@ async def run(): if __name__ == "__main__": asyncio.run(run()) ``` + _Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_ - ### Pagination (Advanced) @@ -2030,9 +2031,9 @@ async def handle_list_resources( server = Server("paginated-server", on_list_resources=handle_list_resources) ``` + _Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_ - #### Client-side Consumption @@ -2078,9 +2079,9 @@ async def list_all_resources() -> None: if __name__ == "__main__": asyncio.run(list_all_resources()) ``` + _Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/pagination_client.py)_ - #### Key Points @@ -2178,9 +2179,9 @@ def main(): if __name__ == "__main__": main() ``` + _Full example: [examples/snippets/clients/stdio_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/stdio_client.py)_ - Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http): @@ -2211,9 +2212,9 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) ``` + _Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/streamable_basic.py)_ - ### Client Display Utilities @@ -2288,9 +2289,9 @@ def main(): if __name__ == "__main__": main() ``` + _Full example: [examples/snippets/clients/display_utilities.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/display_utilities.py)_ - The `get_display_name()` function implements the proper precedence rules for displaying names: @@ -2394,9 +2395,9 @@ def run(): if __name__ == "__main__": run() ``` + _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_ - For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). diff --git a/examples/snippets/docstrings/mcp/client/auth/extensions/client_credentials.py b/examples/snippets/docstrings/mcp/client/auth/extensions/client_credentials.py new file mode 100644 index 000000000..b8b145fde --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/auth/extensions/client_credentials.py @@ -0,0 +1,95 @@ +"""Companion examples for src/mcp/client/auth/extensions/client_credentials.py docstrings.""" + +from __future__ import annotations + +from mcp.client.auth import TokenStorage +from mcp.client.auth.extensions.client_credentials import ( + ClientCredentialsOAuthProvider, + PrivateKeyJWTOAuthProvider, + SignedJWTParameters, + static_assertion_provider, +) + + +async def fetch_token_from_identity_provider(*, audience: str) -> str: ... + + +def ClientCredentialsOAuthProvider_init(my_token_storage: TokenStorage) -> None: + # region ClientCredentialsOAuthProvider_init + provider = ClientCredentialsOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + client_secret="my-client-secret", + ) + # endregion ClientCredentialsOAuthProvider_init + + +def static_assertion_provider_usage(my_token_storage: TokenStorage, my_prebuilt_jwt: str) -> None: + # region static_assertion_provider_usage + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=static_assertion_provider(my_prebuilt_jwt), + ) + # endregion static_assertion_provider_usage + + +def SignedJWTParameters_usage(my_token_storage: TokenStorage, private_key_pem: str) -> None: + # region SignedJWTParameters_usage + jwt_params = SignedJWTParameters( + issuer="my-client-id", + subject="my-client-id", + signing_key=private_key_pem, + ) + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=jwt_params.create_assertion_provider(), + ) + # endregion SignedJWTParameters_usage + + +def PrivateKeyJWTOAuthProvider_workloadIdentity(my_token_storage: TokenStorage) -> None: + # region PrivateKeyJWTOAuthProvider_workloadIdentity + async def get_workload_identity_token(audience: str) -> str: + # Fetch JWT from your identity provider + # The JWT's audience must match the provided audience parameter + return await fetch_token_from_identity_provider(audience=audience) + + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=get_workload_identity_token, + ) + # endregion PrivateKeyJWTOAuthProvider_workloadIdentity + + +def PrivateKeyJWTOAuthProvider_staticJWT(my_token_storage: TokenStorage, my_prebuilt_jwt: str) -> None: + # region PrivateKeyJWTOAuthProvider_staticJWT + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=static_assertion_provider(my_prebuilt_jwt), + ) + # endregion PrivateKeyJWTOAuthProvider_staticJWT + + +def PrivateKeyJWTOAuthProvider_sdkSigned(my_token_storage: TokenStorage, private_key_pem: str) -> None: + # region PrivateKeyJWTOAuthProvider_sdkSigned + jwt_params = SignedJWTParameters( + issuer="my-client-id", + subject="my-client-id", + signing_key=private_key_pem, + ) + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=jwt_params.create_assertion_provider(), + ) + # endregion PrivateKeyJWTOAuthProvider_sdkSigned diff --git a/examples/snippets/docstrings/mcp/client/client.py b/examples/snippets/docstrings/mcp/client/client.py new file mode 100644 index 000000000..2552e67ac --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/client.py @@ -0,0 +1,24 @@ +"""Companion examples for src/mcp/client/client.py docstrings.""" + +from __future__ import annotations + +import asyncio + + +def Client_usage() -> None: + # region Client_usage + from mcp.client import Client + from mcp.server.mcpserver import MCPServer + + server = MCPServer("test") + + @server.tool() + def add(a: int, b: int) -> int: + return a + b + + async def main(): + async with Client(server) as client: + result = await client.call_tool("add", {"a": 1, "b": 2}) + + asyncio.run(main()) + # endregion Client_usage diff --git a/examples/snippets/docstrings/mcp/client/experimental/task_handlers.py b/examples/snippets/docstrings/mcp/client/experimental/task_handlers.py new file mode 100644 index 000000000..9abd0bc89 --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/experimental/task_handlers.py @@ -0,0 +1,36 @@ +"""Companion examples for src/mcp/client/experimental/task_handlers.py docstrings.""" + +from __future__ import annotations + +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +from mcp import types +from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers +from mcp.client.session import ClientSession +from mcp.shared._context import RequestContext +from mcp.shared.session import SessionMessage + + +async def my_get_task_handler( + context: RequestContext[ClientSession], + params: types.GetTaskRequestParams, +) -> types.GetTaskResult | types.ErrorData: ... + + +async def my_list_tasks_handler( + context: RequestContext[ClientSession], + params: types.PaginatedRequestParams | None, +) -> types.ListTasksResult | types.ErrorData: ... + + +def ExperimentalTaskHandlers_usage( + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], +) -> None: + # region ExperimentalTaskHandlers_usage + handlers = ExperimentalTaskHandlers( + get_task=my_get_task_handler, + list_tasks=my_list_tasks_handler, + ) + session = ClientSession(read_stream, write_stream, experimental_task_handlers=handlers) + # endregion ExperimentalTaskHandlers_usage diff --git a/examples/snippets/docstrings/mcp/client/experimental/tasks.py b/examples/snippets/docstrings/mcp/client/experimental/tasks.py new file mode 100644 index 000000000..423b4754f --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/experimental/tasks.py @@ -0,0 +1,60 @@ +"""Companion examples for src/mcp/client/experimental/tasks.py docstrings.""" + +from __future__ import annotations + +import anyio + +from mcp.client.session import ClientSession +from mcp.types import CallToolResult + + +async def module_overview(session: ClientSession) -> None: + # region module_overview + # Call a tool as a task + result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) + task_id = result.task.task_id + + # Get task status + status = await session.experimental.get_task(task_id) + + # Get task result when complete + if status.status == "completed": + result = await session.experimental.get_task_result(task_id, CallToolResult) + + # List all tasks + tasks = await session.experimental.list_tasks() + + # Cancel a task + await session.experimental.cancel_task(task_id) + # endregion module_overview + + +async def ExperimentalClientFeatures_call_tool_as_task_usage(session: ClientSession) -> None: + # region ExperimentalClientFeatures_call_tool_as_task_usage + # Create task + result = await session.experimental.call_tool_as_task("long_running_tool", {"input": "data"}) + task_id = result.task.task_id + + # Poll for completion + while True: + status = await session.experimental.get_task(task_id) + if status.status == "completed": + break + await anyio.sleep(0.5) + + # Get result + final = await session.experimental.get_task_result(task_id, CallToolResult) + # endregion ExperimentalClientFeatures_call_tool_as_task_usage + + +async def ExperimentalClientFeatures_poll_task_usage(session: ClientSession, task_id: str) -> None: + # region ExperimentalClientFeatures_poll_task_usage + async for status in session.experimental.poll_task(task_id): + print(f"Status: {status.status}") + if status.status == "input_required": + # Handle elicitation request via tasks/result + pass + + # Task is now terminal, get the result + result = await session.experimental.get_task_result(task_id, CallToolResult) + # endregion ExperimentalClientFeatures_poll_task_usage diff --git a/examples/snippets/docstrings/mcp/client/session.py b/examples/snippets/docstrings/mcp/client/session.py new file mode 100644 index 000000000..dee88ee6c --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/session.py @@ -0,0 +1,13 @@ +"""Companion examples for src/mcp/client/session.py docstrings.""" + +from __future__ import annotations + +from mcp.client.session import ClientSession +from mcp.types import CallToolResult + + +async def ClientSession_experimental_usage(session: ClientSession, task_id: str) -> None: + # region ClientSession_experimental_usage + status = await session.experimental.get_task(task_id) + result = await session.experimental.get_task_result(task_id, CallToolResult) + # endregion ClientSession_experimental_usage diff --git a/examples/snippets/docstrings/mcp/client/session_group.py b/examples/snippets/docstrings/mcp/client/session_group.py new file mode 100644 index 000000000..de9ef9636 --- /dev/null +++ b/examples/snippets/docstrings/mcp/client/session_group.py @@ -0,0 +1,19 @@ +"""Companion examples for src/mcp/client/session_group.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.client.session_group import ClientSessionGroup + + +async def ClientSessionGroup_usage(server_params: list[Any]) -> None: + # region ClientSessionGroup_usage + def name_fn(name: str, server_info: Any) -> str: + return f"{server_info.name}_{name}" + + async with ClientSessionGroup(component_name_hook=name_fn) as group: + for server_param in server_params: + await group.connect_to_server(server_param) + ... + # endregion ClientSessionGroup_usage diff --git a/examples/snippets/docstrings/mcp/server/experimental/request_context.py b/examples/snippets/docstrings/mcp/server/experimental/request_context.py new file mode 100644 index 000000000..ae9571f74 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/experimental/request_context.py @@ -0,0 +1,31 @@ +"""Companion examples for src/mcp/server/experimental/request_context.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.server.context import ServerRequestContext +from mcp.server.experimental.task_context import ServerTaskContext +from mcp.types import CallToolRequestParams, CallToolResult, CreateTaskResult, TextContent + + +def Experimental_run_task_usage() -> None: + # region Experimental_run_task_usage + async def handle_tool( + ctx: ServerRequestContext[Any, Any], + params: CallToolRequestParams, + ) -> CreateTaskResult: + async def work(task: ServerTaskContext) -> CallToolResult: + result = await task.elicit( + message="Are you sure?", + requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}}, + ) + if result.action == "accept" and result.content: + confirmed = result.content.get("confirm", False) + else: + confirmed = False + return CallToolResult(content=[TextContent(text="Done" if confirmed else "Cancelled")]) + + return await ctx.experimental.run_task(work) + + # endregion Experimental_run_task_usage diff --git a/examples/snippets/docstrings/mcp/server/experimental/task_context.py b/examples/snippets/docstrings/mcp/server/experimental/task_context.py new file mode 100644 index 000000000..cd50c8c96 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/experimental/task_context.py @@ -0,0 +1,24 @@ +"""Companion examples for src/mcp/server/experimental/task_context.py docstrings.""" + +from __future__ import annotations + +from mcp.server.experimental.task_context import ServerTaskContext +from mcp.types import CallToolResult, TextContent + + +async def ServerTaskContext_usage(task: ServerTaskContext) -> None: + # region ServerTaskContext_usage + async def my_task_work(task: ServerTaskContext) -> CallToolResult: + await task.update_status("Starting...") + + result = await task.elicit( + message="Continue?", + requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}}, + ) + + if result.action == "accept" and result.content and result.content.get("ok"): + return CallToolResult(content=[TextContent(text="Done!")]) + else: + return CallToolResult(content=[TextContent(text="Cancelled")]) + + # endregion ServerTaskContext_usage diff --git a/examples/snippets/docstrings/mcp/server/experimental/task_support.py b/examples/snippets/docstrings/mcp/server/experimental/task_support.py new file mode 100644 index 000000000..057d44069 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/experimental/task_support.py @@ -0,0 +1,33 @@ +"""Companion examples for src/mcp/server/experimental/task_support.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.server.lowlevel.server import Server +from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue +from mcp.shared.experimental.tasks.store import TaskStore + + +# Stubs for undefined references in examples +class RedisTaskStore(TaskStore): # type: ignore[abstract] + def __init__(self, redis_url: str) -> None: ... + + +class RedisTaskMessageQueue(TaskMessageQueue): # type: ignore[abstract] + def __init__(self, redis_url: str) -> None: ... + + +def TaskSupport_simple(server: Server[Any]) -> None: + # region TaskSupport_simple + server.experimental.enable_tasks() + # endregion TaskSupport_simple + + +def TaskSupport_custom(server: Server[Any], redis_url: str) -> None: + # region TaskSupport_custom + server.experimental.enable_tasks( + store=RedisTaskStore(redis_url), + queue=RedisTaskMessageQueue(redis_url), + ) + # endregion TaskSupport_custom diff --git a/examples/snippets/docstrings/mcp/server/lowlevel/experimental.py b/examples/snippets/docstrings/mcp/server/lowlevel/experimental.py new file mode 100644 index 000000000..82dfcae0d --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/lowlevel/experimental.py @@ -0,0 +1,32 @@ +"""Companion examples for src/mcp/server/lowlevel/experimental.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.server.lowlevel.server import Server +from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue +from mcp.shared.experimental.tasks.store import TaskStore + + +class RedisTaskStore(TaskStore): # type: ignore[abstract] + def __init__(self, redis_url: str) -> None: ... + + +class RedisTaskMessageQueue(TaskMessageQueue): # type: ignore[abstract] + def __init__(self, redis_url: str) -> None: ... + + +def ExperimentalHandlers_enable_tasks_simple(server: Server[Any]) -> None: + # region ExperimentalHandlers_enable_tasks_simple + server.experimental.enable_tasks() + # endregion ExperimentalHandlers_enable_tasks_simple + + +def ExperimentalHandlers_enable_tasks_custom(server: Server[Any], redis_url: str) -> None: + # region ExperimentalHandlers_enable_tasks_custom + server.experimental.enable_tasks( + store=RedisTaskStore(redis_url), + queue=RedisTaskMessageQueue(redis_url), + ) + # endregion ExperimentalHandlers_enable_tasks_custom diff --git a/examples/snippets/docstrings/mcp/server/mcpserver/server.py b/examples/snippets/docstrings/mcp/server/mcpserver/server.py new file mode 100644 index 000000000..4b2e596e1 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/mcpserver/server.py @@ -0,0 +1,178 @@ +"""Companion examples for src/mcp/server/mcpserver/server.py docstrings.""" + +from __future__ import annotations + +from typing import Any, TypeAlias + +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import ( + Completion, + CompletionArgument, + CompletionContext, + PromptReference, + ResourceTemplateReference, +) + +Message: TypeAlias = dict[str, Any] + + +async def fetch_data() -> str: ... +async def fetch_weather(city: str) -> str: ... +def read_table_schema(table_name: str) -> str: ... +async def read_file(path: str) -> str: ... + + +def MCPServer_tool_basic(server: MCPServer) -> None: + # region MCPServer_tool_basic + @server.tool() + def my_tool(x: int) -> str: + return str(x) + + # endregion MCPServer_tool_basic + + +def MCPServer_tool_with_context(server: MCPServer) -> None: + # region MCPServer_tool_with_context + @server.tool() + async def tool_with_context(x: int, ctx: Context) -> str: + await ctx.info(f"Processing {x}") + return str(x) + + # endregion MCPServer_tool_with_context + + +def MCPServer_tool_async(server: MCPServer) -> None: + # region MCPServer_tool_async + @server.tool() + async def async_tool(x: int, context: Context) -> str: + await context.report_progress(50, 100) + return str(x) + + # endregion MCPServer_tool_async + + +def MCPServer_completion(server: MCPServer) -> None: + # region MCPServer_completion + @server.completion() + async def handle_completion( + ref: PromptReference | ResourceTemplateReference, + argument: CompletionArgument, + context: CompletionContext | None, + ) -> Completion | None: + if isinstance(ref, ResourceTemplateReference): + # Return completions based on ref, argument, and context + return Completion(values=["option1", "option2"]) + return None + + # endregion MCPServer_completion + + +def MCPServer_resource_sync_static(server: MCPServer) -> None: + # region MCPServer_resource_sync_static + @server.resource("resource://my-resource") + def get_data() -> str: + return "Hello, world!" + + # endregion MCPServer_resource_sync_static + + +def MCPServer_resource_async_static(server: MCPServer) -> None: + # region MCPServer_resource_async_static + @server.resource("resource://my-resource") + async def get_data() -> str: + data = await fetch_data() + return f"Hello, world! {data}" + + # endregion MCPServer_resource_async_static + + +def MCPServer_resource_sync_template(server: MCPServer) -> None: + # region MCPServer_resource_sync_template + @server.resource("resource://{city}/weather") + def get_weather(city: str) -> str: + return f"Weather for {city}" + + # endregion MCPServer_resource_sync_template + + +def MCPServer_resource_async_template(server: MCPServer) -> None: + # region MCPServer_resource_async_template + @server.resource("resource://{city}/weather") + async def get_weather(city: str) -> str: + data = await fetch_weather(city) + return f"Weather for {city}: {data}" + + # endregion MCPServer_resource_async_template + + +def MCPServer_prompt_sync(server: MCPServer) -> None: + # region MCPServer_prompt_sync + @server.prompt() + def analyze_table(table_name: str) -> list[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema:\n{schema}", + } + ] + + # endregion MCPServer_prompt_sync + + +def MCPServer_prompt_async(server: MCPServer) -> None: + # region MCPServer_prompt_async + @server.prompt() + async def analyze_file(path: str) -> list[Message]: + content = await read_file(path) + return [ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": f"file://{path}", + "text": content, + }, + }, + } + ] + + # endregion MCPServer_prompt_async + + +def MCPServer_custom_route(server: MCPServer) -> None: + # region MCPServer_custom_route + @server.custom_route("/health", methods=["GET"]) + async def health_check(request: Request) -> Response: + return JSONResponse({"status": "ok"}) + + # endregion MCPServer_custom_route + + +def Context_usage(server: MCPServer) -> None: + # region Context_usage + @server.tool() + async def my_tool(x: int, ctx: Context) -> str: + # Log messages to the client + await ctx.info(f"Processing {x}") + await ctx.debug("Debug info") + await ctx.warning("Warning message") + await ctx.error("Error message") + + # Report progress + await ctx.report_progress(50, 100) + + # Access resources + data = await ctx.read_resource("resource://data") + + # Get request info + request_id = ctx.request_id + client_id = ctx.client_id + + return str(x) + + # endregion Context_usage diff --git a/examples/snippets/docstrings/mcp/server/sse.py b/examples/snippets/docstrings/mcp/server/sse.py new file mode 100644 index 000000000..1f5036f45 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/sse.py @@ -0,0 +1,42 @@ +"""Companion examples for src/mcp/server/sse.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +import uvicorn +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Mount, Route + +from mcp.server.lowlevel.server import Server +from mcp.server.sse import SseServerTransport + + +def module_overview(app: Server[Any], port: int) -> None: + # region module_overview + # Create an SSE transport at an endpoint + sse = SseServerTransport("/messages/") + + # Define handler functions + async def handle_sse(request: Request) -> Response: + async with sse.connect_sse( + request.scope, + request.receive, + request._send, + ) as streams: + await app.run(streams[0], streams[1], app.create_initialization_options()) + # Return empty response to avoid NoneType error + return Response() + + # Create Starlette routes for SSE and message handling + routes = [ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ] + + # Create and run Starlette app + starlette_app = Starlette(routes=routes) + uvicorn.run(starlette_app, host="127.0.0.1", port=port) + # endregion module_overview diff --git a/examples/snippets/docstrings/mcp/server/stdio.py b/examples/snippets/docstrings/mcp/server/stdio.py new file mode 100644 index 000000000..d3ccab019 --- /dev/null +++ b/examples/snippets/docstrings/mcp/server/stdio.py @@ -0,0 +1,28 @@ +"""Companion examples for src/mcp/server/stdio.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +import anyio + +from mcp.server.lowlevel.server import Server +from mcp.server.models import InitializationOptions +from mcp.server.stdio import stdio_server + + +# Stubs for undefined references in examples +async def create_my_server() -> Server[Any]: ... + + +def module_overview(init_options: InitializationOptions) -> None: + # region module_overview + async def run_server(): + async with stdio_server() as (read_stream, write_stream): + # read_stream contains incoming JSONRPCMessages from stdin + # write_stream allows sending JSONRPCMessages to stdout + server = await create_my_server() + await server.run(read_stream, write_stream, init_options) + + anyio.run(run_server) + # endregion module_overview diff --git a/examples/snippets/docstrings/mcp/shared/_httpx_utils.py b/examples/snippets/docstrings/mcp/shared/_httpx_utils.py new file mode 100644 index 000000000..7531f4683 --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/_httpx_utils.py @@ -0,0 +1,40 @@ +"""Companion examples for src/mcp/shared/_httpx_utils.py docstrings.""" + +from __future__ import annotations + +import httpx + +from mcp.shared._httpx_utils import create_mcp_http_client + + +async def create_mcp_http_client_basic() -> None: + # region create_mcp_http_client_basic + async with create_mcp_http_client() as client: + response = await client.get("https://api.example.com") + # endregion create_mcp_http_client_basic + + +async def create_mcp_http_client_headers() -> None: + # region create_mcp_http_client_headers + headers = {"Authorization": "Bearer token"} + async with create_mcp_http_client(headers) as client: + response = await client.get("/endpoint") + # endregion create_mcp_http_client_headers + + +async def create_mcp_http_client_timeout(headers: dict[str, str]) -> None: + # region create_mcp_http_client_timeout + timeout = httpx.Timeout(60.0, read=300.0) + async with create_mcp_http_client(headers, timeout) as client: + response = await client.get("/long-request") + # endregion create_mcp_http_client_timeout + + +async def create_mcp_http_client_auth(headers: dict[str, str], timeout: httpx.Timeout) -> None: + # region create_mcp_http_client_auth + from httpx import BasicAuth + + auth = BasicAuth(username="user", password="pass") + async with create_mcp_http_client(headers, timeout, auth) as client: + response = await client.get("/protected-endpoint") + # endregion create_mcp_http_client_auth diff --git a/examples/snippets/docstrings/mcp/shared/exceptions.py b/examples/snippets/docstrings/mcp/shared/exceptions.py new file mode 100644 index 000000000..6640916d3 --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/exceptions.py @@ -0,0 +1,20 @@ +"""Companion examples for src/mcp/shared/exceptions.py docstrings.""" + +from __future__ import annotations + +from mcp.shared.exceptions import UrlElicitationRequiredError +from mcp.types import ElicitRequestURLParams + + +def UrlElicitationRequiredError_usage() -> None: + # region UrlElicitationRequiredError_usage + raise UrlElicitationRequiredError( + [ + ElicitRequestURLParams( + message="Authorization required for your files", + url="https://example.com/oauth/authorize", + elicitation_id="auth-001", + ) + ] + ) + # endregion UrlElicitationRequiredError_usage diff --git a/examples/snippets/docstrings/mcp/shared/experimental/tasks/helpers.py b/examples/snippets/docstrings/mcp/shared/experimental/tasks/helpers.py new file mode 100644 index 000000000..e5b0483ee --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/experimental/tasks/helpers.py @@ -0,0 +1,17 @@ +"""Companion examples for src/mcp/shared/experimental/tasks/helpers.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.shared.experimental.tasks.helpers import cancel_task +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import CancelTaskRequestParams, CancelTaskResult + + +def cancel_task_usage(store: TaskStore) -> None: + # region cancel_task_usage + async def handle_cancel(ctx: Any, params: CancelTaskRequestParams) -> CancelTaskResult: + return await cancel_task(store, params.task_id) + + # endregion cancel_task_usage diff --git a/examples/snippets/docstrings/mcp/shared/metadata_utils.py b/examples/snippets/docstrings/mcp/shared/metadata_utils.py new file mode 100644 index 000000000..6b1dabe71 --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/metadata_utils.py @@ -0,0 +1,16 @@ +"""Companion examples for src/mcp/shared/metadata_utils.py docstrings.""" + +from __future__ import annotations + +from mcp.client.session import ClientSession +from mcp.shared.metadata_utils import get_display_name + + +async def get_display_name_usage(session: ClientSession) -> None: + # region get_display_name_usage + # In a client displaying available tools + tools = await session.list_tools() + for tool in tools.tools: + display_name = get_display_name(tool) + print(f"Available tool: {display_name}") + # endregion get_display_name_usage diff --git a/examples/snippets/docstrings/mcp/shared/response_router.py b/examples/snippets/docstrings/mcp/shared/response_router.py new file mode 100644 index 000000000..e8870dd65 --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/response_router.py @@ -0,0 +1,24 @@ +"""Companion examples for src/mcp/shared/response_router.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.shared.experimental.tasks.resolver import Resolver +from mcp.shared.response_router import ResponseRouter +from mcp.types import RequestId + + +def ResponseRouter_usage() -> None: + # region ResponseRouter_usage + class TaskResultHandler(ResponseRouter): + _pending_requests: dict[RequestId, Resolver[dict[str, Any]]] + + def route_response(self, request_id: Any, response: Any) -> bool: + resolver = self._pending_requests.pop(request_id, None) + if resolver: + resolver.set_result(response) + return True + return False + + # endregion ResponseRouter_usage diff --git a/examples/snippets/docstrings/mcp/shared/session.py b/examples/snippets/docstrings/mcp/shared/session.py new file mode 100644 index 000000000..a5cd7f994 --- /dev/null +++ b/examples/snippets/docstrings/mcp/shared/session.py @@ -0,0 +1,14 @@ +"""Companion examples for src/mcp/shared/session.py docstrings.""" + +from __future__ import annotations + +from typing import Any + +from mcp.shared.session import RequestResponder + + +async def RequestResponder_usage(request_responder: RequestResponder[Any, Any], result: Any) -> None: + # region RequestResponder_usage + with request_responder as resp: + await resp.respond(result) + # endregion RequestResponder_usage diff --git a/examples/snippets/pyproject.toml b/examples/snippets/pyproject.toml index 4e68846a0..e5bccb38b 100644 --- a/examples/snippets/pyproject.toml +++ b/examples/snippets/pyproject.toml @@ -11,8 +11,8 @@ dependencies = [ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" -[tool.setuptools] -packages = ["servers", "clients"] +[tool.setuptools.packages.find] +where = ["."] [project.scripts] server = "servers:run_server" diff --git a/pyproject.toml b/pyproject.toml index 737839a23..b73231b85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ executionEnvironments = [ ".", ], reportUnusedFunction = false, reportPrivateUsage = false }, { root = "examples/servers", reportUnusedFunction = false }, + { root = "examples/snippets/docstrings", reportUnusedFunction = false, reportUnusedVariable = false, reportAbstractUsage = false, reportUnusedClass = false, reportPrivateUsage = false }, ] [tool.ruff] @@ -152,6 +153,7 @@ max-complexity = 24 # Default is 10 "__init__.py" = ["F401"] "tests/server/mcpserver/test_func_metadata.py" = ["E501"] "tests/shared/test_progress_notifications.py" = ["PLW0603"] +"examples/snippets/docstrings/**/*.py" = ["F821", "F841"] [tool.ruff.lint.pylint] allow-magic-value-types = ["bytes", "float", "int", "str"] diff --git a/scripts/sync_snippets.py b/scripts/sync_snippets.py new file mode 100644 index 000000000..973eed5ad --- /dev/null +++ b/scripts/sync_snippets.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +"""Sync code snippets from example files into docstrings and markdown. + +This script finds snippet-source markers in Python source files and markdown +files, and replaces the content between them with code from the referenced +example files. + +Supported target files: +- Python source files under src/ (docstring code examples) +- Markdown files under docs/ +- README*.md files at the repo root + +Marker format (same in both docstrings and markdown): + + + ```python + # content replaced by script + ``` + + +With region extraction: + + + ```python + # content replaced by script + ``` + + +Path-less region markers (for src/ files only): + + + ```python + # content replaced by script + ``` + + + The companion file path is derived from the target file's location: + src/mcp/foo/bar.py → examples/snippets/docstrings/mcp/foo/bar.py + +The code fence language is inferred from the source file extension. + +Region markers in example files: + + # region region_name + code here + # endregion region_name + +Path resolution: +- All paths are relative to the repository root +- Path-less markers (#region) resolve via: src/X → COMPANION_BASE/X + +Usage: + uv run python scripts/sync_snippets.py # Sync all snippets + uv run python scripts/sync_snippets.py --check # Check mode for CI +""" + +from __future__ import annotations + +import argparse +import re +import sys +import textwrap +from dataclasses import dataclass, field +from pathlib import Path + +# Pattern to match snippet-source blocks. +# Captures: indent, source path, content between markers. +SNIPPET_PATTERN = re.compile( + r"^(?P[ \t]*)\n" + r"(?P.*?)" + r"^(?P=indent)", + re.MULTILINE | re.DOTALL, +) + +# Region markers in example files. +REGION_START_PATTERN = re.compile(r"^(?P\s*)# region (?P\S+)\s*$") +REGION_END_PATTERN = re.compile(r"^\s*# endregion (?P\S+)\s*$") + +# Base directory for companion example files (relative to repo root). +COMPANION_BASE = Path("examples/snippets/docstrings") + +# Source prefix stripped when deriving companion paths. +SOURCE_PREFIX = Path("src") + + +def find_repo_root() -> Path: + """Find the repository root by looking for pyproject.toml.""" + current = Path(__file__).resolve().parent + while current != current.parent: + if (current / "pyproject.toml").exists(): + return current + current = current.parent + raise RuntimeError("Could not find repository root (no pyproject.toml found)") + + +def resolve_source_path(source_path: str, repo_root: Path) -> Path: + """Resolve a source path relative to the repository root.""" + return (repo_root / source_path).resolve() + + +def list_regions(content: str) -> list[str]: + """List all region names defined in file content.""" + regions: list[str] = [] + for line in content.split("\n"): + m = REGION_START_PATTERN.match(line) + if m: + regions.append(m.group("name")) + return regions + + +def extract_region(content: str, region_name: str, file_path: str) -> str: + """Extract a named region from file content. + + Regions are delimited by: + # region region_name + ... code ... + # endregion region_name + + The extracted content is dedented using textwrap.dedent. + """ + lines = content.split("\n") + + start_idx = None + for i, line in enumerate(lines): + m = REGION_START_PATTERN.match(line) + if m and m.group("name") == region_name: + start_idx = i + break + + if start_idx is None: + available = list_regions(content) + available_str = ", ".join(available) if available else "(none)" + raise ValueError(f"Region '{region_name}' not found in {file_path}. Available regions: {available_str}") + + end_idx = None + for i in range(start_idx + 1, len(lines)): + m = REGION_END_PATTERN.match(lines[i]) + if m and m.group("name") == region_name: + end_idx = i + break + + if end_idx is None: + raise ValueError(f"No matching '# endregion {region_name}' found in {file_path}") + + region_lines = lines[start_idx + 1 : end_idx] + region_content = "\n".join(region_lines) + + return textwrap.dedent(region_content).strip() + + +@dataclass +class ProcessingResult: + """Result of processing a single file.""" + + file_path: Path + modified: bool = False + snippets_processed: int = 0 + errors: list[str] = field(default_factory=lambda: []) + + +class SnippetSyncer: + """Syncs code snippets from example files into target files.""" + + def __init__(self, repo_root: Path) -> None: + self.repo_root = repo_root + self._file_cache: dict[str, str] = {} + self._region_cache: dict[str, str] = {} + + def derive_companion_path(self, target_file: Path) -> str: + """Derive the companion example file path from a source file path. + + Maps src/mcp/X → examples/snippets/docstrings/mcp/X + """ + rel = target_file.relative_to(self.repo_root) + try: + sub = rel.relative_to(SOURCE_PREFIX) + except ValueError: + raise ValueError( + f"Cannot derive companion path for {rel}: " + f"path-less #region markers are only supported in {SOURCE_PREFIX}/ files" + ) from None + return str(COMPANION_BASE / sub) + + def resolve_source_ref(self, source_ref: str, target_file: Path) -> str: + """Resolve a source reference, expanding path-less #region markers.""" + if source_ref.startswith("#"): + companion = self.derive_companion_path(target_file) + return f"{companion}{source_ref}" + return source_ref + + def get_file_content(self, resolved_path: Path) -> str: + """Get file content, using cache.""" + key = str(resolved_path) + if key not in self._file_cache: + if not resolved_path.exists(): + raise FileNotFoundError(f"Example file not found: {resolved_path}") + self._file_cache[key] = resolved_path.read_text() + return self._file_cache[key] + + def get_source_content(self, source_ref: str) -> str: + """Get the content for a source reference (path or path#region).""" + if "#" in source_ref: + file_path_str, region_name = source_ref.rsplit("#", 1) + else: + file_path_str = source_ref + region_name = None + + resolved = resolve_source_path(file_path_str, self.repo_root) + file_content = self.get_file_content(resolved) + + if region_name is None: + return file_content.strip() + + cache_key = f"{resolved}#{region_name}" + if cache_key not in self._region_cache: + self._region_cache[cache_key] = extract_region(file_content, region_name, file_path_str) + return self._region_cache[cache_key] + + def process_file(self, file_path: Path, *, check: bool = False) -> ProcessingResult: + """Process a single file to sync snippets.""" + result = ProcessingResult(file_path=file_path) + + content = file_path.read_text() + original_content = content + + def replace_snippet(match: re.Match[str]) -> str: + indent = match.group("indent") + source_ref = match.group("source") + + try: + resolved_ref = self.resolve_source_ref(source_ref, file_path) + code = self.get_source_content(resolved_ref) + except (FileNotFoundError, ValueError) as e: + result.errors.append(f"{file_path}: {e}") + return match.group(0) + + result.snippets_processed += 1 + + # Infer language from file extension + raw_path = resolved_ref.split("#")[0] + ext = Path(raw_path).suffix.lstrip(".") + lang = {"py": "python", "yml": "yaml"}.get(ext, ext) + + # Indent the code to match the marker indentation + indented_code = textwrap.indent(code, indent) + + # Build replacement block + lines = [ + f"{indent}", + f"{indent}```{lang}", + indented_code, + f"{indent}```", + f"{indent}", + ] + return "\n".join(lines) + + content = SNIPPET_PATTERN.sub(replace_snippet, content) + + if content != original_content: + result.modified = True + if not check: + file_path.write_text(content) + + return result + + def find_target_files(self) -> list[Path]: + """Find all files that should be scanned for snippet markers.""" + files: list[Path] = [] + + # Python source files + src_dir = self.repo_root / "src" + if src_dir.exists(): + files.extend(src_dir.rglob("*.py")) + + # Markdown docs + docs_dir = self.repo_root / "docs" + if docs_dir.exists(): + files.extend(docs_dir.rglob("*.md")) + + # TODO(v2): Change to README.md when v2 is released. + readme = self.repo_root / "README.v2.md" + if readme.exists(): + files.append(readme) + + return sorted(files) + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser(description="Sync code snippets from example files") + parser.add_argument( + "--check", + action="store_true", + help="Check mode - verify snippets are up to date without modifying", + ) + args = parser.parse_args() + + repo_root = find_repo_root() + syncer = SnippetSyncer(repo_root) + + if args.check: + print("Checking code snippets are in sync...\n") + else: + print("Syncing code snippets from example files...\n") + + files = syncer.find_target_files() + results = [syncer.process_file(f, check=args.check) for f in files] + + # Report + modified = [r for r in results if r.modified] + all_errors: list[str] = [] + for r in results: + all_errors.extend(r.errors) + + if modified: + if args.check: + print(f"{len(modified)} file(s) out of sync:") + else: + print(f"Modified {len(modified)} file(s):") + for r in modified: + print(f" {r.file_path} ({r.snippets_processed} snippet(s))") + else: + print("All snippets are up to date") + + if all_errors: + print("\nErrors:") + for error in all_errors: + print(f" {error}") + sys.exit(2) + + if args.check and modified: + print("\nRun 'uv run python scripts/sync_snippets.py' to fix.") + sys.exit(1) + + print("\nSnippet sync complete!") + + +if __name__ == "__main__": + main() diff --git a/scripts/update_readme_snippets.py b/scripts/update_readme_snippets.py deleted file mode 100755 index 8a534e5cb..000000000 --- a/scripts/update_readme_snippets.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -"""Update README.md with live code snippets from example files. - -This script finds specially marked code blocks in README.md and updates them -with the actual code from the referenced files. - -Usage: - python scripts/update_readme_snippets.py - python scripts/update_readme_snippets.py --check # Check mode for CI -""" - -import argparse -import re -import sys -from pathlib import Path - - -def get_github_url(file_path: str) -> str: - """Generate a GitHub URL for the file. - - Args: - file_path: Path to the file relative to repo root - - Returns: - GitHub URL - """ - base_url = "https://github.com/modelcontextprotocol/python-sdk/blob/main" - return f"{base_url}/{file_path}" - - -def process_snippet_block(match: re.Match[str], check_mode: bool = False) -> str: - """Process a single snippet-source block. - - Args: - match: The regex match object - check_mode: If True, return original if no changes needed - - Returns: - The updated block content - """ - full_match = match.group(0) - indent = match.group(1) - file_path = match.group(2) - - try: - # Read the entire file - file = Path(file_path) - if not file.exists(): - print(f"Warning: File not found: {file_path}") - return full_match - - code = file.read_text().rstrip() - github_url = get_github_url(file_path) - - # Build the replacement block - indented_code = code.replace("\n", f"\n{indent}") - replacement = f"""{indent} -{indent}```python -{indent}{indented_code} -{indent}``` - -{indent}_Full example: [{file_path}]({github_url})_ -{indent}""" - - # In check mode, only check if code has changed - if check_mode: - # Extract existing code from the match - existing_content = match.group(3) - if existing_content is not None: - existing_lines = existing_content.strip().split("\n") - # Find code between ```python and ``` - code_lines = [] - in_code = False - for line in existing_lines: - if line.strip() == "```python": - in_code = True - elif line.strip() == "```": - break - elif in_code: - code_lines.append(line) - existing_code = "\n".join(code_lines).strip() - # Compare with the indented version we would generate - expected_code = code.replace("\n", f"\n{indent}").strip() - if existing_code == expected_code: - return full_match - - return replacement - - except Exception as e: - print(f"Error processing {file_path}: {e}") - return full_match - - -def update_readme_snippets(readme_path: Path = Path("README.md"), check_mode: bool = False) -> bool: - """Update code snippets in README.md with live code from source files. - - Args: - readme_path: Path to the README file - check_mode: If True, only check if updates are needed without modifying - - Returns: - True if file is up to date or was updated, False if check failed - """ - if not readme_path.exists(): - print(f"Error: README file not found: {readme_path}") - return False - - content = readme_path.read_text() - original_content = content - - # Pattern to match snippet-source blocks - # Matches: - # ... any content ... - # - pattern = r"^(\s*)\n" r"(.*?)" r"^\1" - - # Process all snippet-source blocks - updated_content = re.sub( - pattern, lambda m: process_snippet_block(m, check_mode), content, flags=re.MULTILINE | re.DOTALL - ) - - if check_mode: - if updated_content != original_content: - print( - f"Error: {readme_path} has outdated code snippets. " - "Run 'python scripts/update_readme_snippets.py' to update." - ) - return False - else: - print(f"✓ {readme_path} code snippets are up to date") - return True - else: - if updated_content != original_content: - readme_path.write_text(updated_content) - print(f"✓ Updated {readme_path}") - else: - print(f"✓ {readme_path} already up to date") - return True - - -def main(): - """Main entry point.""" - parser = argparse.ArgumentParser(description="Update README code snippets from source files") - parser.add_argument( - "--check", action="store_true", help="Check mode - verify snippets are up to date without modifying" - ) - # TODO(v2): Drop the `--readme` argument when v2 is released, and set to `README.md`. - parser.add_argument("--readme", default="README.v2.md", help="Path to README file (default: README.v2.md)") - - args = parser.parse_args() - - success = update_readme_snippets(Path(args.readme), check_mode=args.check) - - if not success: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/src/mcp/client/auth/extensions/client_credentials.py b/src/mcp/client/auth/extensions/client_credentials.py index cb6dafb40..3b87676b0 100644 --- a/src/mcp/client/auth/extensions/client_credentials.py +++ b/src/mcp/client/auth/extensions/client_credentials.py @@ -28,6 +28,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): Use this when you already have client credentials (client_id and client_secret). Example: + ```python provider = ClientCredentialsOAuthProvider( server_url="https://api.example.com", @@ -36,6 +37,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): client_secret="my-client-secret", ) ``` + """ def __init__( @@ -114,6 +116,7 @@ def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]: that doesn't need the audience parameter. Example: + ```python provider = PrivateKeyJWTOAuthProvider( server_url="https://api.example.com", @@ -122,6 +125,7 @@ def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]: assertion_provider=static_assertion_provider(my_prebuilt_jwt), ) ``` + Args: token: The pre-built JWT assertion string. @@ -143,6 +147,7 @@ class SignedJWTParameters(BaseModel): for use with `PrivateKeyJWTOAuthProvider`. Example: + ```python jwt_params = SignedJWTParameters( issuer="my-client-id", @@ -156,6 +161,7 @@ class SignedJWTParameters(BaseModel): assertion_provider=jwt_params.create_assertion_provider(), ) ``` + """ issuer: str = Field(description="Issuer for JWT assertions (typically client_id).") @@ -205,6 +211,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): In production scenarios, the JWT assertion is typically obtained from a workload identity provider (e.g., GCP, AWS IAM, Azure AD): + ```python async def get_workload_identity_token(audience: str) -> str: # Fetch JWT from your identity provider @@ -218,11 +225,13 @@ async def get_workload_identity_token(audience: str) -> str: assertion_provider=get_workload_identity_token, ) ``` + **Option 2: Static pre-built JWT** If you have a static JWT that doesn't need the audience parameter: + ```python provider = PrivateKeyJWTOAuthProvider( server_url="https://api.example.com", @@ -231,11 +240,13 @@ async def get_workload_identity_token(audience: str) -> str: assertion_provider=static_assertion_provider(my_prebuilt_jwt), ) ``` + **Option 3: SDK-signed JWT (for testing/simple setups)** For testing or simple deployments, use `SignedJWTParameters.create_assertion_provider()`: + ```python jwt_params = SignedJWTParameters( issuer="my-client-id", @@ -249,6 +260,7 @@ async def get_workload_identity_token(audience: str) -> str: assertion_provider=jwt_params.create_assertion_provider(), ) ``` + """ def __init__( diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index 7dc67c584..d5f1ca1ea 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -41,6 +41,7 @@ class Client: Streamable HTTP transport (pass a URL string), or a custom Transport instance. Example: + ```python from mcp.client import Client from mcp.server.mcpserver import MCPServer @@ -57,6 +58,7 @@ async def main(): asyncio.run(main()) ``` + """ server: Server[Any] | MCPServer | Transport | str diff --git a/src/mcp/client/experimental/task_handlers.py b/src/mcp/client/experimental/task_handlers.py index 0ab513236..b9aab16f2 100644 --- a/src/mcp/client/experimental/task_handlers.py +++ b/src/mcp/client/experimental/task_handlers.py @@ -187,13 +187,15 @@ class ExperimentalTaskHandlers: WARNING: These APIs are experimental and may change without notice. Example: + ```python handlers = ExperimentalTaskHandlers( get_task=my_get_task_handler, list_tasks=my_list_tasks_handler, ) - session = ClientSession(..., experimental_task_handlers=handlers) + session = ClientSession(read_stream, write_stream, experimental_task_handlers=handlers) ``` + """ # Pure task request handlers diff --git a/src/mcp/client/experimental/tasks.py b/src/mcp/client/experimental/tasks.py index a566df766..b9298269f 100644 --- a/src/mcp/client/experimental/tasks.py +++ b/src/mcp/client/experimental/tasks.py @@ -5,6 +5,7 @@ WARNING: These APIs are experimental and may change without notice. Example: + ```python # Call a tool as a task result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) @@ -23,6 +24,7 @@ # Cancel a task await session.experimental.cancel_task(task_id) ``` + """ from collections.abc import AsyncIterator @@ -74,11 +76,10 @@ async def call_tool_as_task( CreateTaskResult containing the task reference Example: + ```python # Create task - result = await session.experimental.call_tool_as_task( - "long_running_tool", {"input": "data"} - ) + result = await session.experimental.call_tool_as_task("long_running_tool", {"input": "data"}) task_id = result.task.task_id # Poll for completion @@ -91,6 +92,7 @@ async def call_tool_as_task( # Get result final = await session.experimental.get_task_result(task_id, CallToolResult) ``` + """ return await self._session.send_request( types.CallToolRequest( @@ -193,6 +195,7 @@ async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]: GetTaskResult for each poll Example: + ```python async for status in session.experimental.poll_task(task_id): print(f"Status: {status.status}") @@ -203,6 +206,7 @@ async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]: # Task is now terminal, get the result result = await session.experimental.get_task_result(task_id, CallToolResult) ``` + """ async for status in poll_until_terminal(self.get_task, task_id): yield status diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index a0ca751bd..cb8898286 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -206,10 +206,12 @@ def experimental(self) -> ExperimentalClientFeatures: These APIs are experimental and may change without notice. Example: + ```python status = await session.experimental.get_task(task_id) result = await session.experimental.get_task_result(task_id, CallToolResult) ``` + """ if self._experimental_features is None: self._experimental_features = ExperimentalClientFeatures(self) diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index 961021264..b0b8a6fcf 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -92,13 +92,17 @@ class ClientSessionGroup: the client and can be accessed via the session. Example: + ```python - name_fn = lambda name, server_info: f"{(server_info.name)}_{name}" + def name_fn(name: str, server_info: Any) -> str: + return f"{server_info.name}_{name}" + async with ClientSessionGroup(component_name_hook=name_fn) as group: for server_param in server_params: await group.connect_to_server(server_param) ... ``` + """ class _ComponentNames(BaseModel): diff --git a/src/mcp/server/experimental/request_context.py b/src/mcp/server/experimental/request_context.py index 3eba65822..f30cd957d 100644 --- a/src/mcp/server/experimental/request_context.py +++ b/src/mcp/server/experimental/request_context.py @@ -160,18 +160,26 @@ async def run_task( RuntimeError: If task support is not enabled or task_metadata is missing Example: + ```python - async def handle_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult: + async def handle_tool( + ctx: ServerRequestContext[Any, Any], + params: CallToolRequestParams, + ) -> CreateTaskResult: async def work(task: ServerTaskContext) -> CallToolResult: result = await task.elicit( message="Are you sure?", - requested_schema={"type": "object", ...} + requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}}, ) - confirmed = result.content.get("confirm", False) + if result.action == "accept" and result.content: + confirmed = result.content.get("confirm", False) + else: + confirmed = False return CallToolResult(content=[TextContent(text="Done" if confirmed else "Cancelled")]) return await ctx.experimental.run_task(work) ``` + WARNING: This API is experimental and may change without notice. """ diff --git a/src/mcp/server/experimental/task_context.py b/src/mcp/server/experimental/task_context.py index 1fc45badf..72c1f81df 100644 --- a/src/mcp/server/experimental/task_context.py +++ b/src/mcp/server/experimental/task_context.py @@ -56,20 +56,22 @@ class ServerTaskContext: - Status notifications via the session Example: + ```python async def my_task_work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Starting...") result = await task.elicit( message="Continue?", - requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}} + requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}}, ) - if result.content.get("ok"): + if result.action == "accept" and result.content and result.content.get("ok"): return CallToolResult(content=[TextContent(text="Done!")]) else: return CallToolResult(content=[TextContent(text="Cancelled")]) ``` + """ def __init__( diff --git a/src/mcp/server/experimental/task_support.py b/src/mcp/server/experimental/task_support.py index b54219504..bd437fc4f 100644 --- a/src/mcp/server/experimental/task_support.py +++ b/src/mcp/server/experimental/task_support.py @@ -33,18 +33,22 @@ class TaskSupport: Example: Simple in-memory setup: + ```python server.experimental.enable_tasks() ``` + Custom store/queue for distributed systems: + ```python server.experimental.enable_tasks( store=RedisTaskStore(redis_url), queue=RedisTaskMessageQueue(redis_url), ) ``` + """ store: TaskStore diff --git a/src/mcp/server/lowlevel/experimental.py b/src/mcp/server/lowlevel/experimental.py index 5a907b640..e12442076 100644 --- a/src/mcp/server/lowlevel/experimental.py +++ b/src/mcp/server/lowlevel/experimental.py @@ -120,18 +120,22 @@ def enable_tasks( Example: Simple in-memory setup: + ```python server.experimental.enable_tasks() ``` + Custom store/queue for distributed systems: + ```python server.experimental.enable_tasks( store=RedisTaskStore(redis_url), queue=RedisTaskMessageQueue(redis_url), ) ``` + WARNING: This API is experimental and may change without notice. """ diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 9c7105a7b..d17e8a901 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -535,25 +535,31 @@ def tool( - If False, unconditionally creates an unstructured tool Example: + ```python @server.tool() def my_tool(x: int) -> str: return str(x) ``` + + ```python @server.tool() async def tool_with_context(x: int, ctx: Context) -> str: await ctx.info(f"Processing {x}") return str(x) ``` + + ```python @server.tool() async def async_tool(x: int, context: Context) -> str: await context.report_progress(50, 100) return str(x) ``` + """ # Check if user passed function directly instead of calling decorator if callable(name): @@ -585,14 +591,20 @@ def completion(self): - context: Optional CompletionContext with previously resolved arguments Example: + ```python - @mcp.completion() - async def handle_completion(ref, argument, context): + @server.completion() + async def handle_completion( + ref: PromptReference | ResourceTemplateReference, + argument: CompletionArgument, + context: CompletionContext | None, + ) -> Completion | None: if isinstance(ref, ResourceTemplateReference): # Return completions based on ref, argument, and context return Completion(values=["option1", "option2"]) return None ``` + """ def decorator(func: _CallableT) -> _CallableT: @@ -655,25 +667,39 @@ def resource( meta: Optional metadata dictionary for the resource Example: + ```python @server.resource("resource://my-resource") def get_data() -> str: return "Hello, world!" + ``` + + + ```python @server.resource("resource://my-resource") async def get_data() -> str: data = await fetch_data() return f"Hello, world! {data}" + ``` + + + ```python @server.resource("resource://{city}/weather") def get_weather(city: str) -> str: return f"Weather for {city}" + ``` + + + ```python @server.resource("resource://{city}/weather") async def get_weather(city: str) -> str: data = await fetch_weather(city) return f"Weather for {city}: {data}" ``` + """ # Check if user passed function directly instead of calling decorator if callable(uri): @@ -757,6 +783,7 @@ def prompt( icons: Optional list of icons for the prompt Example: + ```python @server.prompt() def analyze_table(table_name: str) -> list[Message]: @@ -764,10 +791,14 @@ def analyze_table(table_name: str) -> list[Message]: return [ { "role": "user", - "content": f"Analyze this schema:\n{schema}" + "content": f"Analyze this schema:\n{schema}", } ] + ``` + + + ```python @server.prompt() async def analyze_file(path: str) -> list[Message]: content = await read_file(path) @@ -778,12 +809,13 @@ async def analyze_file(path: str) -> list[Message]: "type": "resource", "resource": { "uri": f"file://{path}", - "text": content - } - } + "text": content, + }, + }, } ] ``` + """ # Check if user passed function directly instead of calling decorator if callable(name): @@ -825,11 +857,13 @@ def custom_route( include_in_schema: Whether to include in OpenAPI schema, defaults to True Example: + ```python @server.custom_route("/health", methods=["GET"]) async def health_check(request: Request) -> Response: return JSONResponse({"status": "ok"}) ``` + """ def decorator( # pragma: no cover @@ -1113,6 +1147,7 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]): To use context in a tool function, add a parameter with the Context type annotation: + ```python @server.tool() async def my_tool(x: int, ctx: Context) -> str: @@ -1134,6 +1169,7 @@ async def my_tool(x: int, ctx: Context) -> str: return str(x) ``` + The context parameter name can be anything as long as it's annotated with Context. The context is optional - tools that don't need it can omit the parameter. diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 9007230ce..b557fbc62 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -3,31 +3,33 @@ This module implements a Server-Sent Events (SSE) transport layer for MCP servers. Example: + ```python # Create an SSE transport at an endpoint sse = SseServerTransport("/messages/") - # Create Starlette routes for SSE and message handling - routes = [ - Route("/sse", endpoint=handle_sse, methods=["GET"]), - Mount("/messages/", app=sse.handle_post_message), - ] - # Define handler functions - async def handle_sse(request): + async def handle_sse(request: Request) -> Response: async with sse.connect_sse( - request.scope, request.receive, request._send + request.scope, + request.receive, + request._send, ) as streams: - await app.run( - streams[0], streams[1], app.create_initialization_options() - ) + await app.run(streams[0], streams[1], app.create_initialization_options()) # Return empty response to avoid NoneType error return Response() + # Create Starlette routes for SSE and message handling + routes = [ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ] + # Create and run Starlette app starlette_app = Starlette(routes=routes) 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 diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index e526bab56..ab010677d 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -5,6 +5,7 @@ streams. Example: + ```python async def run_server(): async with stdio_server() as (read_stream, write_stream): @@ -15,6 +16,7 @@ async def run_server(): anyio.run(run_server) ``` + """ import sys diff --git a/src/mcp/shared/_httpx_utils.py b/src/mcp/shared/_httpx_utils.py index 251469eaa..e0ec2aa09 100644 --- a/src/mcp/shared/_httpx_utils.py +++ b/src/mcp/shared/_httpx_utils.py @@ -47,35 +47,44 @@ def create_mcp_http_client( Example: Basic usage with MCP defaults: + ```python async with create_mcp_http_client() as client: response = await client.get("https://api.example.com") ``` + With custom headers: + ```python headers = {"Authorization": "Bearer token"} async with create_mcp_http_client(headers) as client: response = await client.get("/endpoint") ``` + With both custom headers and timeout: + ```python timeout = httpx.Timeout(60.0, read=300.0) async with create_mcp_http_client(headers, timeout) as client: response = await client.get("/long-request") ``` + With authentication: + ```python from httpx import BasicAuth + auth = BasicAuth(username="user", password="pass") async with create_mcp_http_client(headers, timeout, auth) as client: response = await client.get("/protected-endpoint") ``` + """ # Set MCP defaults kwargs: dict[str, Any] = {"follow_redirects": True} diff --git a/src/mcp/shared/exceptions.py b/src/mcp/shared/exceptions.py index f153ea319..9b1df452a 100644 --- a/src/mcp/shared/exceptions.py +++ b/src/mcp/shared/exceptions.py @@ -65,15 +65,19 @@ class UrlElicitationRequiredError(MCPError): must complete one or more URL elicitations before the request can be processed. Example: + ```python - raise UrlElicitationRequiredError([ - ElicitRequestURLParams( - message="Authorization required for your files", - url="https://example.com/oauth/authorize", - elicitation_id="auth-001" - ) - ]) + raise UrlElicitationRequiredError( + [ + ElicitRequestURLParams( + message="Authorization required for your files", + url="https://example.com/oauth/authorize", + elicitation_id="auth-001", + ) + ] + ) ``` + """ def __init__(self, elicitations: list[ElicitRequestURLParams], message: str | None = None): diff --git a/src/mcp/shared/experimental/tasks/helpers.py b/src/mcp/shared/experimental/tasks/helpers.py index 3f91cd0d0..4032172ce 100644 --- a/src/mcp/shared/experimental/tasks/helpers.py +++ b/src/mcp/shared/experimental/tasks/helpers.py @@ -72,10 +72,12 @@ async def cancel_task( - Task is already in a terminal state (completed, failed, cancelled) Example: + ```python - async def handle_cancel(ctx, params: CancelTaskRequestParams) -> CancelTaskResult: + async def handle_cancel(ctx: Any, params: CancelTaskRequestParams) -> CancelTaskResult: return await cancel_task(store, params.task_id) ``` + """ task = await store.get_task(task_id) if task is None: diff --git a/src/mcp/shared/metadata_utils.py b/src/mcp/shared/metadata_utils.py index 6e4d33da0..77330142c 100644 --- a/src/mcp/shared/metadata_utils.py +++ b/src/mcp/shared/metadata_utils.py @@ -18,6 +18,7 @@ def get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implemen For other objects: title > name Example: + ```python # In a client displaying available tools tools = await session.list_tools() @@ -25,6 +26,7 @@ def get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implemen display_name = get_display_name(tool) print(f"Available tool: {display_name}") ``` + Args: obj: An MCP object with name and optional title fields diff --git a/src/mcp/shared/response_router.py b/src/mcp/shared/response_router.py index fe24b016f..b8652d9bb 100644 --- a/src/mcp/shared/response_router.py +++ b/src/mcp/shared/response_router.py @@ -25,15 +25,19 @@ class ResponseRouter(Protocol): and deliver the response/error to the appropriate handler. Example: + ```python class TaskResultHandler(ResponseRouter): - def route_response(self, request_id, response): + _pending_requests: dict[RequestId, Resolver[dict[str, Any]]] + + def route_response(self, request_id: Any, response: Any) -> bool: resolver = self._pending_requests.pop(request_id, None) if resolver: resolver.set_result(response) return True return False ``` + """ def route_response(self, request_id: RequestId, response: dict[str, Any]) -> bool: diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index b617d702f..f01db461f 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -60,10 +60,12 @@ class RequestResponder(Generic[ReceiveRequestT, SendResultT]): cancellation handling: Example: + ```python with request_responder as resp: await resp.respond(result) ``` + The context manager ensures: 1. Proper cancellation scope setup and cleanup