diff --git a/CLAUDE.md b/CLAUDE.md index a4ef16e42..ffba37173 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,11 @@ When making breaking changes, document them in `docs/migration.md`. Include: Search for related sections in the migration guide and group related changes together rather than adding new standalone sections. +## Documentation + +- After a heading, always add an introductory paragraph before any code block, list, table, or + other content. Never go directly from a heading to non-paragraph content. + ## Python Tools ## Code Formatting diff --git a/README.v2.md b/README.v2.md index 67f181811..a9d9082a3 100644 --- a/README.v2.md +++ b/README.v2.md @@ -595,7 +595,7 @@ _Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/mo MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts: -```python +```python skip-run="true" from mcp.server.mcpserver import MCPServer, Icon # Create an icon from a file path or URL @@ -1079,7 +1079,7 @@ The MCPServer server instance accessible via `ctx.mcp_server` provides access to - `stateless_http` - Whether the server operates in stateless mode - And other configuration options -```python +```python skip-run="true" @mcp.tool() def server_info(ctx: Context) -> dict: """Get information about the current server.""" @@ -1106,7 +1106,7 @@ The session object accessible via `ctx.session` provides advanced control over c - `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed - `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed -```python +```python skip-run="true" @mcp.tool() async def notify_data_update(resource_uri: str, ctx: Context) -> str: """Update data and notify clients of the change.""" @@ -1134,7 +1134,7 @@ The request context accessible via `ctx.request_context` contains request-specif - `ctx.request_context.request` - The original MCP request object for advanced processing - `ctx.request_context.request_id` - Unique identifier for this request -```python +```python skip-run="true" # Example with typed lifespan context @dataclass class AppContext: diff --git a/docs/concepts.md b/docs/concepts.md index a2d6eb8d3..a7d4fffd1 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,13 +1,167 @@ # Concepts -!!! warning "Under Construction" +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data +and functionality to LLM applications in a standardized way. Think of it like a web API, but specifically +designed for LLM interactions. - This page is currently being written. Check back soon for complete documentation. +## Architecture - +MCP follows a client-server architecture: + +- **Hosts** are LLM applications (like Claude Desktop or an IDE) that initiate connections +- **Clients** maintain 1:1 connections with servers, inside the host application +- **Servers** provide context, tools, and prompts to clients + +```text +Host (e.g. Claude Desktop) +├── Client A ↔ Server A (e.g. file system) +├── Client B ↔ Server B (e.g. database) +└── Client C ↔ Server C (e.g. API wrapper) +``` + +## Primitives + +MCP servers expose three core primitives: **resources**, **tools**, and **prompts**. + +### Resources + +Resources provide data to LLMs — similar to GET endpoints in a REST API. They load information into the +LLM's context without performing computation or causing side effects. + +Resources can be static (fixed URI) or use URI templates for dynamic content: + +```python +from mcp.server.mcpserver import MCPServer + +mcp = MCPServer("Demo") + + +@mcp.resource("config://app") +def get_config() -> dict[str, str]: + """Expose application configuration.""" + return {"theme": "dark", "version": "2.0"} + + +@mcp.resource("users://{user_id}/profile") +def get_profile(user_id: str) -> dict[str, str]: + """Get a user profile by ID.""" + return {"user_id": user_id, "name": "Alice"} +``` + + + +### Tools + +Tools let LLMs take actions — similar to POST endpoints. They perform computation, call external APIs, +or produce side effects: + +```python +from mcp.server.mcpserver import MCPServer + +mcp = MCPServer("Demo") + + +@mcp.tool() +def send_email(to: str, subject: str, body: str) -> str: + """Send an email to the given recipient.""" + return f"Email sent to {to}" +``` + +Tools support structured output, progress reporting, and more. + + +### Prompts + +Prompts are reusable templates for LLM interactions. They help standardize common workflows: + +```python +from mcp.server.mcpserver import MCPServer + +mcp = MCPServer("Demo") + + +@mcp.prompt() +def review_code(code: str, language: str = "python") -> str: + """Generate a code review prompt.""" + return f"Please review the following {language} code:\n\n{code}" +``` + + + +## Transports + +MCP supports multiple transport mechanisms for client-server communication: + +| Transport | Use case | How it works | +|---|---|---| +| **Streamable HTTP** | Remote servers, production deployments | HTTP POST with optional SSE streaming | +| **stdio** | Local processes, CLI tools | Communication over stdin/stdout | +| **SSE** | Legacy remote servers | Server-Sent Events over HTTP (deprecated in favor of Streamable HTTP) | + + + +## Context + +When handling requests, your functions can access a **context object** that provides capabilities +like logging, progress reporting, and access to the current session: + +```python +from mcp.server.mcpserver import Context, MCPServer + +mcp = MCPServer("Demo") + + +@mcp.tool() +async def long_task(ctx: Context) -> str: + """A tool that reports progress.""" + await ctx.report_progress(0, 100) + # ... do work ... + await ctx.report_progress(100, 100) + return "Done" +``` + +Context enables logging, elicitation, sampling, and more. + + +## Server lifecycle + +Servers support a **lifespan** pattern for managing startup and shutdown logic — for example +initializing a database connection pool on startup and closing it on shutdown: + +```python +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass + +from mcp.server.mcpserver import MCPServer + + +@dataclass +class AppContext: + db_url: str + + +@asynccontextmanager +async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]: + # Initialize on startup + ctx = AppContext(db_url="postgresql://localhost/mydb") + try: + yield ctx + finally: + # Cleanup on shutdown + pass + + +mcp = MCPServer("My App", lifespan=app_lifespan) +``` + + + +## Next steps + +Continue learning about MCP: + +- **[Quickstart](quickstart.md)** — build your first server +- **[Testing](testing.md)** — test your server with the `Client` class +- **[Authorization](authorization.md)** — securing your servers with OAuth 2.1 +- **[API Reference](api.md)** — full API documentation diff --git a/docs/experimental/index.md b/docs/experimental/index.md index 1d496b3f1..cab64f7de 100644 --- a/docs/experimental/index.md +++ b/docs/experimental/index.md @@ -26,7 +26,7 @@ Tasks are useful for: Experimental features are accessed via the `.experimental` property: -```python +```python skip="true" # Server-side @server.experimental.get_task() async def handle_get_task(request: GetTaskRequest) -> GetTaskResult: diff --git a/docs/experimental/tasks-client.md b/docs/experimental/tasks-client.md index 0374ed86b..225079562 100644 --- a/docs/experimental/tasks-client.md +++ b/docs/experimental/tasks-client.md @@ -10,7 +10,7 @@ This guide covers calling task-augmented tools from clients, handling the `input Call a tool as a task and poll for the result: -```python +```python skip="true" from mcp.client.session import ClientSession from mcp.types import CallToolResult @@ -38,7 +38,7 @@ async with ClientSession(read, write) as session: Use `call_tool_as_task()` to invoke a tool with task augmentation: -```python +```python skip="true" result = await session.experimental.call_tool_as_task( "my_tool", # Tool name {"arg": "value"}, # Arguments @@ -62,7 +62,7 @@ The response is a `CreateTaskResult` containing: The `poll_task()` async iterator polls until the task reaches a terminal state: -```python +```python skip="true" async for status in session.experimental.poll_task(task_id): print(f"Status: {status.status}") if status.statusMessage: @@ -79,7 +79,7 @@ It automatically: When a task needs user input (elicitation), it transitions to `input_required`. You must call `get_task_result()` to receive and respond to the elicitation: -```python +```python skip="true" async for status in session.experimental.poll_task(task_id): print(f"Status: {status.status}") @@ -95,7 +95,7 @@ The elicitation callback (set during session creation) handles the actual user i To handle elicitation requests from the server, provide a callback when creating the session: -```python +```python skip="true" from mcp.types import ElicitRequestParams, ElicitResult async def handle_elicitation(context, params: ElicitRequestParams) -> ElicitResult: @@ -124,7 +124,7 @@ async with ClientSession( Similarly, handle sampling requests with a callback: -```python +```python skip="true" from mcp.types import CreateMessageRequestParams, CreateMessageResult, TextContent async def handle_sampling(context, params: CreateMessageRequestParams) -> CreateMessageResult: @@ -150,7 +150,7 @@ async with ClientSession( Once a task completes, retrieve the result: -```python +```python skip="true" if status.status == "completed": result = await session.experimental.get_task_result(task_id, CallToolResult) for content in result.content: @@ -174,7 +174,7 @@ The result type matches the original request: Cancel a running task: -```python +```python skip="true" cancel_result = await session.experimental.cancel_task(task_id) print(f"Cancelled, status: {cancel_result.status}") ``` @@ -185,7 +185,7 @@ Note: Cancellation is cooperative—the server must check for and handle cancell View all tasks on the server: -```python +```python skip="true" result = await session.experimental.list_tasks() for task in result.tasks: print(f"{task.taskId}: {task.status}") @@ -205,7 +205,7 @@ Servers can send task-augmented requests to clients. This is useful when the ser Register task handlers to declare what task-augmented requests your client accepts: -```python +```python skip="true" from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers from mcp.types import ( CreateTaskResult, GetTaskResult, GetTaskPayloadResult, @@ -279,7 +279,7 @@ This enables flows where: A client that handles all task scenarios: -```python +```python skip="true" import anyio from mcp.client.session import ClientSession from mcp.client.stdio import stdio_client @@ -336,7 +336,7 @@ if __name__ == "__main__": Handle task errors gracefully: -```python +```python skip="true" from mcp.shared.exceptions import MCPError try: diff --git a/docs/experimental/tasks-server.md b/docs/experimental/tasks-server.md index 761dc5de5..46759ae71 100644 --- a/docs/experimental/tasks-server.md +++ b/docs/experimental/tasks-server.md @@ -10,7 +10,7 @@ This guide covers implementing task support in MCP servers, from basic setup to The simplest way to add task support: -```python +```python skip="true" from mcp.server import Server from mcp.server.experimental.task_context import ServerTaskContext from mcp.types import CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED @@ -57,7 +57,7 @@ That's it. `enable_tasks()` automatically: Tools declare task support via the `execution.taskSupport` field: -```python +```python skip="true" from mcp.types import Tool, ToolExecution, TASK_REQUIRED, TASK_OPTIONAL, TASK_FORBIDDEN Tool( @@ -75,7 +75,7 @@ Tool( Validate the request matches your tool's requirements: -```python +```python skip="true" @server.call_tool() async def handle_tool(name: str, arguments: dict): ctx = server.request_context @@ -95,7 +95,7 @@ async def handle_tool(name: str, arguments: dict): `run_task()` is the recommended way to execute task work: -```python +```python skip="true" async def handle_my_tool(arguments: dict) -> CreateTaskResult: ctx = server.request_context ctx.experimental.validate_task_mode(TASK_REQUIRED) @@ -127,7 +127,7 @@ async def handle_my_tool(arguments: dict) -> CreateTaskResult: Keep clients informed of progress: -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Starting...") @@ -149,7 +149,7 @@ Tasks can request user input via elicitation. This transitions the task to `inpu Collect structured data from the user: -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Waiting for confirmation...") @@ -177,7 +177,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: Direct users to external URLs for OAuth, payments, or other out-of-band flows: -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Waiting for OAuth...") @@ -198,7 +198,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: Tasks can request LLM completions from the client: -```python +```python skip="true" from mcp.types import SamplingMessage, TextContent async def work(task: ServerTaskContext) -> CallToolResult: @@ -220,7 +220,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: Sampling supports additional parameters: -```python +```python skip="true" result = await task.create_message( messages=[...], max_tokens=500, @@ -235,7 +235,7 @@ result = await task.create_message( Check for cancellation in long-running work: -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: for i in range(1000): if task.is_cancelled: @@ -254,7 +254,7 @@ The SDK's default cancel handler updates the task status. Your work function sho For production, implement `TaskStore` with persistent storage: -```python +```python skip="true" from mcp.shared.experimental.tasks.store import TaskStore from mcp.types import Task, TaskMetadata, Result @@ -287,7 +287,7 @@ class RedisTaskStore(TaskStore): Use your custom store: -```python +```python skip="true" store = RedisTaskStore(redis_client) server.experimental.enable_tasks(store=store) ``` @@ -296,7 +296,7 @@ server.experimental.enable_tasks(store=store) A server with multiple task-supporting tools: -```python +```python skip="true" from mcp.server import Server from mcp.server.experimental.task_context import ServerTaskContext from mcp.types import ( @@ -382,7 +382,7 @@ async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTask Tasks handle errors automatically, but you can also fail explicitly: -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: try: result = await risky_operation() @@ -407,7 +407,7 @@ For custom error messages, call `task.fail()` before raising. For web applications, use the Streamable HTTP transport: -```python +```python skip="true" from collections.abc import AsyncIterator from contextlib import asynccontextmanager @@ -484,7 +484,7 @@ if __name__ == "__main__": Test task functionality with the SDK's testing utilities: -```python +```python skip="true" import pytest import anyio from mcp.client.session import ClientSession @@ -530,7 +530,7 @@ async def test_task_tool(): ### Keep Work Functions Focused -```python +```python skip="true" # Good: focused work function async def work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Validating...") @@ -544,7 +544,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: ### Check Cancellation in Loops -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: results = [] for item in large_dataset: @@ -558,7 +558,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: ### Use Meaningful Status Messages -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: await task.update_status("Connecting to database...") db = await connect() @@ -575,7 +575,7 @@ async def work(task: ServerTaskContext) -> CallToolResult: ### Handle Elicitation Responses -```python +```python skip="true" async def work(task: ServerTaskContext) -> CallToolResult: result = await task.elicit(message="Continue?", requestedSchema={...}) diff --git a/docs/experimental/tasks.md b/docs/experimental/tasks.md index 2d4d06a02..9fdda3bed 100644 --- a/docs/experimental/tasks.md +++ b/docs/experimental/tasks.md @@ -101,7 +101,7 @@ Server Client When augmenting a request with task execution, include `TaskMetadata`: -```python +```python skip="true" from mcp.types import TaskMetadata task = TaskMetadata(ttl=60000) # TTL in milliseconds @@ -113,7 +113,7 @@ The `ttl` (time-to-live) specifies how long the task and result are retained aft Servers persist task state in a `TaskStore`. The SDK provides `InMemoryTaskStore` for development: -```python +```python skip="true" from mcp.shared.experimental.tasks import InMemoryTaskStore store = InMemoryTaskStore() @@ -140,7 +140,7 @@ The SDK manages these automatically when you enable task support. **Server** (simplified API): -```python +```python skip="true" from mcp.server import Server from mcp.server.experimental.task_context import ServerTaskContext from mcp.types import CallToolResult, TextContent, TASK_REQUIRED @@ -163,7 +163,7 @@ async def handle_tool(name: str, arguments: dict): **Client:** -```python +```python skip="true" from mcp.client.session import ClientSession from mcp.types import CallToolResult diff --git a/docs/index.md b/docs/index.md index e096d910b..48689222d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,67 +1,48 @@ # MCP Python SDK -The **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. +The **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, +separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to: - **Build MCP servers** that expose resources, prompts, and tools -- **Create MCP clients** that can connect to any MCP server +- **Create MCP clients** that connect to any MCP server - **Use standard transports** like stdio, SSE, and Streamable HTTP -If you want to read more about the specification, please visit the [MCP documentation](https://modelcontextprotocol.io). +## Quick example -## Quick Example +A minimal MCP server with a single tool: -Here's a simple MCP server that exposes a tool, resource, and prompt: - -```python title="server.py" +```python from mcp.server.mcpserver import MCPServer -mcp = MCPServer("Test Server", json_response=True) - +mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: - """Add two numbers""" + """Add two numbers.""" return a + b - -@mcp.resource("greeting://{name}") -def get_greeting(name: str) -> str: - """Get a personalized greeting""" - return f"Hello, {name}!" - - -@mcp.prompt() -def greet_user(name: str, style: str = "friendly") -> str: - """Generate a greeting prompt""" - return f"Write a {style} greeting for someone named {name}." - - if __name__ == "__main__": mcp.run(transport="streamable-http") ``` -Run the server: - ```bash uv run --with mcp server.py ``` -Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) and connect to `http://localhost:8000/mcp`: +## Getting started -```bash -npx -y @modelcontextprotocol/inspector -``` +Follow these steps to start building with MCP: -## Getting Started +1. **[Install](installation.md)** the SDK +2. **[Quickstart](quickstart.md)** — build your first MCP server +3. **[Concepts](concepts.md)** — understand the protocol architecture and primitives - -1. **[Install](installation.md)** the MCP SDK -2. **[Learn concepts](concepts.md)** - understand the three primitives and architecture -3. **[Explore authorization](authorization.md)** - add security to your servers -4. **[Use low-level APIs](low-level-server.md)** - for advanced customization +## Links -## API Reference +Useful references for working with MCP: -Full API documentation is available in the [API Reference](api.md). +- [MCP specification](https://modelcontextprotocol.io) +- [API Reference](api.md) +- [Migration guide](migration.md) (v1 → v2) diff --git a/docs/low-level-server.md b/docs/low-level-server.md deleted file mode 100644 index a5b4f3df3..000000000 --- a/docs/low-level-server.md +++ /dev/null @@ -1,5 +0,0 @@ -# Low-Level Server - -!!! warning "Under Construction" - - This page is currently being written. Check back soon for complete documentation. diff --git a/docs/migration.md b/docs/migration.md index 7d30f0ac9..91bbc8bec 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -14,7 +14,7 @@ The deprecated `streamablehttp_client` function has been removed. Use `streamabl **Before (v1):** -```python +```python skip="true" from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client( @@ -29,7 +29,7 @@ async with streamablehttp_client( **After (v2):** -```python +```python skip="true" import httpx from mcp.client.streamable_http import streamable_http_client @@ -56,7 +56,7 @@ If you need to capture the session ID (e.g., for session resumption testing), yo **Before (v1):** -```python +```python skip="true" from mcp.client.streamable_http import streamable_http_client async with streamable_http_client(url) as (read_stream, write_stream, get_session_id): @@ -67,7 +67,7 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio **After (v2):** -```python +```python skip="true" import httpx from mcp.client.streamable_http import streamable_http_client @@ -115,13 +115,13 @@ The following deprecated type aliases and classes have been removed from `mcp.ty **Before (v1):** -```python +```python skip="true" from mcp.types import Content, ResourceReference, Cursor ``` **After (v2):** -```python +```python skip="true" from mcp.types import ContentBlock, ResourceTemplateReference # Use `str` instead of `Cursor` for pagination cursors ``` @@ -132,13 +132,13 @@ The deprecated `args` parameter has been removed from `ClientSessionGroup.call_t **Before (v1):** -```python +```python skip="true" result = await session_group.call_tool("my_tool", args={"key": "value"}) ``` **After (v2):** -```python +```python skip="true" result = await session_group.call_tool("my_tool", arguments={"key": "value"}) ``` @@ -155,14 +155,14 @@ Use `params=PaginatedRequestParams(cursor=...)` instead. **Before (v1):** -```python +```python skip="true" result = await session.list_resources(cursor="next_page_token") result = await session.list_tools(cursor="next_page_token") ``` **After (v2):** -```python +```python skip="true" from mcp.types import PaginatedRequestParams result = await session.list_resources(params=PaginatedRequestParams(cursor="next_page_token")) @@ -175,7 +175,7 @@ The `McpError` exception class has been renamed to `MCPError` for consistent nam **Before (v1):** -```python +```python skip="true" from mcp.shared.exceptions import McpError try: @@ -186,7 +186,7 @@ except McpError as e: **After (v2):** -```python +```python skip="true" from mcp.shared.exceptions import MCPError try: @@ -197,7 +197,7 @@ except MCPError as e: `MCPError` is also exported from the top-level `mcp` package: -```python +```python skip="true" from mcp import MCPError ``` @@ -207,7 +207,7 @@ The `FastMCP` class has been renamed to `MCPServer` to better reflect its role a **Before (v1):** -```python +```python skip="true" from mcp.server.fastmcp import FastMCP mcp = FastMCP("Demo") @@ -215,7 +215,7 @@ mcp = FastMCP("Demo") **After (v2):** -```python +```python skip="true" from mcp.server.mcpserver import MCPServer mcp = MCPServer("Demo") @@ -242,7 +242,7 @@ Transport-specific parameters have been moved from the `MCPServer` constructor t **Before (v1):** -```python +```python skip="true" from mcp.server.fastmcp import FastMCP # Transport params in constructor @@ -256,7 +256,7 @@ mcp.run(transport="sse") **After (v2):** -```python +```python skip="true" from mcp.server.mcpserver import MCPServer # Transport params passed to run() @@ -272,7 +272,7 @@ mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events") When mounting in a Starlette app, pass transport params to the app methods: -```python +```python skip="true" # Before (v1) from mcp.server.fastmcp import FastMCP @@ -304,7 +304,7 @@ This means you can no longer access `.root` on these types or use `model_validat **Before (v1):** -```python +```python skip="true" from mcp.types import ClientRequest, ServerNotification # Using RootModel.model_validate() @@ -317,7 +317,7 @@ actual_notification = notification.root **After (v2):** -```python +```python skip="true" from mcp.types import client_request_adapter, server_notification_adapter # Using TypeAdapter.validate_python() @@ -355,7 +355,7 @@ The nested `RequestParams.Meta` Pydantic model class has been replaced with a to **In request context handlers:** -```python +```python skip="true" # Before (v1) @server.call_tool() async def handle_tool(name: str, arguments: dict) -> list[TextContent]: @@ -386,7 +386,7 @@ The `RequestContext` class has been split to separate shared fields from server- **Before (v1):** -```python +```python skip="true" from mcp.client.session import ClientSession from mcp.shared.context import RequestContext, LifespanContextT, RequestT from mcp.shared.progress import ProgressContext @@ -400,7 +400,7 @@ progress_ctx: ProgressContext[SendRequestT, SendNotificationT, SendResultT, Rece **After (v2):** -```python +```python skip="true" from mcp.client.context import ClientRequestContext from mcp.client.session import ClientSession from mcp.server.context import ServerRequestContext, LifespanContextT, RequestT @@ -422,7 +422,7 @@ The `uri` field on resource-related types now uses `str` instead of Pydantic's ` **Before (v1):** -```python +```python skip="true" from pydantic import AnyUrl from mcp.types import Resource @@ -432,7 +432,7 @@ resource = Resource(name="test", uri=AnyUrl("users/me")) # Would fail validatio **After (v2):** -```python +```python skip="true" from mcp.types import Resource # Plain strings accepted @@ -443,7 +443,7 @@ resource = Resource(name="test", uri="https://example.com") # Works If your code passes `AnyUrl` objects to URI fields, convert them to strings: -```python +```python skip="true" # If you have an AnyUrl from elsewhere uri = str(my_any_url) # Convert to string ``` @@ -459,7 +459,7 @@ Affected types: The `Client` and `ClientSession` methods `read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` now only accept `str` for the `uri` parameter. If you were passing `AnyUrl` objects, convert them to strings: -```python +```python skip="true" # Before (v1) from pydantic import AnyUrl @@ -481,7 +481,7 @@ await client.read_resource(str(my_any_url)) MCP protocol types no longer accept arbitrary extra fields at the top level. This matches the MCP specification which only allows extra fields within `_meta` objects, not on the types themselves. -```python +```python skip="true" # This will now raise a validation error from mcp.types import CallToolRequestParams @@ -505,7 +505,7 @@ params = CallToolRequestParams( The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper. -```python +```python skip="true" from mcp.server.lowlevel.server import Server server = Server("my-server") diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 000000000..98d1a8049 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,89 @@ +# Quickstart + +This guide will get you up and running with a simple MCP server in minutes. + +## Prerequisites + +You'll need Python 3.10+ and [uv](https://docs.astral.sh/uv/) (recommended) or pip. + +## Create a server + +Create a file called `server.py` with a tool, a resource, and a prompt: + +```python +from mcp.server.mcpserver import MCPServer + +# Create an MCP server +mcp = MCPServer("Demo") + + +# Add an addition tool +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +# Add a dynamic greeting resource +@mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting.""" + return f"Hello, {name}!" + + +# Add a prompt +@mcp.prompt() +def greet_user(name: str, style: str = "friendly") -> str: + """Generate a greeting prompt.""" + styles = { + "friendly": "Please write a warm, friendly greeting", + "formal": "Please write a formal, professional greeting", + "casual": "Please write a casual, relaxed greeting", + } + + return f"{styles.get(style, styles['friendly'])} for someone named {name}." + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") +``` + +## Run the server + +Start the server with uv: + +```bash +uv run --with mcp server.py +``` + +The server starts on `http://localhost:8000/mcp` using Streamable HTTP transport. + +## Connect a client + +You can connect to your running server using any MCP client: + +=== "Claude Code" + + Add the server to [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp): + + ```bash + claude mcp add --transport http my-server http://localhost:8000/mcp + ``` + +=== "MCP Inspector" + + Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to explore your server interactively: + + ```bash + npx -y @modelcontextprotocol/inspector + ``` + + In the inspector UI, connect to `http://localhost:8000/mcp`. + +## Next steps + +Now that you have a running server, explore these topics: + +- **[Concepts](concepts.md)** — understand the protocol architecture and primitives +- **[Testing](testing.md)** — test your server with the `Client` class +- **[API Reference](api.md)** — full API documentation diff --git a/docs/testing.md b/docs/testing.md index 9a222c906..1489908c2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -7,7 +7,7 @@ This makes it easy to write tests without network overhead. Let's assume you have a simple server with a single tool: -```python title="server.py" +```python title="server.py" skip-run="true" from mcp.server import MCPServer app = MCPServer("Calculator") @@ -40,7 +40,7 @@ To run the below test, you'll need to install the following dependencies: you to take snapshots of the output of your tests. Which makes it easier to create tests for your server - you don't need to use it, but we are spreading the word for best practices. -```python title="test_server.py" +```python title="test_server.py" skip-run="true" import pytest from inline_snapshot import snapshot from mcp import Client diff --git a/mkdocs.yml b/mkdocs.yml index 3019f5214..8cf0db529 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,12 +13,11 @@ site_url: https://modelcontextprotocol.github.io/python-sdk nav: - Introduction: index.md - Installation: installation.md + - Quickstart: quickstart.md + - Concepts: concepts.md + - Authorization: authorization.md + - Testing: testing.md - Migration Guide: migration.md - - Documentation: - - Concepts: concepts.md - - Low-Level Server: low-level-server.md - - Authorization: authorization.md - - Testing: testing.md - Experimental: - Overview: experimental/index.md - Tasks: diff --git a/tests/test_examples.py b/tests/test_examples.py index aa9de0957..e58ddc822 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -7,6 +7,7 @@ import sys from pathlib import Path +from typing import Any import pytest from inline_snapshot import snapshot @@ -96,16 +97,47 @@ async def test_desktop(monkeypatch: pytest.MonkeyPatch): assert "/fake/path/file2.txt" in content.text -# TODO(v2): Change back to README.md when v2 is released -@pytest.mark.parametrize("example", find_examples("README.v2.md"), ids=str) -def test_docs_examples(example: CodeExample, eval_example: EvalExample): - ruff_ignore: list[str] = ["F841", "I001", "F821"] # F821: undefined names (snippets lack imports) +SKIP_RUN_TAGS = ["skip", "skip-run"] +SKIP_LINT_TAGS = ["skip", "skip-lint"] + +# TODO(v2): Change "README.v2.md" back to "README.md" when v2 is released +_ALL_EXAMPLES = list(find_examples("README.v2.md", "docs/")) + - # Use project's actual line length of 120 - eval_example.set_config(ruff_ignore=ruff_ignore, target_version="py310", line_length=120) +def _set_eval_config(eval_example: EvalExample) -> None: + eval_example.set_config( + ruff_ignore=["F841", "I001", "F821"], + target_version="py310", + line_length=120, + ) + + +@pytest.mark.parametrize( + "example", + [ex for ex in _ALL_EXAMPLES if not any(ex.prefix_settings().get(key) == "true" for key in SKIP_LINT_TAGS)], + ids=str, +) +def test_docs_examples(example: CodeExample, eval_example: EvalExample): + _set_eval_config(eval_example) - # Use Ruff for both formatting and linting (skip Black) if eval_example.update_examples: # pragma: no cover eval_example.format_ruff(example) else: eval_example.lint_ruff(example) + + +@pytest.mark.parametrize( + "example", + [ex for ex in _ALL_EXAMPLES if not any(ex.prefix_settings().get(key) == "true" for key in SKIP_RUN_TAGS)], + ids=str, +) +def test_docs_examples_run(example: CodeExample, eval_example: EvalExample): + _set_eval_config(eval_example) + + # Prevent `if __name__ == "__main__"` blocks from starting servers + module_globals: dict[str, Any] = {"__name__": "__docs_test__"} + + if eval_example.update_examples: # pragma: no cover + eval_example.run_print_update(example, module_globals=module_globals) + else: + eval_example.run_print_check(example, module_globals=module_globals)