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..e38e2bf03 100644 --- a/README.v2.md +++ b/README.v2.md @@ -181,8 +181,6 @@ 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,8 +277,6 @@ 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,8 +306,6 @@ 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,8 +331,6 @@ 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,8 +359,6 @@ 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,8 +442,6 @@ 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,8 +546,6 @@ 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,8 +573,6 @@ 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 @@ -624,8 +608,6 @@ def my_resource(): return "content" ``` -_Full example: [examples/mcpserver/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/mcpserver/icons_demo.py)_ - ### Images MCPServer provides an `Image` class that automatically handles image data: @@ -648,8 +630,6 @@ 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,8 +695,6 @@ 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 +783,8 @@ 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,8 +891,6 @@ 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,8 +934,6 @@ 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,8 +962,6 @@ 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,8 +1020,6 @@ 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,8 +1192,6 @@ 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,8 +1239,6 @@ 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,8 +1299,6 @@ 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,8 +1392,6 @@ 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,8 +1437,6 @@ 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,8 +1492,6 @@ 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,8 +1530,6 @@ 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,8 +1693,6 @@ 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,8 +1767,6 @@ 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,8 +1858,6 @@ 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,8 +1931,6 @@ 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,8 +1977,6 @@ 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,8 +2023,6 @@ 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,8 +2121,6 @@ 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,8 +2152,6 @@ 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,8 +2227,6 @@ 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,8 +2331,6 @@ 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/docs/client-quickstart.md b/docs/client-quickstart.md new file mode 100644 index 000000000..020183265 --- /dev/null +++ b/docs/client-quickstart.md @@ -0,0 +1,359 @@ +# Quickstart: Build an LLM-powered chatbot + +In this tutorial, we'll build an LLM-powered chatbot that connects to an MCP server, discovers its tools, and uses Claude to call them. + +Before you begin, it helps to have gone through the [server quickstart](https://modelcontextprotocol.io/quickstart/server) so you understand how clients and servers communicate. + +[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/clients/quickstart-client/) + +## Prerequisites + +This quickstart assumes you have familiarity with: + +- Python +- LLMs like Claude + +Before starting, ensure your system meets these requirements: + +- Python 3.10 or later installed +- Latest version of `uv` installed +- An Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys) + +## Set up your environment + +First, create a new Python project with `uv`: + +=== "macOS/Linux" + + ```bash + # Create project directory + uv init mcp-client + cd mcp-client + + # Install required packages + uv add mcp anthropic + + # Remove boilerplate files + rm main.py + + # Create our main file + touch client.py + ``` + +=== "Windows" + + ```powershell + # Create project directory + uv init mcp-client + cd mcp-client + + # Install required packages + uv add mcp anthropic + + # Remove boilerplate files + del main.py + + # Create our main file + new-item client.py + ``` + +## Creating the client + +### Basic client structure + +First, let's set up our imports and create the basic client class in `client.py`: + + +```python +import asyncio +import os +import sys +from contextlib import AsyncExitStack +from pathlib import Path + +from anthropic import Anthropic +from anthropic.types import MessageParam, TextBlock, TextBlockParam, ToolParam, ToolResultBlockParam, ToolUseBlock +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.types import TextContent + +# Claude model constant +ANTHROPIC_MODEL = "claude-sonnet-4-5" + + +class MCPClient: + def __init__(self) -> None: + # Initialize session and client objects + self.session: ClientSession | None = None + self.exit_stack = AsyncExitStack() + self._anthropic: Anthropic | None = None + + @property + def anthropic(self) -> Anthropic: + """Lazy-initialize Anthropic client when needed""" + if self._anthropic is None: + self._anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) + return self._anthropic +``` + + +### Server connection management + +Next, we'll implement the method to connect to an MCP server: + + +```python +async def connect_to_server(self, server_script_path: str) -> None: + """Connect to an MCP server + + Args: + server_script_path: Path to the server script (.py or .js) + """ + is_python = server_script_path.endswith(".py") + is_js = server_script_path.endswith(".js") + if not (is_python or is_js): + raise ValueError("Server script must be a .py or .js file") + + if is_python: + path = Path(server_script_path).resolve() + server_params = StdioServerParameters( + command="uv", + args=["--directory", str(path.parent), "run", path.name], + env=None, + ) + else: + server_params = StdioServerParameters(command="node", args=[server_script_path], env=None) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) +``` + + +### Query processing logic + +Now let's add the core functionality for processing queries and handling tool calls: + + +```python +async def process_query(self, query: str) -> str: + """Process a query using Claude and available tools""" + assert self.session is not None + messages: list[MessageParam] = [{"role": "user", "content": query}] + + response = await self.session.list_tools() + available_tools: list[ToolParam] = [ + {"name": tool.name, "description": tool.description or "", "input_schema": tool.input_schema or {}} + for tool in response.tools + ] + + # Initial Claude API call + response = self.anthropic.messages.create( + model=ANTHROPIC_MODEL, max_tokens=1000, messages=messages, tools=available_tools + ) + + # Process response and handle tool calls + final_text: list[str] = [] + + for content in response.content: + if isinstance(content, TextBlock): + final_text.append(content.text) + elif isinstance(content, ToolUseBlock): + tool_name = content.name + tool_args = content.input + + # Execute tool call + assert self.session is not None + result = await self.session.call_tool(tool_name, tool_args) + final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") + + # Continue conversation with tool results + messages.append({"role": "assistant", "content": response.content}) + tool_result_content: list[TextBlockParam] = [ + {"type": "text", "text": block.text} for block in result.content if isinstance(block, TextContent) + ] + tool_result: ToolResultBlockParam = { + "type": "tool_result", + "tool_use_id": content.id, + "content": tool_result_content, + } + messages.append({"role": "user", "content": [tool_result]}) + + # Get next response from Claude + response = self.anthropic.messages.create( + model=ANTHROPIC_MODEL, + max_tokens=1000, + messages=messages, + ) + + response_text = response.content[0] + if isinstance(response_text, TextBlock): + final_text.append(response_text.text) + + return "\n".join(final_text) +``` + + +### Interactive chat interface + +Now we'll add the chat loop and cleanup functionality: + + +```python +async def chat_loop(self) -> None: + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + + while True: + try: + query = input("\nQuery: ").strip() + + if query.lower() == "quit": + break + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + +async def cleanup(self) -> None: + """Clean up resources""" + await self.exit_stack.aclose() +``` + + +### Main entry point + +Finally, we'll add the main execution logic: + + +```python +async def main() -> None: + if len(sys.argv) < 2: + print("Usage: python client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_server(sys.argv[1]) + + # Check if we have a valid API key to continue + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + print("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:") + print(" export ANTHROPIC_API_KEY=your-api-key-here") + return + + await client.chat_loop() + finally: + await client.cleanup() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +## Running the client + +To run your client with any MCP server: + +=== "macOS/Linux" + + ```bash + ANTHROPIC_API_KEY=your-key-here uv run client.py path/to/server.py + + # Example: connect to the weather server from the server quickstart + ANTHROPIC_API_KEY=your-key-here uv run client.py /absolute/path/to/weather/weather.py + ``` + +=== "Windows" + + ```powershell + $env:ANTHROPIC_API_KEY="your-key-here"; uv run client.py path\to\server.py + ``` + +The client will: + +1. Connect to the specified server +2. List available tools +3. Start an interactive chat session where you can: + - Enter queries + - See tool executions + - Get responses from Claude + +## What's happening under the hood + +When you submit a query: + +1. Your query is sent to Claude along with the tool descriptions discovered during connection +2. Claude decides which tools (if any) to use +3. The client executes any requested tool calls through the server +4. Results are sent back to Claude +5. Claude provides a natural language response +6. The response is displayed to you + +## Troubleshooting + +### Server path issues + +- Double-check the path to your server script is correct +- Use the absolute path if the relative path isn't working +- For Windows users, make sure to use forward slashes (`/`) or escaped backslashes (`\\`) in the path +- Verify the server file has the correct extension (`.py` for Python or `.js` for Node.js) + +Example of correct path usage: + +=== "macOS/Linux" + + ```bash + # Relative path + uv run client.py ./server/weather.py + + # Absolute path + uv run client.py /Users/username/projects/mcp-server/weather.py + ``` + +=== "Windows" + + ```powershell + # Relative path + uv run client.py .\server\weather.py + + # Absolute path (either format works) + uv run client.py C:\projects\mcp-server\weather.py + uv run client.py C:/projects/mcp-server/weather.py + ``` + +### Response timing + +- The first response might take up to 30 seconds to return +- This is normal and happens while: + - The server initializes + - Claude processes the query + - Tools are being executed +- Subsequent responses are typically faster +- Don't interrupt the process during this initial waiting period + +### Common error messages + +If you see: + +- `FileNotFoundError`: Check your server script path +- `ModuleNotFoundError: No module named 'mcp'`: Make sure you ran `uv add mcp anthropic` in your project +- `ValueError: Server script must be a .py or .js file`: The client only supports Python and Node.js servers +- `anthropic.AuthenticationError`: Check that your `ANTHROPIC_API_KEY` is valid + +## Next steps + +- **[Example servers](https://modelcontextprotocol.io/examples)** — Browse official MCP servers and implementations +- **[Example clients](https://modelcontextprotocol.io/clients)** — View clients that support MCP integrations diff --git a/examples/clients/quickstart-client/README.md b/examples/clients/quickstart-client/README.md new file mode 100644 index 000000000..ace2cce0b --- /dev/null +++ b/examples/clients/quickstart-client/README.md @@ -0,0 +1,3 @@ +# An LLM-Powered Chatbot MCP Client written in Python + +See the [client quickstart](../../../docs/client-quickstart.md) tutorial for more information. diff --git a/examples/clients/quickstart-client/client.py b/examples/clients/quickstart-client/client.py new file mode 100644 index 000000000..3cbe03b5b --- /dev/null +++ b/examples/clients/quickstart-client/client.py @@ -0,0 +1,178 @@ +# region MCPClient_init +import asyncio +import os +import sys +from contextlib import AsyncExitStack +from pathlib import Path + +from anthropic import Anthropic +from anthropic.types import MessageParam, TextBlock, TextBlockParam, ToolParam, ToolResultBlockParam, ToolUseBlock +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.types import TextContent + +# Claude model constant +ANTHROPIC_MODEL = "claude-sonnet-4-5" + + +class MCPClient: + def __init__(self) -> None: + # Initialize session and client objects + self.session: ClientSession | None = None + self.exit_stack = AsyncExitStack() + self._anthropic: Anthropic | None = None + + @property + def anthropic(self) -> Anthropic: + """Lazy-initialize Anthropic client when needed""" + if self._anthropic is None: + self._anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) + return self._anthropic + + # endregion MCPClient_init + + # region MCPClient_connect_to_server + async def connect_to_server(self, server_script_path: str) -> None: + """Connect to an MCP server + + Args: + server_script_path: Path to the server script (.py or .js) + """ + is_python = server_script_path.endswith(".py") + is_js = server_script_path.endswith(".js") + if not (is_python or is_js): + raise ValueError("Server script must be a .py or .js file") + + if is_python: + path = Path(server_script_path).resolve() + server_params = StdioServerParameters( + command="uv", + args=["--directory", str(path.parent), "run", path.name], + env=None, + ) + else: + server_params = StdioServerParameters(command="node", args=[server_script_path], env=None) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) + + # endregion MCPClient_connect_to_server + + # region MCPClient_process_query + async def process_query(self, query: str) -> str: + """Process a query using Claude and available tools""" + assert self.session is not None + messages: list[MessageParam] = [{"role": "user", "content": query}] + + response = await self.session.list_tools() + available_tools: list[ToolParam] = [ + {"name": tool.name, "description": tool.description or "", "input_schema": tool.input_schema or {}} + for tool in response.tools + ] + + # Initial Claude API call + response = self.anthropic.messages.create( + model=ANTHROPIC_MODEL, max_tokens=1000, messages=messages, tools=available_tools + ) + + # Process response and handle tool calls + final_text: list[str] = [] + + for content in response.content: + if isinstance(content, TextBlock): + final_text.append(content.text) + elif isinstance(content, ToolUseBlock): + tool_name = content.name + tool_args = content.input + + # Execute tool call + assert self.session is not None + result = await self.session.call_tool(tool_name, tool_args) + final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") + + # Continue conversation with tool results + messages.append({"role": "assistant", "content": response.content}) + tool_result_content: list[TextBlockParam] = [ + {"type": "text", "text": block.text} for block in result.content if isinstance(block, TextContent) + ] + tool_result: ToolResultBlockParam = { + "type": "tool_result", + "tool_use_id": content.id, + "content": tool_result_content, + } + messages.append({"role": "user", "content": [tool_result]}) + + # Get next response from Claude + response = self.anthropic.messages.create( + model=ANTHROPIC_MODEL, + max_tokens=1000, + messages=messages, + ) + + response_text = response.content[0] + if isinstance(response_text, TextBlock): + final_text.append(response_text.text) + + return "\n".join(final_text) + + # endregion MCPClient_process_query + + # region MCPClient_chat_loop + async def chat_loop(self) -> None: + """Run an interactive chat loop""" + print("\nMCP Client Started!") + print("Type your queries or 'quit' to exit.") + + while True: + try: + query = input("\nQuery: ").strip() + + if query.lower() == "quit": + break + + response = await self.process_query(query) + print("\n" + response) + + except Exception as e: + print(f"\nError: {str(e)}") + + async def cleanup(self) -> None: + """Clean up resources""" + await self.exit_stack.aclose() + + # endregion MCPClient_chat_loop + + +# region main_entrypoint +async def main() -> None: + if len(sys.argv) < 2: + print("Usage: python client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_server(sys.argv[1]) + + # Check if we have a valid API key to continue + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + print("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:") + print(" export ANTHROPIC_API_KEY=your-api-key-here") + return + + await client.chat_loop() + finally: + await client.cleanup() + + +if __name__ == "__main__": + asyncio.run(main()) +# endregion main_entrypoint diff --git a/examples/clients/quickstart-client/mcp_quickstart_client/__init__.py b/examples/clients/quickstart-client/mcp_quickstart_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/clients/quickstart-client/pyproject.toml b/examples/clients/quickstart-client/pyproject.toml new file mode 100644 index 000000000..44ceda5de --- /dev/null +++ b/examples/clients/quickstart-client/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "mcp-quickstart-client" +version = "0.1.0" +description = "Tutorial companion: an LLM-powered chatbot MCP client" +requires-python = ">=3.10" +dependencies = [ + "anthropic>=0.72.0", + "mcp", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["mcp_quickstart_client"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = [] 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/mkdocs.yml b/mkdocs.yml index 070c533e3..f6e8bb4d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ site_url: https://modelcontextprotocol.github.io/python-sdk nav: - Introduction: index.md - Installation: installation.md + - Client Quickstart: client-quickstart.md - Migration Guide: migration.md - Documentation: - Concepts: concepts.md diff --git a/pyproject.toml b/pyproject.toml index 737839a23..8d541a717 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,8 @@ required-version = ">=0.9.5" dev = [ # We add mcp[cli,ws] so `uv sync` considers the extras. "mcp[cli,ws]", + # Pull in workspace members whose deps aren't already covered by the root package. + "mcp-quickstart-client", "pyright>=1.1.400", "pytest>=8.3.4", "ruff>=0.8.5", @@ -120,6 +122,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 +155,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"] @@ -165,6 +169,7 @@ members = ["examples/clients/*", "examples/servers/*", "examples/snippets"] [tool.uv.sources] mcp = { workspace = true } +mcp-quickstart-client = { workspace = true } strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" } [tool.pytest.ini_options] 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 diff --git a/uv.lock b/uv.lock index d01d510f1..032bf25f7 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,7 @@ resolution-markers = [ members = [ "mcp", "mcp-everything-server", + "mcp-quickstart-client", "mcp-simple-auth", "mcp-simple-auth-client", "mcp-simple-chatbot", @@ -38,6 +39,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" }, +] + [[package]] name = "anyio" version = "4.10.0" @@ -527,6 +547,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/0c/03cc99bf3b6328604b10829de3460f2b2ad3373200c45665c38508e550c6/dirty_equals-0.9.0-py3-none-any.whl", hash = "sha256:ff4d027f5cfa1b69573af00f7ba9043ea652dbdce3fe5cbe828e478c7346db9c", size = 28226, upload-time = "2025-01-11T23:23:37.489Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -673,6 +711,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -817,6 +952,7 @@ dev = [ { name = "dirty-equals" }, { name = "inline-snapshot" }, { name = "mcp", extra = ["cli", "ws"] }, + { name = "mcp-quickstart-client" }, { name = "pillow" }, { name = "pyright" }, { name = "pytest" }, @@ -865,6 +1001,7 @@ dev = [ { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "inline-snapshot", specifier = ">=0.23.0" }, { name = "mcp", extras = ["cli", "ws"], editable = "." }, + { name = "mcp-quickstart-client", editable = "examples/clients/quickstart-client" }, { name = "pillow", specifier = ">=12.0" }, { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.3.4" }, @@ -920,6 +1057,21 @@ dev = [ { name = "ruff", specifier = ">=0.6.9" }, ] +[[package]] +name = "mcp-quickstart-client" +version = "0.1.0" +source = { editable = "examples/clients/quickstart-client" } +dependencies = [ + { name = "anthropic" }, + { name = "mcp" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", specifier = ">=0.72.0" }, + { name = "mcp", editable = "." }, +] + [[package]] name = "mcp-simple-auth" version = "0.1.0"