diff --git a/CLAUDE.md b/CLAUDE.md index 186a040cc..c18ead0a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ This document contains critical information about working with this codebase. Fo 2. Code Quality - Type hints required for all code - - Public APIs must have docstrings + - All public members MUST have Google Python Style Guide-compliant docstrings - Functions must be focused and small - Follow existing patterns exactly - Line length: 120 chars maximum @@ -24,6 +24,10 @@ This document contains critical information about working with this codebase. Fo - Coverage: test edge cases and errors - New features require tests - Bug fixes require regression tests + - Documentation + - Test changes in docs/ and Python docstrings: `uv run mkdocs build` + - On macOS: `export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib & uv run mkdocs build` + - Fix WARNING and ERROR issues and re-run build until clean - For commits fixing bugs or adding features based on user reports add: @@ -132,3 +136,46 @@ This document contains critical information about working with this codebase. Fo - **Only catch `Exception` for**: - Top-level handlers that must not crash - Cleanup blocks (log at debug level) + +## Docstring best practices for SDK documentation + +The following guidance ensures docstrings are genuinely helpful for new SDK users by providing navigation, context, and accurate examples. + +### Structure and formatting + +- Follow Google Python Style Guide for docstrings +- Format docstrings in Markdown compatible with mkdocs-material and mkdocstrings +- Always surround lists with blank lines (before and after) - also applies to Markdown (.md) files +- Always surround headings with blank lines - also applies to Markdown (.md) files +- Always surround fenced code blocks with blank lines - also applies to Markdown (.md) files +- Use sentence case for all headings and heading-like text - also applies to Markdown (.md) files + +### Content requirements + +- Access patterns: Explicitly state how users typically access the method/class with phrases like "You typically access this +method through..." or "You typically call this method by..." +- Cross-references: Use extensive cross-references to related members to help SDK users navigate: + - Format: [`displayed_text`][module.path.to.Member] + - Include backticks around the displayed text + - Link to types, related methods, and alternative approaches +- Parameter descriptions: + - Document all valid values for enums/literals + - Explain what each parameter does and when to use it + - Cross-reference parameter types where helpful +- Real-world examples: + - Show actual usage patterns from the SDK, not theoretical code + - Include imports and proper module paths + - Verify examples against source code for accuracy + - Show multiple approaches (e.g., low-level SDK vs FastMCP) + - Add comments explaining what's happening + - Examples should be concise and only as complex as needed to clearly demonstrate real-world usage +- Context and purpose: + - Explain not just what the method does, but why and when to use it + - Include notes about important considerations (e.g., client filtering, performance) + - Mention alternative approaches where applicable + +### Verification + + - All code examples MUST be 100% accurate to the actual SDK implementation + - Verify imports, class names, method signatures against source code + - You MUST NOT rely on existing documentation as authoritative - you MUST check the source diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c18937f5b..a29f3faaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,15 +10,15 @@ Thank you for your interest in contributing to the MCP Python SDK! This document 4. Clone your fork: `git clone https://github.com/YOUR-USERNAME/python-sdk.git` 5. Install dependencies: -```bash -uv sync --frozen --all-extras --dev -``` + ```bash + uv sync --frozen --all-extras --dev + ``` 6. Set up pre-commit hooks: -```bash -uv tool install pre-commit --with pre-commit-uv --force-reinstall -``` + ```bash + uv tool install pre-commit --with pre-commit-uv --force-reinstall + ``` ## Development Workflow @@ -33,37 +33,64 @@ uv tool install pre-commit --with pre-commit-uv --force-reinstall 4. Ensure tests pass: -```bash -uv run pytest -``` + ```bash + uv run pytest + ``` 5. Run type checking: -```bash -uv run pyright -``` + ```bash + uv run pyright + ``` 6. Run linting: -```bash -uv run ruff check . -uv run ruff format . -``` + ```bash + uv run ruff check . + uv run ruff format . + ``` 7. Update README snippets if you modified example code: -```bash -uv run scripts/update_readme_snippets.py -``` + ```bash + uv run scripts/update_readme_snippets.py + ``` 8. (Optional) Run pre-commit hooks on all files: -```bash -pre-commit run --all-files -``` + ```bash + pre-commit run --all-files + ``` 9. Submit a pull request to the same branch you branched from +## Building and viewing documentation + +To build and view the documentation locally: + +1. Install documentation dependencies (included with `--dev` flag above): + + ```bash + uv sync --frozen --group docs + ``` + +2. Serve the documentation locally: + + ```bash + uv run mkdocs serve + ``` + + **Note for macOS users**: If you encounter a [Cairo library error](https://squidfunk.github.io/mkdocs-material/plugins/requirements/image-processing/#cairo-library-was-not-found), set the library path before running mkdocs: + + ```bash + export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib + uv run mkdocs serve + ``` + +3. Open your browser to `http://127.0.0.1:8000/python-sdk/` to view the documentation + +The documentation will auto-reload when you make changes to files in `docs/`, `mkdocs.yml`, or `src/mcp/`. + ## Code Style - We use `ruff` for linting and formatting diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 3f696af54..000000000 --- a/docs/api.md +++ /dev/null @@ -1 +0,0 @@ -::: mcp diff --git a/docs/examples-authentication.md b/docs/examples-authentication.md new file mode 100644 index 000000000..509d3d7cf --- /dev/null +++ b/docs/examples-authentication.md @@ -0,0 +1,144 @@ +# Authentication examples + +MCP supports OAuth 2.1 authentication for protecting server resources. This section demonstrates both server-side token verification and client-side authentication flows. + +## Security considerations + +When implementing authentication: + +- **Use HTTPS**: All OAuth flows must use HTTPS in production +- **Token validation**: Always validate tokens on the resource server side +- **Scope checking**: Verify that tokens have required scopes +- **Introspection**: Use token introspection for distributed validation +- **RFC compliance**: Follow RFC 9728 for proper authoriazation server (AS) discovery + +## OAuth architecture + +The MCP OAuth implementation follows the OAuth 2.1 authorization code flow with token introspection: + +```mermaid +sequenceDiagram + participant C as Client + participant AS as Authorization Server + participant RS as Resource Server
(MCP Server) + participant U as User + + Note over C,RS: 1. Discovery Phase (RFC 9728) + C->>RS: GET /.well-known/oauth-protected-resource + RS->>C: Protected Resource Metadata
(issuer, scopes, etc.) + + Note over C,AS: 2. Authorization Phase + C->>AS: GET /authorize?response_type=code&client_id=... + AS->>U: Redirect to login/consent + U->>AS: User authenticates and consents + AS->>C: Authorization code (via redirect) + + Note over C,AS: 3. Token Exchange + C->>AS: POST /token
(authorization_code grant) + AS->>C: Access token + refresh token + + Note over C,RS: 4. Resource Access + C->>RS: MCP request + Authorization: Bearer + RS->>AS: POST /introspect
(validate token) + AS->>RS: Token info (active, scopes, user) + RS->>C: MCP response (if authorized) + + Note over C,AS: 5. Token Refresh (when needed) + C->>AS: POST /token
(refresh_token grant) + AS->>C: New access token +``` + +**Components:** + +- **Authorization Server (AS)**: Handles OAuth flows, issues and validates tokens +- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources +- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with MCP server +- **User**: Resource owner who authorizes access + +## OAuth server implementation + +FastMCP server with OAuth token verification: + +```python +--8<-- "examples/snippets/servers/oauth_server.py" +``` + +This example shows: + +- Implementing the `TokenVerifier` protocol for token validation +- Using `AuthSettings` for RFC 9728 Protected Resource Metadata +- Resource server configuration with authorization server discovery +- Protected tools that require authentication + +## Complete authentication server + +Full Authorization Server implementation with token introspection: + +```python +--8<-- "examples/servers/simple-auth/mcp_simple_auth/auth_server.py" +``` + +This comprehensive example includes: + +- OAuth 2.1 authorization flows (authorization code, refresh token) +- Token introspection endpoint for resource servers +- Client registration and metadata management +- RFC 9728 protected resource metadata endpoint + +## Resource server with introspection + +MCP Resource Server that validates tokens via Authorization Server introspection: + +```python +--8<-- "examples/servers/simple-auth/mcp_simple_auth/server.py" +``` + +This demonstrates: + +- Token introspection for validation instead of local token verification +- Separation of Authorization Server (AS) and Resource Server (RS) +- Protected MCP tools and resources +- Production-ready server patterns + +## Token verification implementation + +Custom token verification logic: + +```python +--8<-- "examples/servers/simple-auth/mcp_simple_auth/token_verifier.py" +``` + +This component handles: + +- HTTP token introspection requests +- Token validation with scope checking +- RFC 8707 resource parameter validation +- Error handling and logging + +## Simple authentication provider + +Authentication provider for development and testing: + +```python +--8<-- "examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py" +``` + +This utility provides: + +- Simplified token generation for testing +- Development authentication flows +- Testing utilities for protected resources + +## Legacy Authorization Server + +Backward compatibility with older OAuth implementations: + +```python +--8<-- "examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py" +``` + +This example shows: + +- Support for non-RFC 9728 compliant clients +- Legacy endpoint compatibility +- Migration patterns for existing systems diff --git a/docs/examples-clients.md b/docs/examples-clients.md new file mode 100644 index 000000000..5fa7a2992 --- /dev/null +++ b/docs/examples-clients.md @@ -0,0 +1,127 @@ +# Client examples + +MCP clients connect to servers to access tools, resources, and prompts. This section demonstrates various client patterns and connection types. + +These examples provide comprehensive patterns for building MCP clients that can handle various server types, authentication methods, and interaction patterns. + +## Basic stdio client + +Connecting to MCP servers over stdio transport: + +```python +--8<-- "examples/snippets/clients/stdio_client.py" +``` + +This fundamental example demonstrates: + +- Creating `StdioServerParameters` for server connection +- Using `ClientSession` for MCP communication +- Listing and calling tools, reading resources, getting prompts +- Handling both structured and unstructured tool results +- Sampling callback implementation for LLM integration + +## Streamable HTTP client + +Connecting to HTTP-based MCP servers: + +```python +--8<-- "examples/snippets/clients/streamable_basic.py" +``` + +This example shows: + +- Using `streamablehttp_client` for HTTP connections +- Simpler connection setup for web-deployed servers +- Basic tool listing and execution over HTTP + +## Display utilities + +Helper utilities for client user interfaces: + +```python +--8<-- "examples/snippets/clients/display_utilities.py" +``` + +This practical example covers: + +- Using `get_display_name()` for human-readable names +- Proper precedence rules for tool/resource titles +- Building user-friendly client interfaces +- Consistent naming across different MCP objects + +## OAuth authentication client + +Client-side OAuth 2.1 authentication flow: + +```python +--8<-- "examples/snippets/clients/oauth_client.py" +``` + +This comprehensive example demonstrates: + +- `OAuthClientProvider` setup and configuration +- Token storage with custom `TokenStorage` implementation +- Authorization flow handling (redirect and callback) +- Authenticated requests to protected MCP servers + +## Completion client + +Using completion suggestions for better user experience: + +```python +--8<-- "examples/snippets/clients/completion_client.py" +``` + +This advanced example shows: + +- Resource template argument completion +- Context-aware completions (e.g., repository suggestions based on owner) +- Prompt argument completion +- Dynamic suggestion generation + +## Tool result parsing + +Understanding and processing tool results: + +```python +--8<-- "examples/snippets/clients/parsing_tool_results.py" +``` + +This detailed example covers: + +- Parsing different content types (`TextContent`, `ImageContent`, `EmbeddedResource`) +- Handling structured output data +- Processing embedded resources +- Error handling for failed tool executions + +## Complete chatbot client + +A full-featured chatbot that integrates with multiple MCP servers: + +```python +--8<-- "examples/clients/simple-chatbot/mcp_simple_chatbot/main.py" +``` + +This production-ready example includes: + +- **Multi-server management**: Connect to multiple MCP servers simultaneously +- **LLM integration**: Use Groq API for natural language processing +- **Tool orchestration**: Automatic tool selection and execution +- **Error handling**: Retry mechanisms and graceful failure handling +- **Configuration management**: JSON-based server configuration +- **Session management**: Persistent conversation context + +## Authentication client + +Complete OAuth client implementation: + +```python +--8<-- "examples/clients/simple-auth-client/mcp_simple_auth_client/main.py" +``` + +This example demonstrates: + +- Full OAuth 2.1 client implementation +- Token management and refresh +- Protected resource access +- Integration with authenticated MCP servers diff --git a/docs/examples-echo-servers.md b/docs/examples-echo-servers.md new file mode 100644 index 000000000..0da3d1a41 --- /dev/null +++ b/docs/examples-echo-servers.md @@ -0,0 +1,76 @@ +# Echo server examples + +Echo servers provide a foundation for understanding MCP patterns before building more complex functionality. + +Echo servers are useful for: + +- **Testing client connections**: Verify that your client can connect and call tools +- **Understanding MCP basics**: Learn the fundamental request/response patterns +- **Development and debugging**: Simple, predictable behavior for testing +- **Protocol verification**: Ensure transport layers work correctly + +The following servers are minimal examples that demonstrate basic MCP functionality by echoing input back to clients. + +## Simple echo server + +The most basic echo implementation: + +```python +--8<-- "examples/fastmcp/simple_echo.py" +``` + +This minimal example shows: + +- Single tool implementation with string input/output +- Basic parameter handling +- Simple string manipulation and return + +## Enhanced echo server + +More sophisticated echo patterns: + +```python +--8<-- "examples/fastmcp/echo.py" +``` + +This enhanced version demonstrates: + +- Multiple echo variants (basic echo, uppercase, reverse) +- Different parameter types and patterns +- Tool naming and description best practices + +## Usage + +These echo servers can be used to test different aspects of MCP: + +```bash +# Test with MCP Inspector +uv run mcp dev echo.py + +# Test direct execution +python echo.py + +# Test with custom clients +# (Use the client examples to connect to these echo servers) +``` + +## Testing tool calls + +Example tool calls you can make to echo servers: + +```json +{ + "tool": "echo", + "arguments": { + "message": "Hello, MCP!" + } +} +``` + +Expected response: + +```json +{ + "result": "Echo: Hello, MCP!" +} +``` diff --git a/docs/examples-lowlevel-servers.md b/docs/examples-lowlevel-servers.md new file mode 100644 index 000000000..e440cf2a4 --- /dev/null +++ b/docs/examples-lowlevel-servers.md @@ -0,0 +1,95 @@ +# Low-level server examples + +The [low-level server API](reference/mcp/server/lowlevel/server.md) provides maximum control over MCP protocol implementation. Use these patterns when you need fine-grained control or when [`FastMCP`][mcp.server.fastmcp.FastMCP] doesn't meet your requirements. + +The low-level API provides the foundation that FastMCP is built upon, giving you access to all MCP protocol features with complete control over implementation details. + +## When to use low-level API + +Choose the low-level API when you need: + +- Custom protocol message handling +- Complex initialization sequences +- Fine-grained control over capabilities +- Integration with existing server infrastructure +- Performance optimization at the protocol level +- Custom authentication or authorization logic + +Key differences between the low-level server API and FastMCP are: + +| | Low-level API | FastMCP | +| --------------- | ------------------------ | ----------------------------- | +| **Control** | Maximum control | Convention over configuration | +| **Boilerplate** | More verbose | Minimal setup | +| **Decorators** | Server method decorators | Simple function decorators | +| **Schema** | Manual definition | Automatic from type hints | +| **Lifecycle** | Manual management | Automatic handling | +| **Best for** | Complex custom logic | Rapid development | + +## Basic low-level server + +Fundamental low-level server patterns: + +```python +--8<-- "examples/snippets/servers/lowlevel/basic.py" +``` + +This example demonstrates: + +- Creating a [`Server`][mcp.server.lowlevel.Server] instance directly +- Manual handler registration with decorators +- Prompt management with `@server.list_prompts()` and `@server.get_prompt()` +- Manual capability declaration +- Explicit initialization and connection handling + +## Low-level server with lifespan + +Resource management and lifecycle control: + +```python +--8<-- "examples/snippets/servers/lowlevel/lifespan.py" +``` + +This advanced pattern shows: + +- Custom lifespan context manager for resource initialization +- Database connection management example +- Accessing lifespan context through `server.request_context` +- Tool implementation with resource access +- Proper cleanup and connection management + +## Structured output with low-level API + +Manual structured output control: + +```python +--8<-- "examples/snippets/servers/lowlevel/structured_output.py" +``` + +And a standalone implementation: + +```python +--8<-- "examples/servers/structured_output_lowlevel.py" +``` + +These examples cover: + +- Manual `outputSchema` definition in tool specifications +- Direct dictionary return for structured data +- Automatic validation against defined schemas +- Backward compatibility with text content + +## Simple tool server + +Complete low-level server focused on tools: + +```python +--8<-- "examples/servers/simple-tool/mcp_simple_tool/server.py" +``` + +This production-ready example includes: + +- Full tool lifecycle management +- Input validation and error handling +- Proper MCP protocol compliance +- Tool execution with structured responses diff --git a/docs/examples-quickstart.md b/docs/examples-quickstart.md new file mode 100644 index 000000000..20d76ae08 --- /dev/null +++ b/docs/examples-quickstart.md @@ -0,0 +1,52 @@ +# Getting started + +This section provides quick and simple examples to get you started with the MCP Python SDK. + +These examples can be run directly with: + +```bash +python server.py +``` + +Or test with the MCP Inspector: + +```bash +uv run mcp dev server.py +``` + +## FastMCP quickstart + +The easiest way to create an MCP server is with [`FastMCP`][mcp.server.fastmcp.FastMCP]. This example demonstrates the core concepts: tools, resources, and prompts. + +```python +--8<-- "examples/snippets/servers/fastmcp_quickstart.py" +``` + +This example shows how to: + +- Create a FastMCP server instance +- Add a tool that performs computation (`add`) +- Add a dynamic resource that provides data (`greeting://`) +- Add a prompt template for LLM interactions (`greet_user`) + +## Basic server + +An even simpler starting point: + +```python +--8<-- "examples/fastmcp/readme-quickstart.py" +``` + +## Direct execution + +For the simplest possible server deployment: + +```python +--8<-- "examples/snippets/servers/direct_execution.py" +``` + +This example demonstrates: + +- Minimal server setup with just a greeting tool +- Direct execution without additional configuration +- Entry point setup for standalone running diff --git a/docs/examples-server-advanced.md b/docs/examples-server-advanced.md new file mode 100644 index 000000000..5c7912c2d --- /dev/null +++ b/docs/examples-server-advanced.md @@ -0,0 +1,95 @@ +# Advanced server examples + +This section covers advanced server patterns including lifecycle management, context handling, and interactive capabilities. + +These advanced patterns enable rich, interactive server implementations that go beyond simple request-response workflows. + +## Lifespan management + +Managing server lifecycle with resource initialization and cleanup: + +```python +--8<-- "examples/snippets/servers/lifespan_example.py" +``` + +This example demonstrates: + +- Type-safe lifespan context management +- Resource initialization on startup (database connections, etc.) +- Automatic cleanup on shutdown +- Accessing lifespan context from tools via [`ctx.request_context.lifespan_context`][mcp.server.fastmcp.Context.request_context] + +## User interaction and elicitation + +Tools that can request additional information from users: + +```python +--8<-- "examples/snippets/servers/elicitation.py" +``` + +This example shows: + +- Using [`ctx.elicit()`][mcp.server.fastmcp.Context.elicit] to request user input +- Pydantic schemas for validating user responses +- Handling user acceptance, decline, or cancellation +- Interactive booking workflow patterns + +## LLM sampling and integration + +Tools that interact with LLMs through sampling: + +```python +--8<-- "examples/snippets/servers/sampling.py" +``` + +This demonstrates: + +- Using [`ctx.session.create_message()`][mcp.server.session.ServerSession.create_message] for LLM interaction +- Structured message creation with [`SamplingMessage`][mcp.types.SamplingMessage] and [`TextContent`][mcp.types.TextContent] +- Processing LLM responses within tools +- Chaining LLM interactions for complex workflows + +## Logging and notifications + +Advanced logging and client notification patterns: + +```python +--8<-- "examples/snippets/servers/notifications.py" +``` + +This example covers: + +- Multiple log levels (debug, info, warning, error) +- Resource change notifications via [`ctx.session.send_resource_list_changed()`][mcp.server.session.ServerSession.send_resource_list_changed] +- Contextual logging within tool execution +- Client communication patterns + +## Image handling + +Working with images in MCP servers: + +```python +--8<-- "examples/snippets/servers/images.py" +``` + +This shows: + +- Using FastMCP's [`Image`][mcp.server.fastmcp.Image] class for automatic image handling +- PIL integration for image processing with [`PIL.Image.open()`][PIL.Image.open] +- Returning images from tools +- Image format conversion and optimization + +## Completion support + +Providing argument completion for enhanced user experience: + +```python +--8<-- "examples/snippets/servers/completion.py" +``` + +This advanced pattern demonstrates: + +- Dynamic completion based on partial input +- Context-aware suggestions (repository suggestions based on owner) +- Resource template parameter completion +- Prompt argument completion diff --git a/docs/examples-server-prompts.md b/docs/examples-server-prompts.md new file mode 100644 index 000000000..b9d7f20e4 --- /dev/null +++ b/docs/examples-server-prompts.md @@ -0,0 +1,42 @@ +# Server prompts examples + +Prompts are reusable templates that help structure LLM interactions. They provide a way to define consistent interaction patterns that users can invoke. + +Prompts are user-controlled primitives and are particularly useful for: + +- Code review templates +- Debugging assistance workflows +- Content generation patterns +- Structured analysis requests + +Unlike tools (which are model-controlled) and resources (which are application-controlled), prompts are invoked directly by users to initiate specific types of interactions with the LLM. + +## Basic prompts + +Simple prompt templates for common scenarios: + +```python +--8<-- "examples/snippets/servers/basic_prompt.py" +``` + +This example demonstrates: + +- Simple string prompts (`review_code`) +- Multi-message prompt conversations (`debug_error`) +- Using different message types (User and Assistant messages) +- Prompt titles for better user experience + +## Simple prompt server + +A complete server focused on prompt management: + +```python +--8<-- "examples/servers/simple-prompt/mcp_simple_prompt/server.py" +``` + +This low-level server example shows: + +- Prompt listing and retrieval +- Argument handling and validation +- Dynamic prompt generation based on parameters +- Production-ready prompt patterns using the low-level API diff --git a/docs/examples-server-resources.md b/docs/examples-server-resources.md new file mode 100644 index 000000000..b3a5cfa33 --- /dev/null +++ b/docs/examples-server-resources.md @@ -0,0 +1,51 @@ +# Server resources examples + +Resources provide data to LLMs without side effects. They're similar to GET endpoints in REST APIs and should be used for exposing information rather than performing actions. + +Resources are essential for providing contextual information to LLMs, whether it's configuration data, file contents, or dynamic information that changes over time. + +## Basic resources + +Simple resource patterns for exposing data: + +```python +--8<-- "examples/snippets/servers/basic_resource.py" +``` + +This example demonstrates: + +- Static resources with fixed URIs (`config://settings`) +- Dynamic resources with URI templates (`file://documents/{name}`) +- Simple string data return +- JSON configuration data + +## Simple resource server + +A complete server focused on resource management: + +```python +--8<-- "examples/servers/simple-resource/mcp_simple_resource/server.py" +``` + +This is an example of a low-level server that: + +- Uses the low-level server API for maximum control +- Implements resource listing and reading +- Handles URI templates and parameter extraction +- Demonstrates production-ready resource patterns + + +## Memory and state management + +Resources that manage server memory and state: + +```python +--8<-- "examples/fastmcp/memory.py" +``` + +This example shows how to: + +- Implement persistent memory across requests +- Store and retrieve conversational context +- Handle memory cleanup and management +- Provide memory resources to LLMs diff --git a/docs/examples-server-tools.md b/docs/examples-server-tools.md new file mode 100644 index 000000000..7662f5b5e --- /dev/null +++ b/docs/examples-server-tools.md @@ -0,0 +1,74 @@ +# Server tools examples + +Tools are functions that LLMs can call to perform actions or computations. This section demonstrates various tool patterns and capabilities. + +## Basic tools + +Simple tools that perform computations and return results: + +```python +--8<-- "examples/snippets/servers/basic_tool.py" +``` + +## Tools with context and progress reporting + +Tools can access MCP context for logging, progress reporting, and other capabilities: + +```python +--8<-- "examples/snippets/servers/tool_progress.py" +``` + +This example shows: + +- Using the `Context` parameter for MCP capabilities +- Progress reporting during long-running operations +- Structured logging at different levels +- Async tool functions + +## Complex input handling + +Tools can handle complex data structures and validation: + +```python +--8<-- "examples/fastmcp/complex_inputs.py" +``` + +## Parameter descriptions + +Tools with detailed parameter descriptions and validation: + +```python +--8<-- "examples/fastmcp/parameter_descriptions.py" +``` + +## Unicode and internationalization + +Handling Unicode and international text in tools: + +```python +--8<-- "examples/fastmcp/unicode_example.py" +``` + +## Desktop integration + +Tools that interact with the desktop environment: + +```python +--8<-- "examples/fastmcp/desktop.py" +``` + +## Screenshot tools + +Tools for taking and processing screenshots: + +```python +--8<-- "examples/fastmcp/screenshot.py" +``` + +## Text messaging tool + +Tool to send a text message. + +```python +--8<-- "examples/fastmcp/text_me.py" +``` diff --git a/docs/examples-structured-output.md b/docs/examples-structured-output.md new file mode 100644 index 000000000..55bf27c10 --- /dev/null +++ b/docs/examples-structured-output.md @@ -0,0 +1,67 @@ +# Structured output examples + +Structured output allows tools to return well-typed, validated data that clients can easily process. This section covers various approaches to structured data. + +Structured output provides several advantages: + +- **Type Safety**: Automatic validation ensures data integrity +- **Documentation**: Schemas serve as API documentation +- **Client Integration**: Easier processing by client applications +- **Backward Compatibility**: Still provides unstructured text content +- **IDE Support**: Better development experience with type hints + +Choose structured output when you need reliable, processable data from your tools. + +## FastMCP structured output + +Using FastMCP's automatic structured output capabilities: + +```python +--8<-- "examples/snippets/servers/structured_output.py" +``` + +This comprehensive example demonstrates: + +- **Pydantic models**: Rich validation and documentation (`WeatherData`) +- **TypedDict**: Simpler structures (`LocationInfo`) +- **Dictionary types**: Flexible schemas (`dict[str, float]`) +- **Regular classes**: With type hints for structured output (`UserProfile`) +- **Untyped classes**: Fall back to unstructured output (`UntypedConfig`) +- **Primitive wrapping**: Simple types wrapped in `{"result": value}` + +## Weather service with structured output + +A complete weather service demonstrating real-world structured output patterns: + +```python +--8<-- "examples/fastmcp/weather_structured.py" +``` + +This extensive example shows: + +- **Nested Pydantic models**: Complex data structures with validation +- **Multiple output formats**: Different approaches for different use cases +- **Dataclass support**: Using dataclasses for structured output +- **Production patterns**: Realistic data structures for weather APIs +- **Testing integration**: Built-in testing via MCP protocol + +## Low-level structured output + +Using the low-level server API for maximum control: + +```python +--8<-- "examples/snippets/servers/lowlevel/structured_output.py" +``` + +And a standalone low-level example: + +```python +--8<-- "examples/servers/structured_output_lowlevel.py" +``` + +These examples demonstrate: + +- Manual schema definition with `outputSchema` +- Validation against defined schemas +- Returning structured data directly from tools +- Backward compatibility with unstructured content diff --git a/docs/examples-transport-http.md b/docs/examples-transport-http.md new file mode 100644 index 000000000..54e45640e --- /dev/null +++ b/docs/examples-transport-http.md @@ -0,0 +1,91 @@ +# HTTP transport examples + +HTTP transports enable web-based MCP server deployment with support for multiple clients and scalable architectures. + +Choose HTTP transports for production deployments that need to serve multiple clients or integrate with web infrastructure. + +## Transport comparison + +| Feature | Streamable HTTP | SSE | stdio | +| ---------------- | ------------------ | ----------------- | ---------------- | +| **Resumability** | ✅ With event store | ❌ | ❌ | +| **Scalability** | ✅ Multi-client | ✅ Multi-client | ❌ Single process | +| **State** | Configurable | Session-based | Process-based | +| **Deployment** | Web servers | Web servers | Local execution | +| **Best for** | Production APIs | Real-time updates | Development/CLI | + +## Streamable HTTP configuration + +Basic streamable HTTP server setup with different configurations: + +```python +--8<-- "examples/snippets/servers/streamable_config.py" +``` + +This example demonstrates: + +- **Stateful servers**: Maintain session state (default) +- **Stateless servers**: No session persistence (`stateless_http=True`) +- **JSON responses**: Disable SSE streaming (`json_response=True`) +- Transport selection at runtime + +## Mounting multiple servers + +Deploying multiple MCP servers in a single Starlette application: + +```python +--8<-- "examples/snippets/servers/streamable_starlette_mount.py" +``` + +This pattern shows: + +- Creating multiple FastMCP server instances +- Mounting servers at different paths (`/echo`, `/math`) +- Shared lifespan management across servers +- Combined session manager lifecycle + +## Stateful HTTP server + +Full low-level implementation of a stateful HTTP server: + +```python +--8<-- "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py" +``` + +This comprehensive example includes: + +- Event store for resumability (reconnection support) +- Progress notifications and logging +- Resource change notifications +- Streaming tool execution with progress updates +- Production-ready ASGI integration + +## Stateless HTTP server + +Low-level stateless server for high-scale deployments: + +```python +--8<-- "examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py" +``` + +Features of stateless design: + +- No session state persistence +- Simplified architecture for load balancing +- Better horizontal scaling capabilities +- Each request is independent + +## Event store implementation + +Supporting resumable connections with event storage: + +```python +--8<-- "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py" +``` + +This component enables: + +- Client reconnection with `Last-Event-ID` headers +- Event replay for missed messages +- Persistent streaming across connection interruptions +- Production-ready resumability patterns diff --git a/docs/index.md b/docs/index.md index 42ad9ca0c..885cf858d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,85 @@ -# MCP Server +# MCP Python SDK -This is the MCP Server implementation in Python. +A Python implementation of the Model Context Protocol (MCP) that enables applications to provide context for LLMs in a standardized way. -It only contains the [API Reference](api.md) for the time being. +## Examples + +The [Examples](examples-quickstart.md) section provides working code examples covering many aspects of MCP development. Each code example in these documents corresponds to an example .py file in the examples/ directory in the repository. + +- [Getting started](examples-quickstart.md): Quick introduction to FastMCP and basic server patterns +- [Server development](examples-server-tools.md): Tools, resources, prompts, and structured output examples +- [Transport protocols](examples-transport-http.md): HTTP and streamable transport implementations +- [Low-level servers](examples-lowlevel-servers.md): Advanced patterns using the low-level server API +- [Authentication](examples-authentication.md): OAuth 2.1 server and client implementations +- [Client development](examples-clients.md): Complete client examples with various connection types + +## API Reference + +Complete API documentation is auto-generated from the source code and available in the [API Reference](reference/mcp/index.md) section. + +## Code example index + +### Servers + +| File | Transport | Resources | Prompts | Tools | Completions | Sampling | Elicitation | Progress | Logging | Authentication | Configuration | +|---|---|---|---|---|---|---|---|---|---|---|---| +| [Complex input handling](examples-server-tools.md#complex-input-handling) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Desktop integration](examples-server-tools.md#desktop-integration) | stdio | ✅ | — | ✅ | — | — | — | — | — | — | — | +| [Enhanced echo server](examples-echo-servers.md#enhanced-echo-server) | stdio | ✅ | ✅ | ✅ | — | — | — | — | — | — | — | +| [Memory and state management](examples-server-resources.md#memory-and-state-management) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Parameter descriptions](examples-server-tools.md#parameter-descriptions) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Basic server](examples-quickstart.md#basic-server) | stdio | ✅ | — | ✅ | — | — | — | — | — | — | — | +| [Screenshot tools](examples-server-tools.md#screenshot-tools) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Simple echo server](examples-echo-servers.md#simple-echo-server) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Text messaging tool](examples-server-tools.md#text-messaging-tool) | stdio | — | — | ✅ | — | — | — | — | — | — | ✅ | +| [Unicode and internationalization](examples-server-tools.md#unicode-and-internationalization) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Weather service with structured output](examples-structured-output.md#weather-service-with-structured-output) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Complete authentication server](examples-authentication.md#complete-authentication-server) | streamable-http | — | — | — | — | — | — | — | — | ✅ | — | +| [Legacy Authorization Server](examples-authentication.md#legacy-authorization-server) | streamable-http | — | — | ✅ | — | — | — | — | — | ✅ | ✅ | +| [Resource server with introspection](examples-authentication.md#resource-server-with-introspection) | streamable-http | — | — | ✅ | — | — | — | — | — | ✅ | ✅ | +| [Simple prompt server](examples-server-prompts.md#simple-prompt-server) | stdio | — | ✅ | — | — | — | — | — | — | — | — | +| [Simple resource server](examples-server-resources.md#simple-resource-server) | stdio | ✅ | — | — | — | — | — | — | — | — | — | +| [Stateless HTTP server](examples-transport-http.md#stateless-http-server) | streamable-http | — | — | ✅ | — | — | — | — | ✅ | — | ✅ | +| [Stateful HTTP server](examples-transport-http.md#stateful-http-server) | streamable-http | — | — | ✅ | — | — | — | — | ✅ | — | ✅ | +| [Simple tool server](examples-lowlevel-servers.md#simple-tool-server) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Low-level structured output](examples-structured-output.md#low-level-structured-output) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Basic prompts](examples-server-prompts.md#basic-prompts) | stdio | — | ✅ | — | — | — | — | — | — | — | — | +| [Basic resources](examples-server-resources.md#basic-resources) | stdio | ✅ | — | — | — | — | — | — | — | — | ✅ | +| [Basic tools](examples-server-tools.md#basic-tools) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Completion support](examples-server-advanced.md#completion-support) | stdio | ✅ | ✅ | — | ✅ | — | — | — | — | — | — | +| [Direct execution](examples-quickstart.md#direct-execution) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [User interaction and elicitation](examples-server-advanced.md#user-interaction-and-elicitation) | stdio | — | — | ✅ | — | — | ✅ | — | — | — | — | +| [FastMCP quickstart](examples-quickstart.md#fastmcp-quickstart) | stdio | ✅ | ✅ | ✅ | — | — | — | — | — | — | — | +| [Image handling](examples-server-advanced.md#image-handling) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Lifespan management](examples-server-advanced.md#lifespan-management) | stdio | — | — | ✅ | — | — | — | — | — | — | ✅ | +| [Basic low-level server](examples-lowlevel-servers.md#basic-low-level-server) | stdio | — | ✅ | — | — | — | — | — | — | — | — | +| [Low-level server with lifespan](examples-lowlevel-servers.md#low-level-server-with-lifespan) | stdio | — | — | ✅ | — | — | — | — | — | — | ✅ | +| [Low-level structured output](examples-structured-output.md#low-level-structured-output) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Logging and notifications](examples-server-advanced.md#logging-and-notifications) | stdio | — | — | ✅ | — | — | — | — | ✅ | — | — | +| [OAuth server implementation](examples-authentication.md#oauth-server-implementation) | streamable-http | — | — | ✅ | — | — | — | — | — | ✅ | — | +| [LLM sampling and integration](examples-server-advanced.md#llm-sampling-and-integration) | stdio | — | — | ✅ | — | ✅ | — | — | — | — | — | +| [Streamable HTTP configuration](examples-transport-http.md#streamable-http-configuration) | streamable-http | — | — | ✅ | — | — | — | — | — | — | ✅ | +| [Mounting multiple servers](examples-transport-http.md#mounting-multiple-servers) | streamable-http | — | — | ✅ | — | — | — | — | — | — | ✅ | +| [FastMCP structured output](examples-structured-output.md#fastmcp-structured-output) | stdio | — | — | ✅ | — | — | — | — | — | — | — | +| [Tools with context and progress reporting](examples-server-tools.md#tools-with-context-and-progress-reporting) | stdio | — | — | ✅ | — | — | — | ✅ | ✅ | — | — | + +### Clients + +| File | Transport | Resources | Prompts | Tools | Completions | Sampling | Authentication | +|---|---|---|---|---|---|---|---| +| [Authentication client](examples-clients.md#authentication-client) | streamable-http | — | — | ✅ | — | — | ✅ | +| [Complete chatbot client](examples-clients.md#complete-chatbot-client) | stdio | — | — | ✅ | — | — | — | +| [Completion client](examples-clients.md#completion-client) | stdio | ✅ | ✅ | — | ✅ | — | — | +| [Display utilities](examples-clients.md#display-utilities) | stdio | ✅ | — | ✅ | — | — | — | +| [OAuth authentication client](examples-clients.md#oauth-authentication-client) | streamable-http | ✅ | — | ✅ | — | — | ✅ | +| [Tool result parsing](examples-clients.md#tool-result-parsing) | stdio | — | — | ✅ | — | — | — | +| [Basic stdio client](examples-clients.md#basic-stdio-client) | stdio | ✅ | ✅ | ✅ | — | ✅ | — | +| [Streamable HTTP client](examples-clients.md#streamable-http-client) | streamable-http | — | — | ✅ | — | — | — | + +Notes: + +- **Resources** for clients indicates the example uses the Resources API (reading resources or listing resource templates). +- **Completions** refers to the completion/complete API for argument autocompletion. +- **Sampling** indicates the example exercises the sampling/createMessage flow (server-initiated in server examples; client-provided callback in stdio_client). +- **Authentication** indicates OAuth support is implemented in the example. +- Em dash (—) indicates **not demonstrated** in the example. diff --git a/mkdocs.yml b/mkdocs.yml index b907cb873..7d03fea64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,18 +1,31 @@ -site_name: MCP Server -site_description: MCP Server -strict: true +site_name: MCP Python SDK +site_description: Python implementation of the Model Context Protocol (MCP) +strict: false -repo_name: modelcontextprotocol/python-sdk -repo_url: https://github.com/modelcontextprotocol/python-sdk +repo_name: mmacy/python-sdk +repo_url: https://github.com/mmacy/python-sdk edit_uri: edit/main/docs/ -site_url: https://modelcontextprotocol.github.io/python-sdk +site_url: https://mmacy.github.io/python-sdk # TODO(Marcelo): Add Anthropic copyright? # copyright: © Model Context Protocol 2025 to present nav: - Home: index.md - - API Reference: api.md + - Code examples: + - Getting started: examples-quickstart.md + - Echo servers: examples-echo-servers.md + - Server development: + - Tools: examples-server-tools.md + - Resources: examples-server-resources.md + - Prompts: examples-server-prompts.md + - Structured output: examples-structured-output.md + - Advanced patterns: examples-server-advanced.md + - Transport protocols: + - HTTP transport: examples-transport-http.md + - Low-level servers: examples-lowlevel-servers.md + - Authentication: examples-authentication.md + - Client development: examples-clients.md theme: name: "material" @@ -99,22 +112,24 @@ watch: plugins: - search - - social + # - social # Disabled due to Cairo dependency issues - glightbox - mkdocstrings: handlers: python: paths: [src/mcp] options: + group_by_category: false + # members_order: source relative_crossrefs: true - members_order: source separate_signature: true show_signature_annotations: true + show_source: false signature_crossrefs: true - group_by_category: false - # 3 because docs are in pages with an H2 just above them - heading_level: 3 import: - url: https://docs.python.org/3/objects.inv - url: https://docs.pydantic.dev/latest/objects.inv - url: https://typing-extensions.readthedocs.io/en/latest/objects.inv + - url: https://pillow.readthedocs.io/en/stable/objects.inv + - api-autonav: + modules: ["src/mcp"] diff --git a/pyproject.toml b/pyproject.toml index 9b84c5815..73c6f6148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ dev = [ ] docs = [ "mkdocs>=1.6.1", + "mkdocs-api-autonav>=0.3.1", "mkdocs-glightbox>=0.4.0", "mkdocs-material[imaging]>=9.5.45", "mkdocstrings-python>=1.12.2", diff --git a/src/mcp/__init__.py b/src/mcp/__init__.py index e93b95c90..8de2e4e47 100644 --- a/src/mcp/__init__.py +++ b/src/mcp/__init__.py @@ -1,3 +1,47 @@ +"""An implementation of the [Model Context Protocol (MCP) specification](https://modelcontextprotocol.io/specification/latest) in Python. + +Use the MCP Python SDK to: + +- Build MCP clients that can connect to any MCP server +- Build MCP servers that expose resources, prompts, and tools +- Use standard transports like stdio, SSE, and Streamable HTTP +- Handle MCP protocol messages and lifecycle events + +## Example - create a [`FastMCP`][mcp.server.fastmcp.FastMCP] server + +```python +from mcp.server.fastmcp import FastMCP + +# Create an MCP server +mcp = FastMCP("Demo") + +@mcp.tool() +def add(a: int, b: int) -> int: + \"\"\"Add two numbers\"\"\" + return a + b + +if __name__ == "__main__": + mcp.run() +``` + +## Example - create a client + +```python +from mcp import ClientSession, StdioServerParameters, stdio_client + +server_params = StdioServerParameters( + command="python", args=["server.py"] +) + +async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + tools = await session.list_tools() + result = await session.call_tool("add", {"a": 5, "b": 3}) +``` + +""" + from .client.session import ClientSession from .client.session_group import ClientSessionGroup from .client.stdio import StdioServerParameters, stdio_client diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 1853ce7c1..4f0d0bbec 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -107,6 +107,46 @@ class ClientSession( types.ServerNotification, ] ): + """A client session for communicating with an MCP server. + + This class provides a high-level interface for MCP client operations, including + tool calling, resource management, prompt handling, and protocol initialization. + It manages the bidirectional communication channel with an MCP server and handles + protocol-level concerns like message validation and capability negotiation. + + The session supports various MCP capabilities: + + - Tool execution with structured output validation + - Resource access and subscription management + - Prompt template retrieval and completion + - Progress notifications and logging + - Custom sampling, elicitation, and root listing callbacks + + Args: + read_stream: Stream for receiving messages from the server. + write_stream: Stream for sending messages to the server. + read_timeout_seconds: Optional timeout for read operations. + sampling_callback: Optional callback for handling sampling requests from the server. + elicitation_callback: Optional callback for handling elicitation requests from the server. + list_roots_callback: Optional callback for handling root listing requests from the server. + logging_callback: Optional callback for handling log messages from the server. + message_handler: Optional custom handler for incoming messages and exceptions. + client_info: Optional client implementation information. + + Example: + ```python + async with create_client_session() as session: + # Initialize the session + await session.initialize() + + # List available tools + tools = await session.list_tools() + + # Call a tool + result = await session.call_tool("my_tool", {"arg": "value"}) + ``` + """ + def __init__( self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], @@ -135,6 +175,17 @@ def __init__( self._tool_output_schemas: dict[str, dict[str, Any] | None] = {} async def initialize(self) -> types.InitializeResult: + """Initialize the MCP session with the server. + + Sends an initialization request to establish capabilities and protocol version. + This must be called before any other operations can be performed. + + Returns: + Server's initialization response containing capabilities and metadata + + Raises: + McpError: If initialization fails or protocol version is unsupported + """ sampling = types.SamplingCapability() if self._sampling_callback is not _default_sampling_callback else None elicitation = ( types.ElicitationCapability() if self._elicitation_callback is not _default_elicitation_callback else None @@ -288,7 +339,81 @@ async def call_tool( read_timeout_seconds: timedelta | None = None, progress_callback: ProgressFnT | None = None, ) -> types.CallToolResult: - """Send a tools/call request with optional progress callback support.""" + """Execute a tool on the connected MCP server. + + This method sends a tools/call request to execute a specific tool with provided + arguments. The server will validate the arguments against the tool's input schema + and return structured or unstructured content based on the tool's configuration. + + For tools that return structured output, the result will be automatically validated + against the tool's output schema if one is defined. Tools may also return various + content types including text, images, and embedded resources. + + Args: + name: The name of the tool to execute. Must match a tool exposed by the server. + arguments: Optional dictionary of arguments to pass to the tool. The structure + must match the tool's input schema. Defaults to None for tools that don't + require arguments. + read_timeout_seconds: Optional timeout for the tool execution. If not specified, + uses the session's default read timeout. Useful for long-running tools. + progress_callback: Optional callback function to receive progress updates during + tool execution. The callback receives progress notifications as they're sent + by the server. + + Returns: + CallToolResult containing the tool's response. The result includes: + - content: List of content blocks (text, images, embedded resources) + - structuredContent: Validated structured data if the tool has an output schema + - isError: Boolean indicating if the tool execution failed + + Raises: + RuntimeError: If the tool returns structured content that doesn't match its + output schema, or if the tool name is not found on the server. + ValidationError: If the provided arguments don't match the tool's input schema. + TimeoutError: If the tool execution exceeds the specified timeout. + + Example: + ```python + # Simple tool call without arguments + result = await session.call_tool("ping") + + # Tool call with arguments + result = await session.call_tool("add", {"a": 5, "b": 3}) + + # Access text content + for content in result.content: + if isinstance(content, types.TextContent): + print(content.text) + + # Access structured output (if available) + if result.structuredContent: + user_data = result.structuredContent + print(f"Result: {user_data}") + + # Handle tool execution errors + if result.isError: + print("Tool execution failed") + + # Long-running tool with progress tracking + def on_progress(progress_token, progress, total, message): + percent = (progress / total) * 100 if total else 0 + print(f"Progress: {percent:.1f}% - {message}") + + result = await session.call_tool( + "long_task", + {"steps": 10}, + read_timeout_seconds=timedelta(minutes=5), + progress_callback=on_progress + ) + ``` + + Note: + Tools may return different content types: + - TextContent: Plain text responses + - ImageContent: Generated images with MIME type and binary data + - EmbeddedResource: File contents or external resources + - Structured data via structuredContent when output schema is defined + """ result = await self.send_request( types.ClientRequest( diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index 700b5417f..79fb702d6 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -75,12 +75,14 @@ class ClientSessionGroup: the client and can be accessed via the session. Example Usage: - name_fn = lambda name, server_info: f"{(server_info.name)}_{name}" - async with ClientSessionGroup(component_name_hook=name_fn) as group: - for server_params in server_params: - await group.connect_to_server(server_param) - ... + ```python + name_fn = lambda name, server_info: f"{(server_info.name)}_{name}" + async with ClientSessionGroup(component_name_hook=name_fn) as group: + for server_params in server_params: + await group.connect_to_server(server_param) + # ... + ``` """ class _ComponentNames(BaseModel): diff --git a/src/mcp/server/fastmcp/exceptions.py b/src/mcp/server/fastmcp/exceptions.py index fb5bda106..2a9edefa0 100644 --- a/src/mcp/server/fastmcp/exceptions.py +++ b/src/mcp/server/fastmcp/exceptions.py @@ -2,20 +2,52 @@ class FastMCPError(Exception): - """Base error for FastMCP.""" + """Base exception class for all FastMCP-related errors. + + This is the root exception type for all errors that can occur within + the FastMCP framework. Specific error types inherit from this class. + """ class ValidationError(FastMCPError): - """Error in validating parameters or return values.""" + """Raised when parameter or return value validation fails. + + This exception is raised when input arguments don't match a tool's + input schema, or when output values fail validation against output schemas. + It typically indicates incorrect data types, missing required fields, + or values that don't meet schema constraints. + """ class ResourceError(FastMCPError): - """Error in resource operations.""" + """Raised when resource operations fail. + + This exception is raised for resource-related errors such as: + + - Resource not found for a given URI + - Resource content cannot be read or generated + - Resource template parameter validation failures + - Resource access permission errors + """ class ToolError(FastMCPError): - """Error in tool operations.""" + """Raised when tool operations fail. + + This exception is raised for tool-related errors such as: + + - Tool not found for a given name + - Tool execution failures or unhandled exceptions + - Tool registration conflicts or validation errors + - Tool parameter or result processing errors + """ class InvalidSignature(Exception): - """Invalid signature for use with FastMCP.""" + """Raised when a function signature is incompatible with FastMCP. + + This exception is raised when trying to register a function as a tool, + resource, or prompt that has an incompatible signature. This can occur + when functions have unsupported parameter types, complex annotations + that cannot be converted to JSON schema, or other signature issues. + """ diff --git a/src/mcp/server/fastmcp/prompts/base.py b/src/mcp/server/fastmcp/prompts/base.py index b45cfc917..cf5d2b0cf 100644 --- a/src/mcp/server/fastmcp/prompts/base.py +++ b/src/mcp/server/fastmcp/prompts/base.py @@ -74,6 +74,7 @@ def from_function( """Create a Prompt from a function. The function can return: + - A string (converted to a message) - A Message object - A dict (converted to a message) diff --git a/src/mcp/server/fastmcp/prompts/manager.py b/src/mcp/server/fastmcp/prompts/manager.py index 6b01d91cd..1aa949b59 100644 --- a/src/mcp/server/fastmcp/prompts/manager.py +++ b/src/mcp/server/fastmcp/prompts/manager.py @@ -1,4 +1,70 @@ -"""Prompt management functionality.""" +"""Prompt management functionality for FastMCP servers. + +This module provides the PromptManager class, which serves as the central registry +for managing prompts in FastMCP servers. Prompts are reusable templates that generate +structured messages for AI model interactions, enabling consistent and parameterized +communication patterns. + +The PromptManager handles the complete lifecycle of prompts: + +- Registration and storage of prompt templates +- Retrieval by name for use in MCP protocol handlers +- Rendering with arguments to produce message sequences +- Duplicate detection and management + +Key concepts: + +- Prompts are created from functions using Prompt.from_function() +- Each prompt has a unique name used for registration and retrieval +- Prompts can accept typed arguments for dynamic content generation +- Rendered prompts return Message objects ready for AI model consumption + +Examples: + Basic prompt management workflow: + + ```python + from mcp.server.fastmcp.prompts import PromptManager, Prompt + + # Initialize the manager + manager = PromptManager() + + # Create a prompt from a function + def analysis_prompt(topic: str, context: str) -> list[str]: + return [ + f"Please analyze the following topic: {topic}", + f"Additional context: {context}", + "Provide a detailed analysis with key insights." + ] + + # Register the prompt + prompt = Prompt.from_function(analysis_prompt) + manager.add_prompt(prompt) + + # Render the prompt with arguments + messages = await manager.render_prompt( + "analysis_prompt", + {"topic": "AI Safety", "context": "Enterprise deployment"} + ) + ``` + + Integration with FastMCP servers: + + ```python + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("My Server") + + @mcp.prompt() + def code_review(language: str, code: str) -> str: + return f"Review this {language} code for best practices:\\n\\n{code}" + + # The prompt is automatically registered with the server's PromptManager + ``` + +Note: + This module is primarily used internally by FastMCP servers, but can be used + directly for advanced prompt management scenarios or custom MCP implementations. +""" from typing import Any @@ -9,25 +75,91 @@ class PromptManager: - """Manages FastMCP prompts.""" + """Manages prompt registration, storage, and rendering for FastMCP servers. + + The PromptManager is the central registry for all prompts in a FastMCP server. It handles + prompt registration, retrieval by name, listing all available prompts, and rendering + prompts with provided arguments. Prompts are templates that can generate structured + messages for AI model interactions. + + This class is typically used internally by FastMCP servers but can be used directly + for advanced prompt management scenarios. + + Args: + warn_on_duplicate_prompts: Whether to log warnings when attempting to register + a prompt with a name that already exists. Defaults to True. + + Attributes: + warn_on_duplicate_prompts: Whether duplicate prompt warnings are enabled. + + Examples: + Basic usage: + + ```python + from mcp.server.fastmcp.prompts import PromptManager, Prompt + + # Create a manager + manager = PromptManager() + + # Create and add a prompt + def greeting_prompt(name: str) -> str: + return f"Hello, {name}! How can I help you today?" + + prompt = Prompt.from_function(greeting_prompt) + manager.add_prompt(prompt) + + # Render the prompt + messages = await manager.render_prompt("greeting_prompt", {"name": "Alice"}) + ``` + + Disabling duplicate warnings: + + ```python + # Useful in testing scenarios or when you need to replace prompts + manager = PromptManager(warn_on_duplicate_prompts=False) + ``` + """ def __init__(self, warn_on_duplicate_prompts: bool = True): self._prompts: dict[str, Prompt] = {} self.warn_on_duplicate_prompts = warn_on_duplicate_prompts def get_prompt(self, name: str) -> Prompt | None: - """Get prompt by name.""" + """Retrieve a registered prompt by its name. + + Args: + name: The name of the prompt to retrieve. + + Returns: + The Prompt object if found, None if no prompt exists with the given name. + """ return self._prompts.get(name) def list_prompts(self) -> list[Prompt]: - """List all registered prompts.""" + """Get a list of all registered prompts. + + Returns: + A list containing all Prompt objects currently registered with this manager. + Returns an empty list if no prompts are registered. + """ return list(self._prompts.values()) def add_prompt( self, prompt: Prompt, ) -> Prompt: - """Add a prompt to the manager.""" + """Register a prompt with the manager. + + If a prompt with the same name already exists, the existing prompt is returned + without modification. A warning is logged if warn_on_duplicate_prompts is True. + + Args: + prompt: The Prompt object to register. + + Returns: + The registered Prompt object. If a prompt with the same name already exists, + returns the existing prompt instead of the new one. + """ # Check for duplicates existing = self._prompts.get(prompt.name) @@ -40,7 +172,40 @@ def add_prompt( return prompt async def render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> list[Message]: - """Render a prompt by name with arguments.""" + """Render a prompt into a list of messages ready for AI model consumption. + + This method looks up the prompt by name, validates that all required arguments + are provided, executes the prompt function with the given arguments, and converts + the result into a standardized list of Message objects. + + Args: + name: The name of the prompt to render. + arguments: Optional dictionary of arguments to pass to the prompt function. + Must include all required arguments defined by the prompt. + + Returns: + A list of Message objects containing the rendered prompt content. + Each Message has a role ("user" or "assistant") and content. + + Raises: + ValueError: If the prompt name is not found or if required arguments are missing. + + Examples: + Simple prompt without arguments: + + ```python + messages = await manager.render_prompt("welcome") + ``` + + Prompt with arguments: + + ```python + messages = await manager.render_prompt( + "greeting", + {"name": "Alice", "language": "en"} + ) + ``` + """ prompt = self.get_prompt(name) if not prompt: raise ValueError(f"Unknown prompt: {name}") diff --git a/src/mcp/server/fastmcp/resources/base.py b/src/mcp/server/fastmcp/resources/base.py index f57631cc1..660436ab6 100644 --- a/src/mcp/server/fastmcp/resources/base.py +++ b/src/mcp/server/fastmcp/resources/base.py @@ -15,7 +15,19 @@ class Resource(BaseModel, abc.ABC): - """Base class for all resources.""" + """Base class for all MCP resources. + + Resources provide contextual data that can be read by LLMs. Each resource + has a URI, optional metadata like name and description, and content that + can be retrieved via the read() method. + + Attributes: + uri: Unique identifier for the resource + name: Optional name for the resource (defaults to URI if not provided) + title: Optional human-readable title + description: Optional description of the resource content + mime_type: MIME type of the resource content (defaults to text/plain) + """ model_config = ConfigDict(validate_default=True) @@ -32,7 +44,18 @@ class Resource(BaseModel, abc.ABC): @field_validator("name", mode="before") @classmethod def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: - """Set default name from URI if not provided.""" + """Set default name from URI if not provided. + + Args: + name: The provided name value + info: Pydantic validation info containing other field values + + Returns: + The name to use for the resource + + Raises: + ValueError: If neither name nor uri is provided + """ if name: return name if uri := info.data.get("uri"): @@ -41,5 +64,12 @@ def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: @abc.abstractmethod async def read(self) -> str | bytes: - """Read the resource content.""" + """Read the resource content. + + Returns: + The resource content as either a string or bytes + + Raises: + ResourceError: If the resource cannot be read + """ pass diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 924baaa9b..fb884efb8 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -119,6 +119,84 @@ async def wrap(_: MCPServer[LifespanResultT, Request]) -> AsyncIterator[Lifespan class FastMCP(Generic[LifespanResultT]): + """A high-level ergonomic interface for creating MCP servers. + + FastMCP provides a decorator-based API for building MCP servers with automatic + parameter validation, structured output support, and built-in transport handling. + It supports stdio, SSE, and Streamable HTTP transports out of the box. + + Features include automatic validation using Pydantic, structured output conversion, + context injection for MCP capabilities, lifespan management, multiple transport + support, and built-in OAuth 2.1 authentication. + + Args: + name: Human-readable name for the server. If None, defaults to "FastMCP" + instructions: Optional instructions/description for the server + auth_server_provider: OAuth authorization server provider for authentication + token_verifier: Token verifier for validating OAuth tokens + event_store: Event store for Streamable HTTP transport persistence + tools: Pre-configured tools to register with the server + debug: Enable debug mode for additional logging + log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + host: Host address for HTTP transports + port: Port number for HTTP transports + mount_path: Base mount path for SSE transport + sse_path: Path for SSE endpoint + message_path: Path for message endpoint + streamable_http_path: Path for Streamable HTTP endpoint + json_response: Whether to use JSON responses instead of SSE for Streamable HTTP + stateless_http: Whether to operate in stateless mode for Streamable HTTP + warn_on_duplicate_resources: Whether to warn when duplicate resources are registered + warn_on_duplicate_tools: Whether to warn when duplicate tools are registered + warn_on_duplicate_prompts: Whether to warn when duplicate prompts are registered + dependencies: List of package dependencies (currently unused) + lifespan: Async context manager for server startup/shutdown lifecycle + auth: Authentication settings for OAuth 2.1 support + transport_security: Transport security settings + + Examples: + Basic server creation: + + ```python + from mcp.server.fastmcp import FastMCP + + # Create a server + mcp = FastMCP("My Server") + + # Add a tool + @mcp.tool() + def add_numbers(a: int, b: int) -> int: + \"\"\"Add two numbers together.\"\"\" + return a + b + + # Add a resource + @mcp.resource("greeting://{name}") + def get_greeting(name: str) -> str: + \"\"\"Get a personalized greeting.\"\"\" + return f"Hello, {name}!" + + # Run the server + if __name__ == "__main__": + mcp.run() + ``` + + Server with authentication: + + ```python + from mcp.server.auth.settings import AuthSettings + from pydantic import AnyHttpUrl + + mcp = FastMCP( + "Protected Server", + auth=AuthSettings( + issuer_url=AnyHttpUrl("https://auth.example.com"), + resource_server_url=AnyHttpUrl("http://localhost:8000"), + required_scopes=["read", "write"] + ) + ) + ``` + """ + def __init__( self, name: str | None = None, @@ -235,7 +313,7 @@ def run( transport: Literal["stdio", "sse", "streamable-http"] = "stdio", mount_path: str | None = None, ) -> None: - """Run the FastMCP server. Note this is a synchronous function. + """Run the FastMCP server. This is a synchronous function. Args: transport: Transport protocol to use ("stdio", "sse", or "streamable-http") @@ -282,9 +360,91 @@ async def list_tools(self) -> list[MCPTool]: ] def get_context(self) -> Context[ServerSession, LifespanResultT, Request]: - """ - Returns a Context object. Note that the context will only be valid - during a request; outside a request, most methods will error. + """Get the current request context when automatic injection isn't available. + + This method provides access to the current [`Context`][mcp.server.fastmcp.Context] + object when you can't rely on FastMCP's automatic parameter injection. It's + primarily useful in helper functions, callbacks, or other scenarios where + the context isn't automatically provided via function parameters. + + In most cases, you should prefer automatic context injection by declaring + a Context parameter in your tool/resource functions. Use this method only + when you need context access from code that isn't directly called by FastMCP. + + You might call this method directly in: + + - **Helper functions** + + ```python + mcp = FastMCP(name="example") + + async def log_operation(operation: str): + # Get context when it's not injected + ctx = mcp.get_context() + await ctx.info(f"Performing operation: {operation}") + + @mcp.tool() + async def main_tool(data: str) -> str: + await log_operation("data_processing") # Helper needs context + return process_data(data) + ``` + + - **Callbacks** and **event handlers** when context is needed in async callbacks + + ```python + async def progress_callback(current: int, total: int): + ctx = mcp.get_context() # Access context in callback + await ctx.report_progress(current, total) + + @mcp.tool() + async def long_operation(data: str) -> str: + return await process_with_callback(data, progress_callback) + ``` + + - **Class methods** when context is needed in class-based code + + ```python + class DataProcessor: + def __init__(self, mcp_server: FastMCP): + self.mcp = mcp_server + + async def process_chunk(self, chunk: str) -> str: + ctx = self.mcp.get_context() # Get context in method + await ctx.debug(f"Processing chunk of size {len(chunk)}") + return processed_chunk + + processor = DataProcessor(mcp) + + @mcp.tool() + async def process_data(data: str) -> str: + return await processor.process_chunk(data) + ``` + + Returns: + [`Context`][mcp.server.fastmcp.Context] object for the current request + with access to all MCP capabilities including logging, progress reporting, + user interaction, and session access. + + Raises: + LookupError: If called outside of a request context (e.g., during server + initialization, shutdown, or from code not handling a client request). + + Note: + **Prefer automatic injection**: In most cases, declare a Context parameter + in your function signature instead of calling this method: + + ```python + # Preferred approach + @mcp.tool() + async def my_tool(data: str, ctx: Context) -> str: + await ctx.info("Processing data") + return result + + # Only use get_context() when injection isn't available + async def helper_function(): + ctx = mcp.get_context() + await ctx.info("Helper called") + ``` """ try: request_context = self._mcp_server.request_context @@ -293,12 +453,29 @@ def get_context(self) -> Context[ServerSession, LifespanResultT, Request]: return Context(request_context=request_context, fastmcp=self) async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock] | dict[str, Any]: - """Call a tool by name with arguments.""" + """Call a registered tool by name with the provided arguments. + + Args: + name: Name of the tool to call + arguments: Dictionary of arguments to pass to the tool + + Returns: + Tool execution result, either as content blocks or structured data + + Raises: + ToolError: If the tool is not found or execution fails + ValidationError: If the arguments don't match the tool's schema + """ context = self.get_context() return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True) async def list_resources(self) -> list[MCPResource]: - """List all available resources.""" + """List all available resources registered with this server. + + Returns: + List of MCP Resource objects containing URI, name, description, and MIME type + information for each registered resource. + """ resources = self._resource_manager.list_resources() return [ @@ -313,6 +490,15 @@ async def list_resources(self) -> list[MCPResource]: ] async def list_resource_templates(self) -> list[MCPResourceTemplate]: + """List all available resource templates registered with this server. + + Resource templates define URI patterns that can be dynamically resolved + with different parameters to access multiple related resources. + + Returns: + List of MCP ResourceTemplate objects containing URI templates, names, + and descriptions for each registered resource template. + """ templates = self._resource_manager.list_templates() return [ MCPResourceTemplate( @@ -325,7 +511,17 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]: ] async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: - """Read a resource by URI.""" + """Read the contents of a resource by its URI. + + Args: + uri: The URI of the resource to read + + Returns: + Iterable of ReadResourceContents containing the resource data + + Raises: + ResourceError: If the resource is not found or cannot be read + """ resource = await self._resource_manager.get_resource(uri) if not resource: @@ -397,19 +593,22 @@ def tool( - If False, unconditionally creates an unstructured tool Example: - @server.tool() - def my_tool(x: int) -> str: - return str(x) - - @server.tool() - def tool_with_context(x: int, ctx: Context) -> str: - ctx.info(f"Processing {x}") - return str(x) - - @server.tool() - async def async_tool(x: int, context: Context) -> str: - await context.report_progress(50, 100) - return str(x) + + ```python + @server.tool() + def my_tool(x: int) -> str: + return str(x) + + @server.tool() + def tool_with_context(x: int, ctx: Context) -> str: + ctx.info(f"Processing {x}") + return str(x) + + @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): @@ -439,12 +638,15 @@ def completion(self): - context: Optional CompletionContext with previously resolved arguments Example: - @mcp.completion() - async def handle_completion(ref, argument, context): - if isinstance(ref, ResourceTemplateReference): - # Return completions based on ref, argument, and context - return Completion(values=["option1", "option2"]) - return None + + ```python + @mcp.completion() + async def handle_completion(ref, argument, context): + if isinstance(ref, ResourceTemplateReference): + # Return completions based on ref, argument, and context + return Completion(values=["option1", "option2"]) + return None + ``` """ return self._mcp_server.completion() @@ -484,23 +686,26 @@ def resource( mime_type: Optional MIME type for the resource Example: - @server.resource("resource://my-resource") - def get_data() -> str: - return "Hello, world!" - - @server.resource("resource://my-resource") - async get_data() -> str: - data = await fetch_data() - return f"Hello, world! {data}" - - @server.resource("resource://{city}/weather") - def get_weather(city: str) -> str: - return f"Weather for {city}" - - @server.resource("resource://{city}/weather") - async def get_weather(city: str) -> str: - data = await fetch_weather(city) - return f"Weather for {city}: {data}" + + ```python + @server.resource("resource://my-resource") + def get_data() -> str: + return "Hello, world!" + + @server.resource("resource://my-resource") + async get_data() -> str: + data = await fetch_data() + return f"Hello, world! {data}" + + @server.resource("resource://{city}/weather") + def get_weather(city: str) -> str: + return f"Weather for {city}" + + @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): @@ -566,32 +771,35 @@ def prompt( title: Optional human-readable title for the prompt description: Optional description of what the prompt does - Example: - @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}" - } - ] - - @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 - } + Examples: + + ```python + @server.prompt() + def analyze_table(table_name: str) -> list[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: {schema}" + } + ] + + @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 } } - ] + } + ] + ``` """ # Check if user passed function directly instead of calling decorator if callable(name): @@ -630,9 +838,12 @@ def custom_route( include_in_schema: Whether to include in OpenAPI schema, defaults to True Example: - @server.custom_route("/health", methods=["GET"]) - async def health_check(request: Request) -> Response: - return JSONResponse({"status": "ok"}) + + ```python + @server.custom_route("/health", methods=["GET"]) + async def health_check(request: Request) -> Response: + return JSONResponse({"status": "ok"}) + ``` """ def decorator( @@ -1007,37 +1218,156 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]): - """Context object providing access to MCP capabilities. + """High-level context object providing convenient access to MCP capabilities. - This provides a cleaner interface to MCP's RequestContext functionality. - It gets injected into tool and resource functions that request it via type hints. + This is FastMCP's user-friendly wrapper around the underlying [`RequestContext`][mcp.shared.context.RequestContext] + that provides the same functionality with additional convenience methods and better + ergonomics. It gets automatically injected into FastMCP tool and resource functions + that declare it in their type hints, eliminating the need to manually access the + request context. - To use context in a tool function, add a parameter with the Context type annotation: + The Context object provides access to all MCP capabilities including logging, + progress reporting, resource reading, user interaction, capability checking, and + access to the underlying session and request metadata. It's the recommended way + to interact with MCP functionality in FastMCP applications. + + ## Automatic injection + + Context is automatically injected into functions based on type hints. The parameter + name can be anything as long as it's annotated with `Context`. The context parameter + is optional - tools that don't need it can omit it entirely. ```python - @server.tool() - def my_tool(x: int, ctx: Context) -> str: - # Log messages to the client - ctx.info(f"Processing {x}") - ctx.debug("Debug info") - ctx.warning("Warning message") - ctx.error("Error message") + from mcp.server.fastmcp import FastMCP, Context + + mcp = FastMCP(name="example") + + @mcp.tool() + async def simple_tool(data: str) -> str: + # No context needed + return f"Processed: {data}" + + @mcp.tool() + async def advanced_tool(data: str, ctx: Context) -> str: + # Context automatically injected + await ctx.info("Starting processing") + return f"Processed: {data}" + ``` + + ## Relationship to RequestContext + + Context is a thin wrapper around [`RequestContext`][mcp.shared.context.RequestContext] + that provides the same underlying functionality with additional convenience methods: - # Report progress - ctx.report_progress(50, 100) + - **Context convenience methods**: `ctx.info()`, `ctx.error()`, `ctx.elicit()`, etc. + - **Direct RequestContext access**: `ctx.request_context` for low-level operations + - **Session access**: `ctx.session` for advanced ServerSession functionality + - **Request metadata**: `ctx.request_id`, access to lifespan context, etc. - # Access resources - data = ctx.read_resource("resource://data") + ## Capabilities provided - # Get request info - request_id = ctx.request_id - client_id = ctx.client_id + **Logging**: Send structured log messages to the client with automatic request linking: - return str(x) + ```python + await ctx.debug("Detailed debug information") + await ctx.info("General status updates") + await ctx.warning("Important warnings") + await ctx.error("Error conditions") + ``` + + **Progress reporting**: Keep users informed during long operations: + + ```python + for i in range(100): + await ctx.report_progress(i, 100, f"Processing item {i}") + # ... do work + ``` + + **User interaction**: Collect additional information during tool execution: + + ```python + class UserPrefs(BaseModel): + format: str + detailed: bool + + result = await ctx.elicit("How should I format the output?", UserPrefs) + if result.action == "accept": + format_data(data, result.data.format) + ``` + + **Resource access**: Read MCP resources during tool execution: + + ```python + content = await ctx.read_resource("file://data/config.json") + ``` + + **Capability checking**: Verify client support before using advanced features: + + ```python + if ctx.session.check_client_capability(types.ClientCapabilities(sampling=...)): + # Use advanced features + pass + ``` + + ## Examples + + Complete tool with context usage: + + ```python + from pydantic import BaseModel + from mcp.server.fastmcp import FastMCP, Context + + class ProcessingOptions(BaseModel): + format: str + include_metadata: bool + + mcp = FastMCP(name="processor") + + @mcp.tool() + async def process_data( + data: str, + ctx: Context, + auto_format: bool = False + ) -> str: + await ctx.info(f"Starting to process {len(data)} characters") + + # Get user preferences if not auto-formatting + if not auto_format: + if ctx.session.check_client_capability( + types.ClientCapabilities(elicitation=types.ElicitationCapability()) + ): + prefs_result = await ctx.elicit( + "How would you like the data processed?", + ProcessingOptions + ) + if prefs_result.action == "accept": + format_type = prefs_result.data.format + include_meta = prefs_result.data.include_metadata + else: + await ctx.warning("Using default format") + format_type = "standard" + include_meta = False + else: + format_type = "standard" + include_meta = False + else: + format_type = "auto" + include_meta = True + + # Process with progress updates + for i in range(0, len(data), 100): + chunk = data[i:i+100] + await ctx.report_progress(i, len(data), f"Processing chunk {i//100 + 1}") + # ... process chunk + + await ctx.info(f"Processing complete with format: {format_type}") + return processed_data ``` - 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. + Note: + Context objects are request-scoped and automatically managed by FastMCP. + Don't store references to them beyond the request lifecycle. Each tool + invocation gets a fresh Context instance tied to that specific request. """ _request_context: RequestContext[ServerSessionT, LifespanContextT, RequestT] | None @@ -1065,7 +1395,36 @@ def fastmcp(self) -> FastMCP: def request_context( self, ) -> RequestContext[ServerSessionT, LifespanContextT, RequestT]: - """Access to the underlying request context.""" + """Access to the underlying RequestContext for low-level operations. + + This property provides direct access to the [`RequestContext`][mcp.shared.context.RequestContext] + that this Context wraps. Use this when you need low-level access to request + metadata, lifespan context, or other features not exposed by Context's + convenience methods. + + Most users should prefer Context's convenience methods like `info()`, `elicit()`, + etc. rather than accessing the underlying RequestContext directly. + + Returns: + The underlying [`RequestContext`][mcp.shared.context.RequestContext] containing + session, metadata, and lifespan context. + + Raises: + ValueError: If called outside of a request context. + + Example: + ```python + @mcp.tool() + async def advanced_tool(data: str, ctx: Context) -> str: + # Access lifespan context directly + db = ctx.request_context.lifespan_context["database"] + + # Access request metadata + progress_token = ctx.request_context.meta.progressToken if ctx.request_context.meta else None + + return processed_data + ``` + """ if self._request_context is None: raise ValueError("Context is not available outside of a request") return self._request_context @@ -1107,26 +1466,132 @@ async def elicit( message: str, schema: type[ElicitSchemaModelT], ) -> ElicitationResult[ElicitSchemaModelT]: - """Elicit information from the client/user. + """Elicit structured information from the client or user during tool execution. + + This method enables interactive data collection from clients during tool processing. + The client may display the message to the user and collect a response according to + the provided Pydantic schema, or if the client is an agent, it may automatically + generate an appropriate response. This is useful for gathering additional parameters, + user preferences, or confirmation before proceeding with operations. - This method can be used to interactively ask for additional information from the - client within a tool's execution. The client might display the message to the - user and collect a response according to the provided schema. Or in case a - client is an agent, it might decide how to handle the elicitation -- either by asking - the user or automatically generating a response. + You typically access this method through the [`Context`][mcp.server.fastmcp.Context] + object injected into your FastMCP tool functions. Always check that the client + supports elicitation using [`check_client_capability`][mcp.server.session.ServerSession.check_client_capability] + before calling this method. Args: - schema: A Pydantic model class defining the expected response structure, according to the specification, - only primive types are allowed. - message: Optional message to present to the user. If not provided, will use - a default message based on the schema + message: The prompt or question to present to the user. Should clearly explain + what information is being requested and why it's needed. + schema: A Pydantic model class defining the expected response structure. + According to the MCP specification, only primitive types (str, int, float, bool) + and simple containers (list, dict) are allowed - no complex nested objects. Returns: - An ElicitationResult containing the action taken and the data if accepted + `ElicitationResult` containing: + + - `action`: One of "accept", "decline", or "cancel" indicating user response + - `data`: The structured response data (only populated if action is "accept") + + Raises: + RuntimeError: If called before session initialization is complete. + ValidationError: If the client response doesn't match the provided schema. + Various exceptions: Depending on client implementation and user interaction. + + Examples: + Collect user preferences before processing: + + ```python + from pydantic import BaseModel + from mcp.server.fastmcp import FastMCP, Context + + class ProcessingOptions(BaseModel): + format: str + include_metadata: bool + max_items: int + + mcp = FastMCP(name="example-server") + + @mcp.tool() + async def process_data(data: str, ctx: Context) -> str: + # Check if client supports elicitation + if not ctx.session.check_client_capability( + types.ClientCapabilities(elicitation=types.ElicitationCapability()) + ): + # Fall back to default processing + return process_with_defaults(data) + + # Ask user for processing preferences + result = await ctx.elicit( + "How would you like me to process this data?", + ProcessingOptions + ) + + if result.action == "accept": + options = result.data + await ctx.info(f"Processing with format: {options.format}") + return process_with_options(data, options) + elif result.action == "decline": + return process_with_defaults(data) + else: # cancel + return "Processing cancelled by user" + ``` + + Confirm before destructive operations: + + ```python + class ConfirmDelete(BaseModel): + confirm: bool + reason: str + + @mcp.tool() + async def delete_files(pattern: str, ctx: Context) -> str: + files = find_matching_files(pattern) + + result = await ctx.elicit( + f"About to delete {len(files)} files matching '{pattern}'. Continue?", + ConfirmDelete + ) + + if result.action == "accept" and result.data.confirm: + await ctx.info(f"Deletion confirmed: {result.data.reason}") + return delete_files(files) + else: + return "Deletion cancelled" + ``` + + Handle different response types: + + ```python + class UserChoice(BaseModel): + option: str # "auto", "manual", "skip" + details: str + + @mcp.tool() + async def configure_system(ctx: Context) -> str: + result = await ctx.elicit( + "How should I configure the system?", + UserChoice + ) + + match result.action: + case "accept": + choice = result.data + await ctx.info(f"User selected: {choice.option}") + return configure_with_choice(choice) + case "decline": + await ctx.warning("User declined configuration") + return "Configuration skipped by user" + case "cancel": + await ctx.info("Configuration cancelled") + return "Operation cancelled" + ``` Note: - Check the result.action to determine if the user accepted, declined, or cancelled. - The result.data will only be populated if action is "accept" and validation succeeded. + The client determines how to handle elicitation requests. Some clients may + show interactive forms to users, while others may automatically generate + responses based on context. Always handle all possible action values + ("accept", "decline", "cancel") in your code and provide appropriate + fallbacks for clients that don't support elicitation. """ return await elicit_with_validation( @@ -1146,7 +1611,6 @@ async def log( level: Log level (debug, info, warning, error) message: Log message logger_name: Optional logger name - **extra: Additional structured data to include """ await self.request_context.session.send_log_message( level=level, @@ -1162,12 +1626,79 @@ def client_id(self) -> str | None: @property def request_id(self) -> str: - """Get the unique ID for this request.""" + """Get the unique identifier for the current request. + + This ID uniquely identifies the current client request and is useful for + logging, tracing, error reporting, and linking related operations. It's + automatically used by Context's convenience methods when sending notifications + or responses to ensure they're associated with the correct request. + + Returns: + str: Unique request identifier that can be used for tracing and logging. + + Example: + ```python + @mcp.tool() + async def traceable_tool(data: str, ctx: Context) -> str: + # Log with request ID for traceability + print(f"Processing request {ctx.request_id}") + + # Request ID is automatically included in Context methods + await ctx.info("Starting processing") # Links to this request + + return processed_data + ``` + """ return str(self.request_context.request_id) @property - def session(self): - """Access to the underlying session for advanced usage.""" + def session(self) -> ServerSession: + """Access to the underlying ServerSession for advanced MCP operations. + + This property provides direct access to the [`ServerSession`][mcp.server.session.ServerSession] + for advanced operations not covered by Context's convenience methods. Use this + when you need direct session control, capability checking, or low-level MCP + protocol operations. + + Most users should prefer Context's convenience methods (`info()`, `elicit()`, etc.) + which internally use this session with appropriate request linking. + + Returns: + [`ServerSession`][mcp.server.session.ServerSession]: The session for + communicating with the client and accessing advanced MCP features. + + Examples: + Capability checking before using advanced features: + + ```python + @mcp.tool() + async def advanced_tool(data: str, ctx: Context) -> str: + # Check client capabilities + if ctx.session.check_client_capability( + types.ClientCapabilities(sampling=types.SamplingCapability()) + ): + # Use LLM sampling + response = await ctx.session.create_message( + messages=[types.SamplingMessage(...)], + max_tokens=100 + ) + return response.content.text + else: + return "Client doesn't support LLM sampling" + ``` + + Direct resource notifications: + + ```python + @mcp.tool() + async def update_resource(uri: str, ctx: Context) -> str: + # ... update the resource ... + + # Notify client of resource changes + await ctx.session.send_resource_updated(AnyUrl(uri)) + return "Resource updated" + ``` + """ return self.request_context.session # Convenience methods for common log levels diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index f50126081..24716a21f 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -48,7 +48,23 @@ def from_function( annotations: ToolAnnotations | None = None, structured_output: bool | None = None, ) -> Tool: - """Create a Tool from a function.""" + """Create a Tool from a function. + + Args: + fn: The function to wrap as a tool + name: Optional name for the tool (defaults to function name) + title: Optional human-readable title for the tool + description: Optional description (defaults to function docstring) + context_kwarg: Name of parameter that should receive the Context object + annotations: Optional tool annotations for additional metadata + structured_output: Whether to enable structured output for this tool + + Returns: + Tool instance configured from the function + + Raises: + ValueError: If the function is a lambda without a provided name + """ from mcp.server.fastmcp.server import Context func_name = name or fn.__name__ @@ -93,7 +109,19 @@ async def run( context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, convert_result: bool = False, ) -> Any: - """Run the tool with arguments.""" + """Run the tool with the provided arguments. + + Args: + arguments: Dictionary of arguments to pass to the tool function + context: Optional MCP context for accessing capabilities + convert_result: Whether to convert the result using the function metadata + + Returns: + The tool's execution result, potentially converted based on convert_result + + Raises: + ToolError: If tool execution fails or validation errors occur + """ try: result = await self.fn_metadata.call_fn_with_arg_validation( self.fn, diff --git a/src/mcp/server/fastmcp/tools/tool_manager.py b/src/mcp/server/fastmcp/tools/tool_manager.py index bfa8b2382..e0c6901a9 100644 --- a/src/mcp/server/fastmcp/tools/tool_manager.py +++ b/src/mcp/server/fastmcp/tools/tool_manager.py @@ -17,7 +17,15 @@ class ToolManager: - """Manages FastMCP tools.""" + """Manages registration and execution of FastMCP tools. + + The ToolManager handles tool registration, validation, and execution. + It maintains a registry of tools and provides methods for adding, + retrieving, and calling tools. + + Attributes: + warn_on_duplicate_tools: Whether to warn when duplicate tools are registered + """ def __init__( self, @@ -35,11 +43,22 @@ def __init__( self.warn_on_duplicate_tools = warn_on_duplicate_tools def get_tool(self, name: str) -> Tool | None: - """Get tool by name.""" + """Get a registered tool by name. + + Args: + name: Name of the tool to retrieve + + Returns: + Tool instance if found, None otherwise + """ return self._tools.get(name) def list_tools(self) -> list[Tool]: - """List all registered tools.""" + """List all registered tools. + + Returns: + List of all Tool instances registered with this manager + """ return list(self._tools.values()) def add_tool( diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 70be8796d..256f63749 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -185,11 +185,12 @@ def func_metadata( func: The function to convert to a pydantic model skip_names: A list of parameter names to skip. These will not be included in the model. - structured_output: Controls whether the tool's output is structured or unstructured - - If None, auto-detects based on the function's return type annotation - - If True, unconditionally creates a structured tool (return type annotation permitting) - - If False, unconditionally creates an unstructured tool + structured_output: Controls whether the tool's output is structured or unstructured. + If None, auto-detects based on the function's return type annotation. + If True, unconditionally creates a structured tool (return type annotation permitting). + If False, unconditionally creates an unstructured tool. + Note: If structured, creates a Pydantic model for the function's result based on its annotation. Supports various return types: - BaseModel subclasses (used directly) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 8c459383c..b65279b23 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -5,64 +5,83 @@ It allows you to easily define and handle various types of requests and notifications in an asynchronous manner. -Usage: -1. Create a Server instance: - server = Server("your_server_name") - -2. Define request handlers using decorators: - @server.list_prompts() - async def handle_list_prompts() -> list[types.Prompt]: - # Implementation - - @server.get_prompt() - async def handle_get_prompt( - name: str, arguments: dict[str, str] | None - ) -> types.GetPromptResult: - # Implementation - - @server.list_tools() - async def handle_list_tools() -> list[types.Tool]: - # Implementation - - @server.call_tool() - async def handle_call_tool( - name: str, arguments: dict | None - ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: - # Implementation - - @server.list_resource_templates() - async def handle_list_resource_templates() -> list[types.ResourceTemplate]: - # Implementation +The [`Server`][mcp.server.lowlevel.server.Server] class provides methods to register handlers for various MCP requests and +notifications. It automatically manages the request context and handles incoming +messages from the client. + +## Usage example + +1. Create a [`Server`][mcp.server.lowlevel.server.Server] instance: + + ```python + server = Server("your_server_name") + ``` + + 2. Define request handlers using decorators: + + ```python + @server.list_prompts() + async def handle_list_prompts() -> list[types.Prompt]: + # Implementation + ... + + @server.get_prompt() + async def handle_get_prompt( + name: str, arguments: dict[str, str] | None + ) -> types.GetPromptResult: + # Implementation + ... + + @server.list_tools() + async def handle_list_tools() -> list[types.Tool]: + # Implementation + ... + + @server.call_tool() + async def handle_call_tool( + name: str, arguments: dict | None + ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + # Implementation + ... + + @server.list_resource_templates() + async def handle_list_resource_templates() -> list[types.ResourceTemplate]: + # Implementation + ... + ``` 3. Define notification handlers if needed: - @server.progress_notification() - async def handle_progress( - progress_token: str | int, progress: float, total: float | None, - message: str | None - ) -> None: - # Implementation + + ```python + @server.progress_notification() + async def handle_progress( + progress_token: str | int, progress: float, total: float | None, + message: str | None + ) -> None: + # Implementation + ... + ``` 4. Run the server: - async def main(): - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="your_server_name", - server_version="your_version", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - asyncio.run(main()) - -The Server class provides methods to register handlers for various MCP requests and -notifications. It automatically manages the request context and handles incoming -messages from the client. + + ```python + async def main(): + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="your_server_name", + server_version="your_version", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + asyncio.run(main()) + ``` """ from __future__ import annotations as _annotations @@ -122,7 +141,7 @@ async def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[s """Default lifespan context manager that does nothing. Args: - server: The server instance this lifespan is managing + _: The server instance this lifespan is managing Returns: An empty context object @@ -226,7 +245,73 @@ def get_capabilities( def request_context( self, ) -> RequestContext[ServerSession, LifespanResultT, RequestT]: - """If called outside of a request context, this will raise a LookupError.""" + """Access the current request context for low-level MCP server operations. + + This property provides access to the [`RequestContext`][mcp.shared.context.RequestContext] + for the current request, which contains the session, request metadata, lifespan + context, and other request-scoped information. This is the primary way to access + MCP capabilities when using the low-level SDK. + + You typically access this property from within handler functions (tool handlers, + resource handlers, prompt handlers, etc.) to get the context for the current + client request. The context is automatically managed by the server and is only + available during request processing. + + Examples: + + **Logging and communication**: + + ```python + @app.call_tool() + async def my_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + await ctx.session.send_log_message( + level="info", + data="Starting tool processing", + related_request_id=ctx.request_id + ) + ``` + + **Capability checking**: + + ```python + @app.call_tool() + async def advanced_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + if ctx.session.check_client_capability( + types.ClientCapabilities(sampling=types.SamplingCapability()) + ): + # Use advanced features + response = await ctx.session.create_message(messages, max_tokens=100) + else: + # Fall back to basic functionality + pass + ``` + + **Accessing lifespan resources**: + + ```python + @app.call_tool() + async def database_query(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + db = ctx.lifespan_context["database"] # Access startup resource + results = await db.query(arguments["sql"]) + return [types.TextContent(type="text", text=str(results))] + ``` + + Returns: + [`RequestContext`][mcp.shared.context.RequestContext] for the current request, + containing session, metadata, and lifespan context. + + Raises: + LookupError: If called outside of a request context (e.g., during server + initialization, shutdown, or from code not handling a client request). + + Note: + For FastMCP applications, consider using the injected [`Context`][mcp.server.fastmcp.Context] + parameter instead, which provides the same functionality with additional + convenience methods and better ergonomics. + """ return request_ctx.get() def list_prompts(self): @@ -430,6 +515,7 @@ def call_tool(self, *, validate_input: bool = True): The handler validates input against inputSchema (if validate_input=True), calls the tool function, and builds a CallToolResult with the results: + - Unstructured content (iterable of ContentBlock): returned in content - Structured content (dict): returned in structuredContent, serialized JSON text returned in content - Both: returned in content and structuredContent diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 5c696b136..edc42fd31 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -6,31 +6,30 @@ used in MCP servers to interact with the client. Common usage pattern: -``` - server = Server(name) - - @server.call_tool() - async def handle_tool_call(ctx: RequestContext, arguments: dict[str, Any]) -> Any: - # Check client capabilities before proceeding - if ctx.session.check_client_capability( - types.ClientCapabilities(experimental={"advanced_tools": dict()}) - ): - # Perform advanced tool operations - result = await perform_advanced_tool_operation(arguments) - else: - # Fall back to basic tool operations - result = await perform_basic_tool_operation(arguments) - - return result - - @server.list_prompts() - async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]: - # Access session for any necessary checks or operations - if ctx.session.client_params: - # Customize prompts based on client initialization parameters - return generate_custom_prompts(ctx.session.client_params) - else: - return default_prompts +```python +server = Server(name) + +@server.call_tool() +async def handle_tool_call(ctx: RequestContext, arguments: dict[str, Any]) -> Any: + # Check client capabilities before proceeding + if ctx.session.check_client_capability( + types.ClientCapabilities(experimental={"advanced_tools": dict()}) + ): + # Perform advanced tool operations + result = await perform_advanced_tool_operation(arguments) + else: + # Fall back to basic tool operations + result = await perform_basic_tool_operation(arguments) + return result + +@server.list_prompts() +async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]: + # Access session for any necessary checks or operations + if ctx.session.client_params: + # Customize prompts based on client initialization parameters + return generate_custom_prompts(ctx.session.client_params) + else: + return default_prompts ``` The ServerSession class is typically used internally by the Server class and should not @@ -103,7 +102,102 @@ def client_params(self) -> types.InitializeRequestParams | None: return self._client_params def check_client_capability(self, capability: types.ClientCapabilities) -> bool: - """Check if the client supports a specific capability.""" + """Check if the client supports specific capabilities before using advanced MCP features. + + This method allows MCP servers to verify that the connected client supports + required capabilities before calling methods that depend on them. It performs + an AND operation - the client must support ALL capabilities specified in the + request, not just some of them. + + You typically access this method through the session available in your request + context via [`app.request_context.session`][mcp.shared.context.RequestContext] + within handler functions. Always check capabilities before using features like + sampling, elicitation, or experimental functionality. + + Args: + capability: A [`types.ClientCapabilities`][mcp.types.ClientCapabilities] object + specifying which capabilities to check. Can include: + + - `roots`: Check if client supports root listing operations + - `sampling`: Check if client supports LLM sampling via [`create_message`][mcp.server.session.ServerSession.create_message] + - `elicitation`: Check if client supports user interaction via [`elicit`][mcp.server.session.ServerSession.elicit] + - `experimental`: Check for non-standard experimental capabilities + + Returns: + bool: `True` if the client supports ALL requested capabilities, `False` if + the client hasn't been initialized yet or lacks any of the requested + capabilities. + + Examples: + Check sampling capability before creating LLM messages: + + ```python + from typing import Any + from mcp.server.lowlevel import Server + import mcp.types as types + + app = Server("example-server") + + @app.call_tool() + async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + + # Check if client supports LLM sampling + if ctx.session.check_client_capability( + types.ClientCapabilities(sampling=types.SamplingCapability()) + ): + # Safe to use create_message + response = await ctx.session.create_message( + messages=[types.SamplingMessage( + role="user", + content=types.TextContent(type="text", text="Help me analyze this data") + )], + max_tokens=100 + ) + return [types.TextContent(type="text", text=response.content.text)] + else: + return [types.TextContent(type="text", text="Client doesn't support LLM sampling")] + ``` + + Check experimental capabilities: + + ```python + @app.call_tool() + async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + + # Check for experimental advanced tools capability + if ctx.session.check_client_capability( + types.ClientCapabilities(experimental={"advanced_tools": {}}) + ): + # Use experimental features + return await use_advanced_tool_features(arguments) + else: + # Fall back to basic functionality + return await use_basic_tool_features(arguments) + ``` + + Check multiple capabilities at once: + + ```python + # Client must support BOTH sampling AND elicitation + if ctx.session.check_client_capability( + types.ClientCapabilities( + sampling=types.SamplingCapability(), + elicitation=types.ElicitationCapability() + ) + ): + # Safe to use both features + user_input = await ctx.session.elicit("What would you like to analyze?", schema) + llm_response = await ctx.session.create_message(messages, max_tokens=100) + ``` + + Note: + This method returns `False` if the session hasn't been initialized yet + (before the client sends the initialization request). It also returns + `False` if the client lacks ANY of the requested capabilities - all + specified capabilities must be supported for this method to return `True`. + """ if self._client_params is None: return False @@ -182,7 +276,157 @@ async def send_log_message( logger: str | None = None, related_request_id: types.RequestId | None = None, ) -> None: - """Send a log message notification.""" + """Send a log message notification from the server to the client. + + This method allows MCP servers to send log messages to the connected client for + debugging, monitoring, and error reporting purposes. The client can filter these + messages based on the logging level it has configured via the logging/setLevel + request. Check client capabilities using [`check_client_capability`][mcp.server.session.ServerSession.check_client_capability] + if you need to verify logging support. + + You typically access this method through the session available in your request + context. When using the low-level SDK, access it via + [`app.request_context.session`][mcp.shared.context.RequestContext] within handler + functions. With FastMCP, use the convenience logging methods on the + [`Context`][mcp.server.fastmcp.Context] object instead, like + [`ctx.info()`][mcp.server.fastmcp.Context.info] or + [`ctx.error()`][mcp.server.fastmcp.Context.error]. + + Log messages are one-way notifications and do not expect a response from the client. + They are useful for providing visibility into server operations, debugging issues, + and tracking the flow of request processing. + + Args: + level: The severity level of the log message as a `types.LoggingLevel`. Must be one of: + + - `debug`: Detailed information for debugging + - `info`: General informational messages + - `notice`: Normal but significant conditions + - `warning`: Warning conditions that should be addressed + - `error`: Error conditions that don't prevent operation + - `critical`: Critical conditions requiring immediate attention + - `alert`: Action must be taken immediately + - `emergency`: System is unusable + + data: The data to log. Can be any JSON-serializable value including: + + - Simple strings for text messages + - Objects/dictionaries for structured logging + - Lists for multiple related items + - Numbers, booleans, or null values + + logger: Optional name to identify the source of the log message. + Useful for categorizing logs from different components or modules + within your server (e.g., "database", "auth", "tool_handler"). + related_request_id: Optional `types.RequestId` linking this log to a specific client request. + Use this to associate log messages with the request they relate to, + making it easier to trace request processing and debug issues. + + Returns: + None + + Raises: + RuntimeError: If called before session initialization is complete. + Various exceptions: Depending on serialization or transport errors. + + Examples: + In a tool handler using the low-level SDK: + + ```python + from typing import Any + from mcp.server.lowlevel import Server + import mcp.types as types + + app = Server("example-server") + + @app.call_tool() + async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + # Access the request context to get the session + ctx = app.request_context + + # Log the start of processing + await ctx.session.send_log_message( + level="info", + data=f"Processing tool call: {name}", + logger="tool_handler", + related_request_id=ctx.request_id + ) + + # Process and log any issues + try: + result = perform_operation(arguments) + except Exception as e: + await ctx.session.send_log_message( + level="error", + data={"error": str(e), "tool": name, "args": arguments}, + logger="tool_handler", + related_request_id=ctx.request_id + ) + raise + + return [types.TextContent(type="text", text=str(result))] + ``` + + Using FastMCP's [`Context`][mcp.server.fastmcp.Context] helper for cleaner logging: + + ```python + from mcp.server.fastmcp import FastMCP, Context + + mcp = FastMCP(name="example-server") + + @mcp.tool() + async def fetch_data(url: str, ctx: Context) -> str: + # FastMCP's Context provides convenience methods that internally + # call send_log_message with the appropriate parameters + await ctx.info(f"Fetching data from {url}") + await ctx.debug("Starting request") + + try: + data = await fetch(url) + await ctx.info("Data fetched successfully") + return data + except Exception as e: + await ctx.error(f"Failed to fetch: {e}") + raise + ``` + + Streaming notifications with progress updates: + + ```python + import anyio + from typing import Any + from mcp.server.lowlevel import Server + import mcp.types as types + + app = Server("example-server") + + @app.call_tool() + async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context + count = arguments.get("count", 5) + + for i in range(count): + # Send progress updates to the client + await ctx.session.send_log_message( + level="info", + data=f"[{i + 1}/{count}] Processing item", + logger="progress_stream", + related_request_id=ctx.request_id + ) + if i < count - 1: + await anyio.sleep(1) + + return [types.TextContent(type="text", text="Operation complete")] + ``` + + Note: + Log messages are only delivered to the client if the client's configured + logging level permits it. For example, if the client has set its level to + "warning", it will not receive "debug" or "info" messages. Consider this + when deciding what level to use for your log messages. This method internally + uses [`send_notification`][mcp.shared.session.BaseSession.send_notification] to + deliver the log message to the client. + """ await self.send_notification( types.ServerNotification( types.LoggingMessageNotification( @@ -221,7 +465,86 @@ async def create_message( model_preferences: types.ModelPreferences | None = None, related_request_id: types.RequestId | None = None, ) -> types.CreateMessageResult: - """Send a sampling/create_message request.""" + """Send a message to an LLM through the MCP client for processing. + + This method enables MCP servers to request LLM sampling from the connected client. + The client forwards the request to its configured LLM provider (OpenAI, Anthropic, etc.) + and returns the generated response. This is useful for tools that need LLM assistance + to process user requests or generate content. + + The client must support the sampling capability for this method to work. Check + client capabilities using [`check_client_capability`][mcp.server.session.ServerSession.check_client_capability] before calling this method. + + Args: + messages: List of [`SamplingMessage`][mcp.types.SamplingMessage] objects representing the conversation history. + Each message has a role ("user" or "assistant") and content (text, image, or audio). + max_tokens: Maximum number of tokens the LLM should generate in the response. + system_prompt: Optional system message to set the LLM's behavior and context. + include_context: Optional context inclusion preferences for the LLM request. + temperature: Optional sampling temperature (0.0-1.0) controlling response randomness. + Lower values make responses more deterministic. + stop_sequences: Optional list of strings that will cause the LLM to stop generating + when encountered in the response. + metadata: Optional arbitrary metadata to include with the request. + model_preferences: Optional preferences for which model the client should use. + related_request_id: Optional ID linking this request to a parent request for tracing. + + Returns: + CreateMessageResult containing the LLM's response with role, content, model name, + and stop reason information. + + Raises: + RuntimeError: If called before session initialization is complete. + Various exceptions: Depending on client implementation and LLM provider errors. + + Examples: + Basic text generation: + + ```python + from mcp.types import SamplingMessage, TextContent + + result = await session.create_message( + messages=[ + SamplingMessage( + role="user", + content=TextContent(type="text", text="Explain quantum computing") + ) + ], + max_tokens=150 + ) + print(result.content.text) # Generated explanation + ``` + + Multi-turn conversation with system prompt: + + ```python + from mcp.types import SamplingMessage, TextContent + + result = await session.create_message( + messages=[ + SamplingMessage( + role="user", + content=TextContent(type="text", text="What's the weather like?") + ), + SamplingMessage( + role="assistant", + content=TextContent(type="text", text="I don't have access to weather data.") + ), + SamplingMessage( + role="user", + content=TextContent(type="text", text="Then help me write a poem about rain") + ) + ], + max_tokens=100, + system_prompt="You are a helpful poetry assistant.", + temperature=0.8 + ) + ``` + + Note: + This method requires the client to have sampling capability enabled. Most modern + MCP clients support this, but always check capabilities before use in production code. + """ return await self.send_request( request=types.ServerRequest( types.CreateMessageRequest( @@ -261,14 +584,36 @@ async def elicit( requestedSchema: types.ElicitRequestedSchema, related_request_id: types.RequestId | None = None, ) -> types.ElicitResult: - """Send an elicitation/create request. + """Send an elicitation request to collect structured information from the client. + + This is the low-level method for client elicitation. For most use cases, prefer + the higher-level [`Context.elicit`][mcp.server.fastmcp.Context.elicit] method + which provides automatic Pydantic validation and a more convenient interface. + + You typically access this method through the session available in your request + context via [`app.request_context.session`][mcp.shared.context.RequestContext] + within handler functions. Always check that the client supports elicitation using + [`check_client_capability`][mcp.server.session.ServerSession.check_client_capability] + before calling this method. Args: - message: The message to present to the user - requestedSchema: Schema defining the expected response structure + message: The prompt or question to present to the user. + requestedSchema: A [`types.ElicitRequestedSchema`][mcp.types.ElicitRequestedSchema] + defining the expected response structure according to JSON Schema. + related_request_id: Optional `types.RequestId` linking + this elicitation to a specific client request for tracing. Returns: - The client's response + [`types.ElicitResult`][mcp.types.ElicitResult] containing the client's response + and action taken (accept, decline, or cancel). + + Raises: + RuntimeError: If called before session initialization is complete. + Various exceptions: Depending on client implementation and user interaction. + + Note: + Most developers should use [`Context.elicit`][mcp.server.fastmcp.Context.elicit] + instead, which provides Pydantic model validation and better error handling. """ return await self.send_request( types.ServerRequest( diff --git a/src/mcp/shared/context.py b/src/mcp/shared/context.py index f3006e7d5..2e35935aa 100644 --- a/src/mcp/shared/context.py +++ b/src/mcp/shared/context.py @@ -13,6 +13,87 @@ @dataclass class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): + """Context object containing information about the current MCP request. + + This is the fundamental context object in the MCP Python SDK that provides access + to request-scoped information and capabilities. It's created automatically for each + incoming client request and contains everything needed to process that request, + including the session for client communication, request metadata, and any resources + initialized during server startup. + + The RequestContext is available throughout the request lifecycle and provides the + foundation for both low-level and high-level SDK usage patterns. In the low-level + SDK, you access it via [`Server.request_context`][mcp.server.lowlevel.server.Server.request_context]. + In FastMCP, it's wrapped by the more convenient [`Context`][mcp.server.fastmcp.Context] + class that provides the same functionality with additional helper methods. + + ## Request lifecycle + + The RequestContext is created when a client request arrives and destroyed when the + request completes. It's only available during request processing - attempting to + access it outside of a request handler will raise a `LookupError`. + + ## Access patterns + + **Low-level SDK**: Access directly via the server's request_context property: + + ```python + @app.call_tool() + async def my_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + ctx = app.request_context # Get the RequestContext + await ctx.session.send_log_message(level="info", data="Processing...") + ``` + + **FastMCP**: Use the injected Context wrapper instead: + + ```python + @mcp.tool() + async def my_tool(data: str, ctx: Context) -> str: + await ctx.info("Processing...") # Context provides convenience methods + ``` + + ## Lifespan context integration + + Resources initialized during server startup (databases, connections, etc.) are + accessible through the `lifespan_context` attribute, enabling request handlers + to use shared resources safely: + + ```python + # Server startup - initialize shared resources + @asynccontextmanager + async def server_lifespan(server): + db = await Database.connect() + try: + yield {"db": db} + finally: + await db.disconnect() + + # Request handling - access shared resources + @server.call_tool() + async def query_data(name: str, arguments: dict[str, Any]): + ctx = server.request_context + db = ctx.lifespan_context["db"] # Access startup resource + results = await db.query(arguments["query"]) + ``` + + Attributes: + request_id: Unique identifier for the current request as a `RequestId`. + Use this for logging, tracing, or linking related operations. + meta: Optional request metadata including progress tokens and other client-provided + information. May be `None` if no metadata was provided. + session: The [`ServerSession`][mcp.server.session.ServerSession] for communicating + with the client. Use this to send responses, log messages, or check capabilities. + lifespan_context: Application-specific resources initialized during server startup. + Contains any objects yielded by the server's lifespan function. + request: The original request object from the client, if available. May be `None` + for some request types. + + Note: + This object is request-scoped and thread-safe within that scope. Each request + gets its own RequestContext instance. Don't store references to it beyond the + request lifecycle, as it becomes invalid when the request completes. + """ + request_id: RequestId meta: RequestParams.Meta | None session: SessionT diff --git a/src/mcp/shared/exceptions.py b/src/mcp/shared/exceptions.py index 97a1c09a9..eb8999a92 100644 --- a/src/mcp/shared/exceptions.py +++ b/src/mcp/shared/exceptions.py @@ -2,13 +2,24 @@ class McpError(Exception): - """ - Exception type raised when an error arrives over an MCP connection. + """Exception raised when an MCP protocol error is received from a peer. + + This exception is raised when the remote MCP peer returns an error response + instead of a successful result. It wraps the ErrorData received from the peer + and provides access to the error code, message, and any additional data. + + Attributes: + error: The ErrorData object received from the MCP peer containing + error code, message, and optional additional data """ error: ErrorData def __init__(self, error: ErrorData): - """Initialize McpError.""" + """Initialize McpError with error data from the MCP peer. + + Args: + error: ErrorData object containing the error details from the peer + """ super().__init__(error.message) self.error = error diff --git a/src/mcp/types.py b/src/mcp/types.py index 98fefa080..51bd9c91c 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -715,7 +715,90 @@ class AudioContent(BaseModel): class SamplingMessage(BaseModel): - """Describes a message issued to or received from an LLM API.""" + """Represents a message in an LLM conversation for sampling/generation requests. + + SamplingMessage is used to structure conversation history when requesting LLM + text generation through the MCP sampling protocol. Each message represents a + single turn in the conversation with a specific role and content. + + This class is primarily used with [`ServerSession.create_message`][mcp.server.session.ServerSession.create_message] to send + conversation context to LLMs via MCP clients. The message format follows + standard LLM conversation patterns with distinct roles for users and assistants. + + Attributes: + role: The speaker role, either "user" for human input or "assistant" for AI responses. + content: The message content, which can be [`TextContent`][mcp.types.TextContent], + [`ImageContent`][mcp.types.ImageContent], or [`AudioContent`][mcp.types.AudioContent]. + + Examples: + Creating a simple text message: + + ```python + from mcp.types import SamplingMessage, TextContent + + user_msg = SamplingMessage( + role="user", + content=TextContent(type="text", text="Hello, how are you?") + ) + ``` + + Creating an assistant response: + + ```python + assistant_msg = SamplingMessage( + role="assistant", + content=TextContent(type="text", text="I'm doing well, thank you!") + ) + ``` + + Creating a message with image content: + + ```python + import base64 + + # Assuming you have image_bytes containing image data + image_data = base64.b64encode(image_bytes).decode() + + image_msg = SamplingMessage( + role="user", + content=ImageContent( + type="image", + data=image_data, + mimeType="image/jpeg" + ) + ) + ``` + + Building a conversation history: + + ```python + conversation = [ + SamplingMessage( + role="user", + content=TextContent(type="text", text="What's 2+2?") + ), + SamplingMessage( + role="assistant", + content=TextContent(type="text", text="2+2 equals 4.") + ), + SamplingMessage( + role="user", + content=TextContent(type="text", text="Now what's 4+4?") + ) + ] + + # Use in create_message call + result = await session.create_message( + messages=conversation, + max_tokens=50 + ) + ``` + + Note: + The role field is constrained to "user" or "assistant" only. The content + supports multiple media types, but actual support depends on the LLM provider + and client implementation. + """ role: Role content: TextContent | ImageContent | AudioContent diff --git a/uv.lock b/uv.lock index 59192bee0..b8802eeba 100644 --- a/uv.lock +++ b/uv.lock @@ -619,6 +619,7 @@ dev = [ ] docs = [ { name = "mkdocs" }, + { name = "mkdocs-api-autonav" }, { name = "mkdocs-glightbox" }, { name = "mkdocs-material", extra = ["imaging"] }, { name = "mkdocstrings-python" }, @@ -659,6 +660,7 @@ dev = [ ] docs = [ { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-api-autonav", specifier = ">=0.3.1" }, { name = "mkdocs-glightbox", specifier = ">=0.4.0" }, { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5.45" }, { name = "mkdocstrings-python", specifier = ">=1.12.2" }, @@ -931,6 +933,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] +[[package]] +name = "mkdocs-api-autonav" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "mkdocstrings-python" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/9f/c73e0b79b9be34f3dd975e7ba175ef6397a986f470f9aafac491d53699f8/mkdocs_api_autonav-0.3.1.tar.gz", hash = "sha256:5d37ad53a03600acff0f7d67fad122a38800d172777d3c4f8c0dfbb9b58e8c29", size = 15980, upload-time = "2025-08-08T04:08:50.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/60/5acc016c75cac9758eff0cbf032d2504c8baca701d5ea4a784932e4764af/mkdocs_api_autonav-0.3.1-py3-none-any.whl", hash = "sha256:363cdf24ec12670971049291b72806ee55ae6560611ffd6ed2fdeb69c43e6d4f", size = 12033, upload-time = "2025-08-08T04:08:48.349Z" }, +] + [[package]] name = "mkdocs-autorefs" version = "1.4.2"