Merge branch 'main' into feature/simple-chatbot-example
This commit is contained in:
commit
eba1ad2772
62 changed files with 6710 additions and 774 deletions
15
.pre-commit-config.yaml
Normal file
15
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
fail_fast: true
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [yaml, json5]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
483
README.md
483
README.md
|
|
@ -1,4 +1,9 @@
|
|||
# MCP Python SDK
|
||||
|
||||
<div align="center">
|
||||
|
||||
<strong>Python implementation of the Model Context Protocol (MCP)</strong>
|
||||
|
||||
[![PyPI][pypi-badge]][pypi-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Python Version][python-badge]][python-url]
|
||||
|
|
@ -6,6 +11,38 @@
|
|||
[![Specification][spec-badge]][spec-url]
|
||||
[![GitHub Discussions][discussions-badge]][discussions-url]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- omit in toc -->
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Installation](#installation)
|
||||
- [Quickstart](#quickstart)
|
||||
- [What is MCP?](#what-is-mcp)
|
||||
- [Core Concepts](#core-concepts)
|
||||
- [Server](#server)
|
||||
- [Resources](#resources)
|
||||
- [Tools](#tools)
|
||||
- [Prompts](#prompts)
|
||||
- [Images](#images)
|
||||
- [Context](#context)
|
||||
- [Running Your Server](#running-your-server)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Claude Desktop Integration](#claude-desktop-integration)
|
||||
- [Direct Execution](#direct-execution)
|
||||
- [Examples](#examples)
|
||||
- [Echo Server](#echo-server)
|
||||
- [SQLite Explorer](#sqlite-explorer)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Low-Level Server](#low-level-server)
|
||||
- [Writing MCP Clients](#writing-mcp-clients)
|
||||
- [MCP Primitives](#mcp-primitives)
|
||||
- [Server Capabilities](#server-capabilities)
|
||||
- [Documentation](#documentation)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg
|
||||
[pypi-url]: https://pypi.org/project/mcp/
|
||||
[mit-badge]: https://img.shields.io/pypi/l/mcp.svg
|
||||
|
|
@ -19,8 +56,6 @@
|
|||
[discussions-badge]: https://img.shields.io/github/discussions/modelcontextprotocol/python-sdk
|
||||
[discussions-url]: https://github.com/modelcontextprotocol/python-sdk/discussions
|
||||
|
||||
Python implementation of the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), providing both client and server capabilities for integrating with LLM surfaces.
|
||||
|
||||
## Overview
|
||||
|
||||
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to:
|
||||
|
|
@ -32,58 +67,277 @@ The Model Context Protocol allows applications to provide context for LLMs in a
|
|||
|
||||
## Installation
|
||||
|
||||
We recommend the use of [uv](https://docs.astral.sh/uv/) to manage your Python projects:
|
||||
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects:
|
||||
|
||||
```bash
|
||||
uv add mcp
|
||||
uv add "mcp[cli]"
|
||||
```
|
||||
|
||||
Alternatively, add mcp to your `requirements.txt`:
|
||||
```
|
||||
Alternatively:
|
||||
```bash
|
||||
pip install mcp
|
||||
# or add to requirements.txt
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Overview
|
||||
MCP servers provide focused functionality like resources, tools, prompts, and other capabilities that can be reused across many client applications. These servers are designed to be easy to build, highly composable, and modular.
|
||||
## Quickstart
|
||||
|
||||
### Key design principles
|
||||
- Servers are extremely easy to build with clear, simple interfaces
|
||||
- Multiple servers can be composed seamlessly through a shared protocol
|
||||
- Each server operates in isolation and cannot access conversation context
|
||||
- Features can be added progressively through capability negotiation
|
||||
|
||||
### Server provided primitives
|
||||
- [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts): Templatable text
|
||||
- [Resources](https://modelcontextprotocol.io/docs/concepts/resources): File-like attachments
|
||||
- [Tools](https://modelcontextprotocol.io/docs/concepts/tools): Functions that models can call
|
||||
- Utilities:
|
||||
- Completion: Auto-completion provider for prompt arguments or resource URI templates
|
||||
- Logging: Logging to the client
|
||||
- Pagination*: Pagination for long results
|
||||
|
||||
### Client provided primitives
|
||||
- [Sampling](https://modelcontextprotocol.io/docs/concepts/sampling): Allow servers to sample using client models
|
||||
- Roots: Information about locations to operate on (e.g., directories)
|
||||
|
||||
Connections between clients and servers are established through transports like **stdio** or **SSE** (Note that most clients support stdio, but not SSE at the moment). The transport layer handles message framing, delivery, and error handling.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating a Server
|
||||
|
||||
MCP servers follow a decorator approach to register handlers for MCP primitives like resources, prompts, and tools. The goal is to provide a simple interface for exposing capabilities to LLM clients.
|
||||
|
||||
**example_server.py**
|
||||
Let's create a simple MCP server that exposes a calculator tool and some data:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "mcp"
|
||||
# ]
|
||||
# ///
|
||||
from mcp.server import Server, NotificationOptions
|
||||
# server.py
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create an MCP server
|
||||
mcp = FastMCP("Demo")
|
||||
|
||||
# Add an addition tool
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
# Add a dynamic greeting resource
|
||||
@mcp.resource("greeting://{name}")
|
||||
def get_greeting(name: str) -> str:
|
||||
"""Get a personalized greeting"""
|
||||
return f"Hello, {name}!"
|
||||
```
|
||||
|
||||
You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running:
|
||||
```bash
|
||||
mcp install server.py
|
||||
```
|
||||
|
||||
Alternatively, you can test it with the MCP Inspector:
|
||||
```bash
|
||||
mcp dev server.py
|
||||
```
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
|
||||
|
||||
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
|
||||
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
|
||||
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
|
||||
- And more!
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Server
|
||||
|
||||
The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create a named server
|
||||
mcp = FastMCP("My App")
|
||||
|
||||
# Specify dependencies for deployment and development
|
||||
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:
|
||||
|
||||
```python
|
||||
@mcp.resource("config://app")
|
||||
def get_config() -> str:
|
||||
"""Static configuration data"""
|
||||
return "App configuration here"
|
||||
|
||||
@mcp.resource("users://{user_id}/profile")
|
||||
def get_user_profile(user_id: str) -> str:
|
||||
"""Dynamic user data"""
|
||||
return f"Profile data for user {user_id}"
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
def calculate_bmi(weight_kg: float, height_m: float) -> float:
|
||||
"""Calculate BMI given weight in kg and height in meters"""
|
||||
return weight_kg / (height_m ** 2)
|
||||
|
||||
@mcp.tool()
|
||||
async def fetch_weather(city: str) -> str:
|
||||
"""Fetch current weather for a city"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"https://api.weather.com/{city}")
|
||||
return response.text
|
||||
```
|
||||
|
||||
### Prompts
|
||||
|
||||
Prompts are reusable templates that help LLMs interact with your server effectively:
|
||||
|
||||
```python
|
||||
@mcp.prompt()
|
||||
def review_code(code: str) -> str:
|
||||
return f"Please review this code:\n\n{code}"
|
||||
|
||||
@mcp.prompt()
|
||||
def debug_error(error: str) -> list[Message]:
|
||||
return [
|
||||
UserMessage("I'm seeing this error:"),
|
||||
UserMessage(error),
|
||||
AssistantMessage("I'll help debug that. What have you tried so far?")
|
||||
]
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
FastMCP provides an `Image` class that automatically handles image data:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP, Image
|
||||
from PIL import Image as PILImage
|
||||
|
||||
@mcp.tool()
|
||||
def create_thumbnail(image_path: str) -> Image:
|
||||
"""Create a thumbnail from an image"""
|
||||
img = PILImage.open(image_path)
|
||||
img.thumbnail((100, 100))
|
||||
return Image(data=img.tobytes(), format="png")
|
||||
```
|
||||
|
||||
### Context
|
||||
|
||||
The Context object gives your tools and resources access to MCP capabilities:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
|
||||
@mcp.tool()
|
||||
async def long_task(files: list[str], ctx: Context) -> str:
|
||||
"""Process multiple files with progress tracking"""
|
||||
for i, file in enumerate(files):
|
||||
ctx.info(f"Processing {file}")
|
||||
await ctx.report_progress(i, len(files))
|
||||
data = await ctx.read_resource(f"file://{file}")
|
||||
return "Processing complete"
|
||||
```
|
||||
|
||||
## Running Your Server
|
||||
|
||||
### Development Mode
|
||||
|
||||
The fastest way to test and debug your server is with the MCP Inspector:
|
||||
|
||||
```bash
|
||||
mcp dev server.py
|
||||
|
||||
# Add dependencies
|
||||
mcp dev server.py --with pandas --with numpy
|
||||
|
||||
# Mount local code
|
||||
mcp dev server.py --with-editable .
|
||||
```
|
||||
|
||||
### Claude Desktop Integration
|
||||
|
||||
Once your server is ready, install it in Claude Desktop:
|
||||
|
||||
```bash
|
||||
mcp install server.py
|
||||
|
||||
# Custom name
|
||||
mcp install server.py --name "My Analytics Server"
|
||||
|
||||
# Environment variables
|
||||
mcp install server.py -e API_KEY=abc123 -e DB_URL=postgres://...
|
||||
mcp install server.py -f .env
|
||||
```
|
||||
|
||||
### Direct Execution
|
||||
|
||||
For advanced scenarios like custom deployments:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("My App")
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
Run it with:
|
||||
```bash
|
||||
python server.py
|
||||
# or
|
||||
mcp run server.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Echo Server
|
||||
|
||||
A simple server demonstrating resources, tools, and prompts:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Echo")
|
||||
|
||||
@mcp.resource("echo://{message}")
|
||||
def echo_resource(message: str) -> str:
|
||||
"""Echo a message as a resource"""
|
||||
return f"Resource echo: {message}"
|
||||
|
||||
@mcp.tool()
|
||||
def echo_tool(message: str) -> str:
|
||||
"""Echo a message as a tool"""
|
||||
return f"Tool echo: {message}"
|
||||
|
||||
@mcp.prompt()
|
||||
def echo_prompt(message: str) -> str:
|
||||
"""Create an echo prompt"""
|
||||
return f"Please process this message: {message}"
|
||||
```
|
||||
|
||||
### SQLite Explorer
|
||||
|
||||
A more complex example showing database integration:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
import sqlite3
|
||||
|
||||
mcp = FastMCP("SQLite Explorer")
|
||||
|
||||
@mcp.resource("schema://main")
|
||||
def get_schema() -> str:
|
||||
"""Provide the database schema as a resource"""
|
||||
conn = sqlite3.connect("database.db")
|
||||
schema = conn.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table'"
|
||||
).fetchall()
|
||||
return "\n".join(sql[0] for sql in schema if sql[0])
|
||||
|
||||
@mcp.tool()
|
||||
def query_data(sql: str) -> str:
|
||||
"""Execute SQL queries safely"""
|
||||
conn = sqlite3.connect("database.db")
|
||||
try:
|
||||
result = conn.execute(sql).fetchall()
|
||||
return "\n".join(str(row) for row in result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Low-Level Server
|
||||
|
||||
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server:
|
||||
|
||||
```python
|
||||
from mcp.server.lowlevel import Server, NotificationOptions
|
||||
from mcp.server.models import InitializationOptions
|
||||
import mcp.server.stdio
|
||||
import mcp.types as types
|
||||
|
|
@ -91,7 +345,6 @@ import mcp.types as types
|
|||
# Create a server instance
|
||||
server = Server("example-server")
|
||||
|
||||
# Add prompt capabilities
|
||||
@server.list_prompts()
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
return [
|
||||
|
|
@ -130,7 +383,6 @@ async def handle_get_prompt(
|
|||
)
|
||||
|
||||
async def run():
|
||||
# Run the server as STDIO
|
||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
|
|
@ -150,9 +402,9 @@ if __name__ == "__main__":
|
|||
asyncio.run(run())
|
||||
```
|
||||
|
||||
### Creating a Client
|
||||
### Writing MCP Clients
|
||||
|
||||
**example_client.py**
|
||||
The SDK provides a high-level client interface for connecting to MCP servers:
|
||||
|
||||
```python
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
|
|
@ -171,17 +423,12 @@ async def run():
|
|||
# Initialize the connection
|
||||
await session.initialize()
|
||||
|
||||
# The example server only supports prompt primitives:
|
||||
|
||||
# List available prompts
|
||||
prompts = await session.list_prompts()
|
||||
|
||||
# Get a prompt
|
||||
prompt = await session.get_prompt("example-prompt", arguments={"arg1": "value"})
|
||||
|
||||
"""
|
||||
Other example calls include:
|
||||
|
||||
# List available resources
|
||||
resources = await session.list_resources()
|
||||
|
||||
|
|
@ -193,16 +440,15 @@ async def run():
|
|||
|
||||
# Call a tool
|
||||
result = await session.call_tool("tool-name", arguments={"arg1": "value"})
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(run())
|
||||
```
|
||||
|
||||
## Primitives
|
||||
### MCP Primitives
|
||||
|
||||
The MCP Python SDK provides decorators that map to the core protocol primitives. Each primitive follows a different interaction pattern based on how it is controlled and used:
|
||||
The MCP protocol defines three core primitives that servers can implement:
|
||||
|
||||
| Primitive | Control | Description | Example Use |
|
||||
|-----------|-----------------------|-----------------------------------------------------|------------------------------|
|
||||
|
|
@ -210,122 +456,17 @@ The MCP Python SDK provides decorators that map to the core protocol primitives.
|
|||
| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses |
|
||||
| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates |
|
||||
|
||||
### User-Controlled Primitives
|
||||
### Server Capabilities
|
||||
|
||||
**Prompts** are designed to be explicitly selected by users for their interactions with LLMs.
|
||||
MCP servers declare capabilities during initialization:
|
||||
|
||||
| Decorator | Description |
|
||||
|--------------------------|----------------------------------------|
|
||||
| `@server.list_prompts()` | List available prompt templates |
|
||||
| `@server.get_prompt()` | Get a specific prompt with arguments |
|
||||
|
||||
### Application-Controlled Primitives
|
||||
|
||||
**Resources** are controlled by the client application, which decides how and when they should be used based on its own logic.
|
||||
|
||||
| Decorator | Description |
|
||||
|--------------------------------|---------------------------------------|
|
||||
| `@server.list_resources()` | List available resources |
|
||||
| `@server.read_resource()` | Read a specific resource's content |
|
||||
| `@server.subscribe_resource()` | Subscribe to resource updates |
|
||||
|
||||
### Model-Controlled Primitives
|
||||
|
||||
**Tools** are exposed to LLMs to enable automated actions, with user approval.
|
||||
|
||||
| Decorator | Description |
|
||||
|------------------------|------------------------------------|
|
||||
| `@server.list_tools()` | List available tools |
|
||||
| `@server.call_tool()` | Execute a tool with arguments |
|
||||
|
||||
### Server Management
|
||||
|
||||
Additional decorators for server functionality:
|
||||
|
||||
| Decorator | Description |
|
||||
|-------------------------------|--------------------------------|
|
||||
| `@server.set_logging_level()` | Update server logging level |
|
||||
|
||||
### Capabilities
|
||||
|
||||
MCP servers declare capabilities during initialization. These map to specific decorators:
|
||||
|
||||
| Capability | Feature Flag | Decorators | Description |
|
||||
|-------------|------------------------------|-----------------------------------------------------------------|-------------------------------------|
|
||||
| `prompts` | `listChanged` | `@list_prompts`<br/>`@get_prompt` | Prompt template management |
|
||||
| `resources` | `subscribe`<br/>`listChanged`| `@list_resources`<br/>`@read_resource`<br/>`@subscribe_resource`| Resource exposure and updates |
|
||||
| `tools` | `listChanged` | `@list_tools`<br/>`@call_tool` | Tool discovery and execution |
|
||||
| `logging` | - | `@set_logging_level` | Server logging configuration |
|
||||
| `completion`| - | `@complete_argument` | Argument completion suggestions |
|
||||
|
||||
Capabilities are negotiated during connection initialization. Servers only need to implement the decorators for capabilities they support.
|
||||
|
||||
## Client Interaction
|
||||
|
||||
The MCP Python SDK enables servers to interact with clients through request context and session management. This allows servers to perform operations like LLM sampling and progress tracking.
|
||||
|
||||
### Request Context
|
||||
|
||||
The Request Context provides access to the current request and client session. It can be accessed through `server.request_context` and enables:
|
||||
|
||||
- Sampling from the client's LLM
|
||||
- Sending progress updates
|
||||
- Logging messages
|
||||
- Accessing request metadata
|
||||
|
||||
Example using request context for LLM sampling:
|
||||
|
||||
```python
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
||||
# Access the current request context
|
||||
context = server.request_context
|
||||
|
||||
# Use the session to sample from the client's LLM
|
||||
result = await context.session.create_message(
|
||||
messages=[
|
||||
types.SamplingMessage(
|
||||
role="user",
|
||||
content=types.TextContent(
|
||||
type="text",
|
||||
text="Analyze this data: " + json.dumps(arguments)
|
||||
)
|
||||
)
|
||||
],
|
||||
max_tokens=100
|
||||
)
|
||||
|
||||
return [types.TextContent(type="text", text=result.content.text)]
|
||||
```
|
||||
|
||||
Using request context for progress updates:
|
||||
|
||||
```python
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
||||
context = server.request_context
|
||||
|
||||
if progress_token := context.meta.progressToken:
|
||||
# Send progress notifications
|
||||
await context.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=0.5,
|
||||
total=1.0
|
||||
)
|
||||
|
||||
# Perform operation...
|
||||
|
||||
if progress_token:
|
||||
await context.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=1.0,
|
||||
total=1.0
|
||||
)
|
||||
|
||||
return [types.TextContent(type="text", text="Operation complete")]
|
||||
```
|
||||
|
||||
The request context is automatically set for each request and provides a safe way to access the current client session and request metadata.
|
||||
| Capability | Feature Flag | Description |
|
||||
|-------------|------------------------------|------------------------------------|
|
||||
| `prompts` | `listChanged` | Prompt template management |
|
||||
| `resources` | `subscribe`<br/>`listChanged`| Resource exposure and updates |
|
||||
| `tools` | `listChanged` | Tool discovery and execution |
|
||||
| `logging` | - | Server logging configuration |
|
||||
| `completion`| - | Argument completion suggestions |
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
@ -339,4 +480,4 @@ We are passionate about supporting contributors of all levels of experience and
|
|||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
30
examples/fastmcp/complex_inputs.py
Normal file
30
examples/fastmcp/complex_inputs.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
FastMCP Complex inputs Example
|
||||
|
||||
Demonstrates validation via pydantic with complex models.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Shrimp Tank")
|
||||
|
||||
|
||||
class ShrimpTank(BaseModel):
|
||||
class Shrimp(BaseModel):
|
||||
name: Annotated[str, Field(max_length=10)]
|
||||
|
||||
shrimp: list[Shrimp]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def name_shrimp(
|
||||
tank: ShrimpTank,
|
||||
# You can use pydantic Field in function signatures for validation.
|
||||
extra_names: Annotated[list[str], Field(max_length=10)],
|
||||
) -> list[str]:
|
||||
"""List all shrimp names in the tank"""
|
||||
return [shrimp.name for shrimp in tank.shrimp] + extra_names
|
||||
25
examples/fastmcp/desktop.py
Normal file
25
examples/fastmcp/desktop.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
FastMCP Desktop Example
|
||||
|
||||
A simple example that exposes the desktop directory as a resource.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Demo")
|
||||
|
||||
|
||||
@mcp.resource("dir://desktop")
|
||||
def desktop() -> list[str]:
|
||||
"""List the files in the user's desktop"""
|
||||
desktop = Path.home() / "Desktop"
|
||||
return [str(f) for f in desktop.iterdir()]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
30
examples/fastmcp/echo.py
Normal file
30
examples/fastmcp/echo.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
FastMCP Echo Server
|
||||
"""
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def echo_tool(text: str) -> str:
|
||||
"""Echo the input text"""
|
||||
return text
|
||||
|
||||
|
||||
@mcp.resource("echo://static")
|
||||
def echo_resource() -> str:
|
||||
return "Echo!"
|
||||
|
||||
|
||||
@mcp.resource("echo://{text}")
|
||||
def echo_template(text: str) -> str:
|
||||
"""Echo the input text"""
|
||||
return f"Echo: {text}"
|
||||
|
||||
|
||||
@mcp.prompt("echo")
|
||||
def echo_prompt(text: str) -> str:
|
||||
return text
|
||||
349
examples/fastmcp/memory.py
Normal file
349
examples/fastmcp/memory.py
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# /// script
|
||||
# dependencies = ["pydantic-ai-slim[openai]", "asyncpg", "numpy", "pgvector"]
|
||||
# ///
|
||||
|
||||
# uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector
|
||||
|
||||
"""
|
||||
Recursive memory system inspired by the human brain's clustering of memories.
|
||||
Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient
|
||||
similarity search.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Self
|
||||
|
||||
import asyncpg
|
||||
import numpy as np
|
||||
from openai import AsyncOpenAI
|
||||
from pgvector.asyncpg import register_vector # Import register_vector
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_ai import Agent
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
MAX_DEPTH = 5
|
||||
SIMILARITY_THRESHOLD = 0.7
|
||||
DECAY_FACTOR = 0.99
|
||||
REINFORCEMENT_FACTOR = 1.1
|
||||
|
||||
DEFAULT_LLM_MODEL = "openai:gpt-4o"
|
||||
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
|
||||
mcp = FastMCP(
|
||||
"memory",
|
||||
dependencies=[
|
||||
"pydantic-ai-slim[openai]",
|
||||
"asyncpg",
|
||||
"numpy",
|
||||
"pgvector",
|
||||
],
|
||||
)
|
||||
|
||||
DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db"
|
||||
# reset memory with rm ~/.fastmcp/{USER}/memory/*
|
||||
PROFILE_DIR = (
|
||||
Path.home() / ".fastmcp" / os.environ.get("USER", "anon") / "memory"
|
||||
).resolve()
|
||||
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def cosine_similarity(a: list[float], b: list[float]) -> float:
|
||||
a_array = np.array(a, dtype=np.float64)
|
||||
b_array = np.array(b, dtype=np.float64)
|
||||
return np.dot(a_array, b_array) / (
|
||||
np.linalg.norm(a_array) * np.linalg.norm(b_array)
|
||||
)
|
||||
|
||||
|
||||
async def do_ai[T](
|
||||
user_prompt: str,
|
||||
system_prompt: str,
|
||||
result_type: type[T] | Annotated,
|
||||
deps=None,
|
||||
) -> T:
|
||||
agent = Agent(
|
||||
DEFAULT_LLM_MODEL,
|
||||
system_prompt=system_prompt,
|
||||
result_type=result_type,
|
||||
)
|
||||
result = await agent.run(user_prompt, deps=deps)
|
||||
return result.data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
openai: AsyncOpenAI
|
||||
pool: asyncpg.Pool
|
||||
|
||||
|
||||
async def get_db_pool() -> asyncpg.Pool:
|
||||
async def init(conn):
|
||||
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
||||
await register_vector(conn)
|
||||
|
||||
pool = await asyncpg.create_pool(DB_DSN, init=init)
|
||||
return pool
|
||||
|
||||
|
||||
class MemoryNode(BaseModel):
|
||||
id: int | None = None
|
||||
content: str
|
||||
summary: str = ""
|
||||
importance: float = 1.0
|
||||
access_count: int = 0
|
||||
timestamp: float = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).timestamp()
|
||||
)
|
||||
embedding: list[float]
|
||||
|
||||
@classmethod
|
||||
async def from_content(cls, content: str, deps: Deps):
|
||||
embedding = await get_embedding(content, deps)
|
||||
return cls(content=content, embedding=embedding)
|
||||
|
||||
async def save(self, deps: Deps):
|
||||
async with deps.pool.acquire() as conn:
|
||||
if self.id is None:
|
||||
result = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO memories (content, summary, importance, access_count,
|
||||
timestamp, embedding)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
""",
|
||||
self.content,
|
||||
self.summary,
|
||||
self.importance,
|
||||
self.access_count,
|
||||
self.timestamp,
|
||||
self.embedding,
|
||||
)
|
||||
self.id = result["id"]
|
||||
else:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE memories
|
||||
SET content = $1, summary = $2, importance = $3,
|
||||
access_count = $4, timestamp = $5, embedding = $6
|
||||
WHERE id = $7
|
||||
""",
|
||||
self.content,
|
||||
self.summary,
|
||||
self.importance,
|
||||
self.access_count,
|
||||
self.timestamp,
|
||||
self.embedding,
|
||||
self.id,
|
||||
)
|
||||
|
||||
async def merge_with(self, other: Self, deps: Deps):
|
||||
self.content = await do_ai(
|
||||
f"{self.content}\n\n{other.content}",
|
||||
"Combine the following two texts into a single, coherent text.",
|
||||
str,
|
||||
deps,
|
||||
)
|
||||
self.importance += other.importance
|
||||
self.access_count += other.access_count
|
||||
self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)]
|
||||
self.summary = await do_ai(
|
||||
self.content, "Summarize the following text concisely.", str, deps
|
||||
)
|
||||
await self.save(deps)
|
||||
# Delete the merged node from the database
|
||||
if other.id is not None:
|
||||
await delete_memory(other.id, deps)
|
||||
|
||||
def get_effective_importance(self):
|
||||
return self.importance * (1 + math.log(self.access_count + 1))
|
||||
|
||||
|
||||
async def get_embedding(text: str, deps: Deps) -> list[float]:
|
||||
embedding_response = await deps.openai.embeddings.create(
|
||||
input=text,
|
||||
model=DEFAULT_EMBEDDING_MODEL,
|
||||
)
|
||||
return embedding_response.data[0].embedding
|
||||
|
||||
|
||||
async def delete_memory(memory_id: int, deps: Deps):
|
||||
async with deps.pool.acquire() as conn:
|
||||
await conn.execute("DELETE FROM memories WHERE id = $1", memory_id)
|
||||
|
||||
|
||||
async def add_memory(content: str, deps: Deps):
|
||||
new_memory = await MemoryNode.from_content(content, deps)
|
||||
await new_memory.save(deps)
|
||||
|
||||
similar_memories = await find_similar_memories(new_memory.embedding, deps)
|
||||
for memory in similar_memories:
|
||||
if memory.id != new_memory.id:
|
||||
await new_memory.merge_with(memory, deps)
|
||||
|
||||
await update_importance(new_memory.embedding, deps)
|
||||
|
||||
await prune_memories(deps)
|
||||
|
||||
return f"Remembered: {content}"
|
||||
|
||||
|
||||
async def find_similar_memories(embedding: list[float], deps: Deps) -> list[MemoryNode]:
|
||||
async with deps.pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT id, content, summary, importance, access_count, timestamp, embedding
|
||||
FROM memories
|
||||
ORDER BY embedding <-> $1
|
||||
LIMIT 5
|
||||
""",
|
||||
embedding,
|
||||
)
|
||||
memories = [
|
||||
MemoryNode(
|
||||
id=row["id"],
|
||||
content=row["content"],
|
||||
summary=row["summary"],
|
||||
importance=row["importance"],
|
||||
access_count=row["access_count"],
|
||||
timestamp=row["timestamp"],
|
||||
embedding=row["embedding"],
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return memories
|
||||
|
||||
|
||||
async def update_importance(user_embedding: list[float], deps: Deps):
|
||||
async with deps.pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"SELECT id, importance, access_count, embedding FROM memories"
|
||||
)
|
||||
for row in rows:
|
||||
memory_embedding = row["embedding"]
|
||||
similarity = cosine_similarity(user_embedding, memory_embedding)
|
||||
if similarity > SIMILARITY_THRESHOLD:
|
||||
new_importance = row["importance"] * REINFORCEMENT_FACTOR
|
||||
new_access_count = row["access_count"] + 1
|
||||
else:
|
||||
new_importance = row["importance"] * DECAY_FACTOR
|
||||
new_access_count = row["access_count"]
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE memories
|
||||
SET importance = $1, access_count = $2
|
||||
WHERE id = $3
|
||||
""",
|
||||
new_importance,
|
||||
new_access_count,
|
||||
row["id"],
|
||||
)
|
||||
|
||||
|
||||
async def prune_memories(deps: Deps):
|
||||
async with deps.pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT id, importance, access_count
|
||||
FROM memories
|
||||
ORDER BY importance DESC
|
||||
OFFSET $1
|
||||
""",
|
||||
MAX_DEPTH,
|
||||
)
|
||||
for row in rows:
|
||||
await conn.execute("DELETE FROM memories WHERE id = $1", row["id"])
|
||||
|
||||
|
||||
async def display_memory_tree(deps: Deps) -> str:
|
||||
async with deps.pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT content, summary, importance, access_count
|
||||
FROM memories
|
||||
ORDER BY importance DESC
|
||||
LIMIT $1
|
||||
""",
|
||||
MAX_DEPTH,
|
||||
)
|
||||
result = ""
|
||||
for row in rows:
|
||||
effective_importance = row["importance"] * (
|
||||
1 + math.log(row["access_count"] + 1)
|
||||
)
|
||||
summary = row["summary"] or row["content"]
|
||||
result += f"- {summary} (Importance: {effective_importance:.2f})\n"
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def remember(
|
||||
contents: list[str] = Field(
|
||||
description="List of observations or memories to store"
|
||||
),
|
||||
):
|
||||
deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
|
||||
try:
|
||||
return "\n".join(
|
||||
await asyncio.gather(*[add_memory(content, deps) for content in contents])
|
||||
)
|
||||
finally:
|
||||
await deps.pool.close()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def read_profile() -> str:
|
||||
deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
|
||||
profile = await display_memory_tree(deps)
|
||||
await deps.pool.close()
|
||||
return profile
|
||||
|
||||
|
||||
async def initialize_database():
|
||||
pool = await asyncpg.create_pool(
|
||||
"postgresql://postgres:postgres@localhost:54320/postgres"
|
||||
)
|
||||
try:
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE pg_stat_activity.datname = 'memory_db'
|
||||
AND pid <> pg_backend_pid();
|
||||
""")
|
||||
await conn.execute("DROP DATABASE IF EXISTS memory_db;")
|
||||
await conn.execute("CREATE DATABASE memory_db;")
|
||||
finally:
|
||||
await pool.close()
|
||||
|
||||
pool = await asyncpg.create_pool(DB_DSN)
|
||||
try:
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
||||
|
||||
await register_vector(conn)
|
||||
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
importance REAL NOT NULL,
|
||||
access_count INT NOT NULL,
|
||||
timestamp DOUBLE PRECISION NOT NULL,
|
||||
embedding vector(1536) NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories
|
||||
USING hnsw (embedding vector_l2_ops);
|
||||
""")
|
||||
finally:
|
||||
await pool.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(initialize_database())
|
||||
18
examples/fastmcp/readme-quickstart.py
Normal file
18
examples/fastmcp/readme-quickstart.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create an MCP server
|
||||
mcp = FastMCP("Demo")
|
||||
|
||||
|
||||
# Add an addition tool
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
|
||||
# Add a dynamic greeting resource
|
||||
@mcp.resource("greeting://{name}")
|
||||
def get_greeting(name: str) -> str:
|
||||
"""Get a personalized greeting"""
|
||||
return f"Hello, {name}!"
|
||||
29
examples/fastmcp/screenshot.py
Normal file
29
examples/fastmcp/screenshot.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""
|
||||
FastMCP Screenshot Example
|
||||
|
||||
Give Claude a tool to capture and view screenshots.
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.fastmcp.utilities.types import Image
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def take_screenshot() -> Image:
|
||||
"""
|
||||
Take a screenshot of the user's screen and return it as an image. Use
|
||||
this tool anytime the user wants you to look at something they're doing.
|
||||
"""
|
||||
import pyautogui
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# if the file exceeds ~1MB, it will be rejected by Claude
|
||||
screenshot = pyautogui.screenshot()
|
||||
screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
|
||||
return Image(data=buffer.getvalue(), format="jpeg")
|
||||
14
examples/fastmcp/simple_echo.py
Normal file
14
examples/fastmcp/simple_echo.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""
|
||||
FastMCP Echo Server
|
||||
"""
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def echo(text: str) -> str:
|
||||
"""Echo the input text"""
|
||||
return text
|
||||
72
examples/fastmcp/text_me.py
Normal file
72
examples/fastmcp/text_me.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# /// script
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
"""
|
||||
FastMCP Text Me Server
|
||||
--------------------------------
|
||||
This defines a simple FastMCP server that sends a text message to a phone number via https://surgemsg.com/.
|
||||
|
||||
To run this example, create a `.env` file with the following values:
|
||||
|
||||
SURGE_API_KEY=...
|
||||
SURGE_ACCOUNT_ID=...
|
||||
SURGE_MY_PHONE_NUMBER=...
|
||||
SURGE_MY_FIRST_NAME=...
|
||||
SURGE_MY_LAST_NAME=...
|
||||
|
||||
Visit https://surgemsg.com/ and click "Get Started" to obtain these values.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import httpx
|
||||
from pydantic import BeforeValidator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
class SurgeSettings(BaseSettings):
|
||||
model_config: SettingsConfigDict = SettingsConfigDict(
|
||||
env_prefix="SURGE_", env_file=".env"
|
||||
)
|
||||
|
||||
api_key: str
|
||||
account_id: str
|
||||
my_phone_number: Annotated[
|
||||
str, BeforeValidator(lambda v: "+" + v if not v.startswith("+") else v)
|
||||
]
|
||||
my_first_name: str
|
||||
my_last_name: str
|
||||
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Text me")
|
||||
surge_settings = SurgeSettings() # type: ignore
|
||||
|
||||
|
||||
@mcp.tool(name="textme", description="Send a text message to me")
|
||||
def text_me(text_content: str) -> str:
|
||||
"""Send a text message to a phone number via https://surgemsg.com/"""
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
"https://api.surgemsg.com/messages",
|
||||
headers={
|
||||
"Authorization": f"Bearer {surge_settings.api_key}",
|
||||
"Surge-Account": surge_settings.account_id,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"body": text_content,
|
||||
"conversation": {
|
||||
"contact": {
|
||||
"first_name": surge_settings.my_first_name,
|
||||
"last_name": surge_settings.my_last_name,
|
||||
"phone_number": surge_settings.my_phone_number,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return f"Message sent: {text_content}"
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import anyio
|
||||
import click
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
from mcp.server.lowlevel import Server
|
||||
|
||||
|
||||
def create_messages(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import anyio
|
||||
import click
|
||||
import mcp.types as types
|
||||
from mcp.server import AnyUrl, Server
|
||||
from mcp.server.lowlevel import Server
|
||||
from pydantic import AnyUrl
|
||||
|
||||
SAMPLE_RESOURCES = {
|
||||
"greeting": "Hello! This is a sample text resource.",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import anyio
|
|||
import click
|
||||
import httpx
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
from mcp.server.lowlevel import Server
|
||||
|
||||
|
||||
async def fetch_website(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mcp"
|
||||
version = "1.1.2.dev0"
|
||||
version = "1.2.0.dev0"
|
||||
description = "Model Context Protocol SDK"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
|
@ -29,11 +25,35 @@ dependencies = [
|
|||
"anyio>=4.5",
|
||||
"httpx>=0.27",
|
||||
"httpx-sse>=0.4",
|
||||
"pydantic>=2.7.2",
|
||||
"pydantic>=2.10.1,<3.0.0",
|
||||
"starlette>=0.27",
|
||||
"sse-starlette>=1.6.1",
|
||||
"pydantic-settings>=2.6.1",
|
||||
"uvicorn>=0.30",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
rich = ["rich>=13.9.4"]
|
||||
cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"]
|
||||
|
||||
[project.scripts]
|
||||
mcp = "mcp.cli:app [cli]"
|
||||
|
||||
[tool.uv]
|
||||
resolution = "lowest-direct"
|
||||
dev-dependencies = [
|
||||
"pyright>=1.1.378",
|
||||
"pytest>=8.3.3",
|
||||
"ruff>=0.8.1",
|
||||
"trio>=0.26.2",
|
||||
"pytest-flakefinder>=1.1.0",
|
||||
"pytest-xdist>=3.6.1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://modelcontextprotocol.io"
|
||||
Repository = "https://github.com/modelcontextprotocol/python-sdk"
|
||||
|
|
@ -57,15 +77,7 @@ target-version = "py310"
|
|||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401"]
|
||||
|
||||
[tool.uv]
|
||||
resolution = "lowest-direct"
|
||||
dev-dependencies = [
|
||||
"pyright>=1.1.378",
|
||||
"pytest>=8.3.3",
|
||||
"ruff>=0.6.9",
|
||||
"trio>=0.26.2",
|
||||
]
|
||||
"tests/server/fastmcp/test_func_metadata.py" = ["E501"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["examples/servers/*"]
|
||||
|
|
|
|||
6
src/mcp/cli/__init__.py
Normal file
6
src/mcp/cli/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""FastMCP CLI package."""
|
||||
|
||||
from .cli import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
137
src/mcp/cli/claude.py
Normal file
137
src/mcp/cli/claude.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Claude app integration utilities."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.server.fastmcp.utilities.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_claude_config_path() -> Path | None:
|
||||
"""Get the Claude config directory based on platform."""
|
||||
if sys.platform == "win32":
|
||||
path = Path(Path.home(), "AppData", "Roaming", "Claude")
|
||||
elif sys.platform == "darwin":
|
||||
path = Path(Path.home(), "Library", "Application Support", "Claude")
|
||||
else:
|
||||
return None
|
||||
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def update_claude_config(
|
||||
file_spec: str,
|
||||
server_name: str,
|
||||
*,
|
||||
with_editable: Path | None = None,
|
||||
with_packages: list[str] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
) -> bool:
|
||||
"""Add or update a FastMCP server in Claude's configuration.
|
||||
|
||||
Args:
|
||||
file_spec: Path to the server file, optionally with :object suffix
|
||||
server_name: Name for the server in Claude's config
|
||||
with_editable: Optional directory to install in editable mode
|
||||
with_packages: Optional list of additional packages to install
|
||||
env_vars: Optional dictionary of environment variables. These are merged with
|
||||
any existing variables, with new values taking precedence.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Claude Desktop's config directory is not found, indicating
|
||||
Claude Desktop may not be installed or properly set up.
|
||||
"""
|
||||
config_dir = get_claude_config_path()
|
||||
if not config_dir:
|
||||
raise RuntimeError(
|
||||
"Claude Desktop config directory not found. Please ensure Claude Desktop"
|
||||
" is installed and has been run at least once to initialize its config."
|
||||
)
|
||||
|
||||
config_file = config_dir / "claude_desktop_config.json"
|
||||
if not config_file.exists():
|
||||
try:
|
||||
config_file.write_text("{}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to create Claude config file",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"config_file": str(config_file),
|
||||
},
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
config = json.loads(config_file.read_text())
|
||||
if "mcpServers" not in config:
|
||||
config["mcpServers"] = {}
|
||||
|
||||
# Always preserve existing env vars and merge with new ones
|
||||
if (
|
||||
server_name in config["mcpServers"]
|
||||
and "env" in config["mcpServers"][server_name]
|
||||
):
|
||||
existing_env = config["mcpServers"][server_name]["env"]
|
||||
if env_vars:
|
||||
# New vars take precedence over existing ones
|
||||
env_vars = {**existing_env, **env_vars}
|
||||
else:
|
||||
env_vars = existing_env
|
||||
|
||||
# Build uv run command
|
||||
args = ["run"]
|
||||
|
||||
# Collect all packages in a set to deduplicate
|
||||
packages = {"fastmcp"}
|
||||
if with_packages:
|
||||
packages.update(pkg for pkg in with_packages if pkg)
|
||||
|
||||
# Add all packages with --with
|
||||
for pkg in sorted(packages):
|
||||
args.extend(["--with", pkg])
|
||||
|
||||
if with_editable:
|
||||
args.extend(["--with-editable", str(with_editable)])
|
||||
|
||||
# Convert file path to absolute before adding to command
|
||||
# Split off any :object suffix first
|
||||
if ":" in file_spec:
|
||||
file_path, server_object = file_spec.rsplit(":", 1)
|
||||
file_spec = f"{Path(file_path).resolve()}:{server_object}"
|
||||
else:
|
||||
file_spec = str(Path(file_spec).resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", file_spec])
|
||||
|
||||
server_config = {
|
||||
"command": "uv",
|
||||
"args": args,
|
||||
}
|
||||
|
||||
# Add environment variables if specified
|
||||
if env_vars:
|
||||
server_config["env"] = env_vars
|
||||
|
||||
config["mcpServers"][server_name] = server_config
|
||||
|
||||
config_file.write_text(json.dumps(config, indent=2))
|
||||
logger.info(
|
||||
f"Added server '{server_name}' to Claude config",
|
||||
extra={"config_file": str(config_file)},
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to update Claude config",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"config_file": str(config_file),
|
||||
},
|
||||
)
|
||||
return False
|
||||
471
src/mcp/cli/cli.py
Normal file
471
src/mcp/cli/cli.py
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
"""MCP CLI tools."""
|
||||
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import typer
|
||||
from typing_extensions import Annotated
|
||||
except ImportError:
|
||||
print("Error: typer is required. Install with 'pip install mcp[cli]'")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from mcp.cli import claude
|
||||
from mcp.server.fastmcp.utilities.logging import get_logger
|
||||
except ImportError:
|
||||
print("Error: mcp.server.fastmcp is not installed or not in PYTHONPATH")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import dotenv
|
||||
except ImportError:
|
||||
dotenv = None
|
||||
|
||||
logger = get_logger("cli")
|
||||
|
||||
app = typer.Typer(
|
||||
name="mcp",
|
||||
help="MCP development tools",
|
||||
add_completion=False,
|
||||
no_args_is_help=True, # Show help if no args provided
|
||||
)
|
||||
|
||||
|
||||
def _get_npx_command():
|
||||
"""Get the correct npx command for the current platform."""
|
||||
if sys.platform == "win32":
|
||||
# Try both npx.cmd and npx.exe on Windows
|
||||
for cmd in ["npx.cmd", "npx.exe", "npx"]:
|
||||
try:
|
||||
subprocess.run(
|
||||
[cmd, "--version"], check=True, capture_output=True, shell=True
|
||||
)
|
||||
return cmd
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
return None
|
||||
return "npx" # On Unix-like systems, just use npx
|
||||
|
||||
|
||||
def _parse_env_var(env_var: str) -> tuple[str, str]:
|
||||
"""Parse environment variable string in format KEY=VALUE."""
|
||||
if "=" not in env_var:
|
||||
logger.error(
|
||||
f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
|
||||
)
|
||||
sys.exit(1)
|
||||
key, value = env_var.split("=", 1)
|
||||
return key.strip(), value.strip()
|
||||
|
||||
|
||||
def _build_uv_command(
|
||||
file_spec: str,
|
||||
with_editable: Path | None = None,
|
||||
with_packages: list[str] | None = None,
|
||||
) -> list[str]:
|
||||
"""Build the uv run command that runs a MCP server through mcp run."""
|
||||
cmd = ["uv"]
|
||||
|
||||
cmd.extend(["run", "--with", "mcp"])
|
||||
|
||||
if with_editable:
|
||||
cmd.extend(["--with-editable", str(with_editable)])
|
||||
|
||||
if with_packages:
|
||||
for pkg in with_packages:
|
||||
if pkg:
|
||||
cmd.extend(["--with", pkg])
|
||||
|
||||
# Add mcp run command
|
||||
cmd.extend(["mcp", "run", file_spec])
|
||||
return cmd
|
||||
|
||||
|
||||
def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
|
||||
"""Parse a file path that may include a server object specification.
|
||||
|
||||
Args:
|
||||
file_spec: Path to file, optionally with :object suffix
|
||||
|
||||
Returns:
|
||||
Tuple of (file_path, server_object)
|
||||
"""
|
||||
# First check if we have a Windows path (e.g., C:\...)
|
||||
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
|
||||
|
||||
# Split on the last colon, but only if it's not part of the Windows drive letter
|
||||
# and there's actually another colon in the string after the drive letter
|
||||
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
|
||||
file_str, server_object = file_spec.rsplit(":", 1)
|
||||
else:
|
||||
file_str, server_object = file_spec, None
|
||||
|
||||
# Resolve the file path
|
||||
file_path = Path(file_str).expanduser().resolve()
|
||||
if not file_path.exists():
|
||||
logger.error(f"File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
if not file_path.is_file():
|
||||
logger.error(f"Not a file: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
return file_path, server_object
|
||||
|
||||
|
||||
def _import_server(file: Path, server_object: str | None = None):
|
||||
"""Import a MCP server from a file.
|
||||
|
||||
Args:
|
||||
file: Path to the file
|
||||
server_object: Optional object name in format "module:object" or just "object"
|
||||
|
||||
Returns:
|
||||
The server object
|
||||
"""
|
||||
# Add parent directory to Python path so imports can be resolved
|
||||
file_dir = str(file.parent)
|
||||
if file_dir not in sys.path:
|
||||
sys.path.insert(0, file_dir)
|
||||
|
||||
# Import the module
|
||||
spec = importlib.util.spec_from_file_location("server_module", file)
|
||||
if not spec or not spec.loader:
|
||||
logger.error("Could not load module", extra={"file": str(file)})
|
||||
sys.exit(1)
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# If no object specified, try common server names
|
||||
if not server_object:
|
||||
# Look for the most common server object names
|
||||
for name in ["mcp", "server", "app"]:
|
||||
if hasattr(module, name):
|
||||
return getattr(module, name)
|
||||
|
||||
logger.error(
|
||||
f"No server object found in {file}. Please either:\n"
|
||||
"1. Use a standard variable name (mcp, server, or app)\n"
|
||||
"2. Specify the object name with file:object syntax",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Handle module:object syntax
|
||||
if ":" in server_object:
|
||||
module_name, object_name = server_object.split(":", 1)
|
||||
try:
|
||||
server_module = importlib.import_module(module_name)
|
||||
server = getattr(server_module, object_name, None)
|
||||
except ImportError:
|
||||
logger.error(
|
||||
f"Could not import module '{module_name}'",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Just object name
|
||||
server = getattr(module, server_object, None)
|
||||
|
||||
if server is None:
|
||||
logger.error(
|
||||
f"Server object '{server_object}' not found",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
return server
|
||||
|
||||
|
||||
@app.command()
|
||||
def version() -> None:
|
||||
"""Show the MCP version."""
|
||||
try:
|
||||
version = importlib.metadata.version("mcp")
|
||||
print(f"MCP version {version}")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
print("MCP version unknown (package not installed)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def dev(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with",
|
||||
help="Additional packages to install",
|
||||
),
|
||||
] = [],
|
||||
) -> None:
|
||||
"""Run a MCP server with the MCP Inspector."""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Starting dev server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_object": server_object,
|
||||
"with_editable": str(with_editable) if with_editable else None,
|
||||
"with_packages": with_packages,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
# Import server to get dependencies
|
||||
server = _import_server(file, server_object)
|
||||
if hasattr(server, "dependencies"):
|
||||
with_packages = list(set(with_packages + server.dependencies))
|
||||
|
||||
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
||||
|
||||
# Get the correct npx command
|
||||
npx_cmd = _get_npx_command()
|
||||
if not npx_cmd:
|
||||
logger.error(
|
||||
"npx not found. Please ensure Node.js and npm are properly installed "
|
||||
"and added to your system PATH."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Run the MCP Inspector command with shell=True on Windows
|
||||
shell = sys.platform == "win32"
|
||||
process = subprocess.run(
|
||||
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
||||
check=True,
|
||||
shell=shell,
|
||||
env=dict(os.environ.items()), # Convert to list of tuples for env update
|
||||
)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Dev server failed",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"error": str(e),
|
||||
"returncode": e.returncode,
|
||||
},
|
||||
)
|
||||
sys.exit(e.returncode)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"npx not found. Please ensure Node.js and npm are properly installed "
|
||||
"and added to your system PATH. You may need to restart your terminal "
|
||||
"after installation.",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def run(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
transport: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--transport",
|
||||
"-t",
|
||||
help="Transport protocol to use (stdio or sse)",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Run a MCP server.
|
||||
|
||||
The server can be specified in two ways:
|
||||
1. Module approach: server.py - runs the module directly, expecting a server.run()
|
||||
call
|
||||
2. Import approach: server.py:app - imports and runs the specified server object
|
||||
|
||||
Note: This command runs the server directly. You are responsible for ensuring
|
||||
all dependencies are available. For dependency management, use mcp install
|
||||
or mcp dev instead.
|
||||
"""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Running server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_object": server_object,
|
||||
"transport": transport,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
# Import and get server object
|
||||
server = _import_server(file, server_object)
|
||||
|
||||
# Run the server
|
||||
kwargs = {}
|
||||
if transport:
|
||||
kwargs["transport"] = transport
|
||||
|
||||
server.run(**kwargs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to run server: {e}",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"error": str(e),
|
||||
},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def install(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or"
|
||||
" file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with",
|
||||
help="Additional packages to install",
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var",
|
||||
"-e",
|
||||
help="Environment variables in KEY=VALUE format",
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a MCP server in the Claude desktop app.
|
||||
|
||||
Environment variables are preserved once added and only updated if new values
|
||||
are explicitly provided.
|
||||
"""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Installing server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_name": server_name,
|
||||
"server_object": server_object,
|
||||
"with_editable": str(with_editable) if with_editable else None,
|
||||
"with_packages": with_packages,
|
||||
},
|
||||
)
|
||||
|
||||
if not claude.get_claude_config_path():
|
||||
logger.error("Claude app not found")
|
||||
sys.exit(1)
|
||||
|
||||
# Try to import server to get its name, but fall back to file name if dependencies
|
||||
# missing
|
||||
name = server_name
|
||||
server = None
|
||||
if not name:
|
||||
try:
|
||||
server = _import_server(file, server_object)
|
||||
name = server.name
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.debug(
|
||||
"Could not import server (likely missing dependencies), using file"
|
||||
" name",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
name = file.stem
|
||||
|
||||
# Get server dependencies if available
|
||||
server_dependencies = getattr(server, "dependencies", []) if server else []
|
||||
if server_dependencies:
|
||||
with_packages = list(set(with_packages + server_dependencies))
|
||||
|
||||
# Process environment variables if provided
|
||||
env_dict: dict[str, str] | None = None
|
||||
if env_file or env_vars:
|
||||
env_dict = {}
|
||||
# Load from .env file if specified
|
||||
if env_file:
|
||||
if dotenv:
|
||||
try:
|
||||
env_dict |= {
|
||||
k: v
|
||||
for k, v in dotenv.dotenv_values(env_file).items()
|
||||
if v is not None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load .env file: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.error("python-dotenv is not installed. Cannot load .env file.")
|
||||
sys.exit(1)
|
||||
|
||||
# Add command line environment variables
|
||||
for env_var in env_vars:
|
||||
key, value = _parse_env_var(env_var)
|
||||
env_dict[key] = value
|
||||
|
||||
if claude.update_claude_config(
|
||||
file_spec,
|
||||
name,
|
||||
with_editable=with_editable,
|
||||
with_packages=with_packages,
|
||||
env_vars=env_dict,
|
||||
):
|
||||
logger.info(f"Successfully installed {name} in Claude app")
|
||||
else:
|
||||
logger.error(f"Failed to install {name} in Claude app")
|
||||
sys.exit(1)
|
||||
|
|
@ -1,500 +1,4 @@
|
|||
"""
|
||||
MCP Server Module
|
||||
from .fastmcp import FastMCP
|
||||
from .lowlevel import NotificationOptions, Server
|
||||
|
||||
This module provides a framework for creating an MCP (Model Context Protocol) server.
|
||||
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
|
||||
|
||||
3. Define notification handlers if needed:
|
||||
@server.progress_notification()
|
||||
async def handle_progress(
|
||||
progress_token: str | int, progress: float, total: float | 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.
|
||||
"""
|
||||
|
||||
import contextvars
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Sequence
|
||||
|
||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||
from pydantic import AnyUrl
|
||||
|
||||
import mcp.types as types
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.server.stdio import stdio_server as stdio_server
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.session import RequestResponder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
request_ctx: contextvars.ContextVar[RequestContext[ServerSession]] = (
|
||||
contextvars.ContextVar("request_ctx")
|
||||
)
|
||||
|
||||
|
||||
class NotificationOptions:
|
||||
def __init__(
|
||||
self,
|
||||
prompts_changed: bool = False,
|
||||
resources_changed: bool = False,
|
||||
tools_changed: bool = False,
|
||||
):
|
||||
self.prompts_changed = prompts_changed
|
||||
self.resources_changed = resources_changed
|
||||
self.tools_changed = tools_changed
|
||||
|
||||
|
||||
class Server:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.request_handlers: dict[
|
||||
type, Callable[..., Awaitable[types.ServerResult]]
|
||||
] = {
|
||||
types.PingRequest: _ping_handler,
|
||||
}
|
||||
self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {}
|
||||
self.notification_options = NotificationOptions()
|
||||
logger.debug(f"Initializing server '{name}'")
|
||||
|
||||
def create_initialization_options(
|
||||
self,
|
||||
notification_options: NotificationOptions | None = None,
|
||||
experimental_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||
) -> InitializationOptions:
|
||||
"""Create initialization options from this server instance."""
|
||||
|
||||
def pkg_version(package: str) -> str:
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
|
||||
v = version(package)
|
||||
if v is not None:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "unknown"
|
||||
|
||||
return InitializationOptions(
|
||||
server_name=self.name,
|
||||
server_version=pkg_version("mcp"),
|
||||
capabilities=self.get_capabilities(
|
||||
notification_options or NotificationOptions(),
|
||||
experimental_capabilities or {},
|
||||
),
|
||||
)
|
||||
|
||||
def get_capabilities(
|
||||
self,
|
||||
notification_options: NotificationOptions,
|
||||
experimental_capabilities: dict[str, dict[str, Any]],
|
||||
) -> types.ServerCapabilities:
|
||||
"""Convert existing handlers to a ServerCapabilities object."""
|
||||
prompts_capability = None
|
||||
resources_capability = None
|
||||
tools_capability = None
|
||||
logging_capability = None
|
||||
|
||||
# Set prompt capabilities if handler exists
|
||||
if types.ListPromptsRequest in self.request_handlers:
|
||||
prompts_capability = types.PromptsCapability(
|
||||
listChanged=notification_options.prompts_changed
|
||||
)
|
||||
|
||||
# Set resource capabilities if handler exists
|
||||
if types.ListResourcesRequest in self.request_handlers:
|
||||
resources_capability = types.ResourcesCapability(
|
||||
subscribe=False, listChanged=notification_options.resources_changed
|
||||
)
|
||||
|
||||
# Set tool capabilities if handler exists
|
||||
if types.ListToolsRequest in self.request_handlers:
|
||||
tools_capability = types.ToolsCapability(
|
||||
listChanged=notification_options.tools_changed
|
||||
)
|
||||
|
||||
# Set logging capabilities if handler exists
|
||||
if types.SetLevelRequest in self.request_handlers:
|
||||
logging_capability = types.LoggingCapability()
|
||||
|
||||
return types.ServerCapabilities(
|
||||
prompts=prompts_capability,
|
||||
resources=resources_capability,
|
||||
tools=tools_capability,
|
||||
logging=logging_capability,
|
||||
experimental=experimental_capabilities,
|
||||
)
|
||||
|
||||
@property
|
||||
def request_context(self) -> RequestContext[ServerSession]:
|
||||
"""If called outside of a request context, this will raise a LookupError."""
|
||||
return request_ctx.get()
|
||||
|
||||
def list_prompts(self):
|
||||
def decorator(func: Callable[[], Awaitable[list[types.Prompt]]]):
|
||||
logger.debug("Registering handler for PromptListRequest")
|
||||
|
||||
async def handler(_: Any):
|
||||
prompts = await func()
|
||||
return types.ServerResult(types.ListPromptsResult(prompts=prompts))
|
||||
|
||||
self.request_handlers[types.ListPromptsRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def get_prompt(self):
|
||||
def decorator(
|
||||
func: Callable[
|
||||
[str, dict[str, str] | None], Awaitable[types.GetPromptResult]
|
||||
],
|
||||
):
|
||||
logger.debug("Registering handler for GetPromptRequest")
|
||||
|
||||
async def handler(req: types.GetPromptRequest):
|
||||
prompt_get = await func(req.params.name, req.params.arguments)
|
||||
return types.ServerResult(prompt_get)
|
||||
|
||||
self.request_handlers[types.GetPromptRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def list_resources(self):
|
||||
def decorator(func: Callable[[], Awaitable[list[types.Resource]]]):
|
||||
logger.debug("Registering handler for ListResourcesRequest")
|
||||
|
||||
async def handler(_: Any):
|
||||
resources = await func()
|
||||
return types.ServerResult(
|
||||
types.ListResourcesResult(resources=resources)
|
||||
)
|
||||
|
||||
self.request_handlers[types.ListResourcesRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def list_resource_templates(self):
|
||||
def decorator(func: Callable[[], Awaitable[list[types.ResourceTemplate]]]):
|
||||
logger.debug("Registering handler for ListResourceTemplatesRequest")
|
||||
|
||||
async def handler(_: Any):
|
||||
templates = await func()
|
||||
return types.ServerResult(
|
||||
types.ListResourceTemplatesResult(resourceTemplates=templates)
|
||||
)
|
||||
|
||||
self.request_handlers[types.ListResourceTemplatesRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def read_resource(self):
|
||||
def decorator(func: Callable[[AnyUrl], Awaitable[str | bytes]]):
|
||||
logger.debug("Registering handler for ReadResourceRequest")
|
||||
|
||||
async def handler(req: types.ReadResourceRequest):
|
||||
result = await func(req.params.uri)
|
||||
match result:
|
||||
case str(s):
|
||||
content = types.TextResourceContents(
|
||||
uri=req.params.uri,
|
||||
text=s,
|
||||
mimeType="text/plain",
|
||||
)
|
||||
case bytes(b):
|
||||
import base64
|
||||
|
||||
content = types.BlobResourceContents(
|
||||
uri=req.params.uri,
|
||||
blob=base64.urlsafe_b64encode(b).decode(),
|
||||
mimeType="application/octet-stream",
|
||||
)
|
||||
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(
|
||||
contents=[content],
|
||||
)
|
||||
)
|
||||
|
||||
self.request_handlers[types.ReadResourceRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def set_logging_level(self):
|
||||
def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]):
|
||||
logger.debug("Registering handler for SetLevelRequest")
|
||||
|
||||
async def handler(req: types.SetLevelRequest):
|
||||
await func(req.params.level)
|
||||
return types.ServerResult(types.EmptyResult())
|
||||
|
||||
self.request_handlers[types.SetLevelRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def subscribe_resource(self):
|
||||
def decorator(func: Callable[[AnyUrl], Awaitable[None]]):
|
||||
logger.debug("Registering handler for SubscribeRequest")
|
||||
|
||||
async def handler(req: types.SubscribeRequest):
|
||||
await func(req.params.uri)
|
||||
return types.ServerResult(types.EmptyResult())
|
||||
|
||||
self.request_handlers[types.SubscribeRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def unsubscribe_resource(self):
|
||||
def decorator(func: Callable[[AnyUrl], Awaitable[None]]):
|
||||
logger.debug("Registering handler for UnsubscribeRequest")
|
||||
|
||||
async def handler(req: types.UnsubscribeRequest):
|
||||
await func(req.params.uri)
|
||||
return types.ServerResult(types.EmptyResult())
|
||||
|
||||
self.request_handlers[types.UnsubscribeRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def list_tools(self):
|
||||
def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
|
||||
logger.debug("Registering handler for ListToolsRequest")
|
||||
|
||||
async def handler(_: Any):
|
||||
tools = await func()
|
||||
return types.ServerResult(types.ListToolsResult(tools=tools))
|
||||
|
||||
self.request_handlers[types.ListToolsRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def call_tool(self):
|
||||
def decorator(
|
||||
func: Callable[
|
||||
...,
|
||||
Awaitable[
|
||||
Sequence[
|
||||
types.TextContent | types.ImageContent | types.EmbeddedResource
|
||||
]
|
||||
],
|
||||
],
|
||||
):
|
||||
logger.debug("Registering handler for CallToolRequest")
|
||||
|
||||
async def handler(req: types.CallToolRequest):
|
||||
try:
|
||||
results = await func(req.params.name, (req.params.arguments or {}))
|
||||
return types.ServerResult(
|
||||
types.CallToolResult(content=list(results), isError=False)
|
||||
)
|
||||
except Exception as e:
|
||||
return types.ServerResult(
|
||||
types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=str(e))],
|
||||
isError=True,
|
||||
)
|
||||
)
|
||||
|
||||
self.request_handlers[types.CallToolRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def progress_notification(self):
|
||||
def decorator(
|
||||
func: Callable[[str | int, float, float | None], Awaitable[None]],
|
||||
):
|
||||
logger.debug("Registering handler for ProgressNotification")
|
||||
|
||||
async def handler(req: types.ProgressNotification):
|
||||
await func(
|
||||
req.params.progressToken, req.params.progress, req.params.total
|
||||
)
|
||||
|
||||
self.notification_handlers[types.ProgressNotification] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def completion(self):
|
||||
"""Provides completions for prompts and resource templates"""
|
||||
|
||||
def decorator(
|
||||
func: Callable[
|
||||
[
|
||||
types.PromptReference | types.ResourceReference,
|
||||
types.CompletionArgument,
|
||||
],
|
||||
Awaitable[types.Completion | None],
|
||||
],
|
||||
):
|
||||
logger.debug("Registering handler for CompleteRequest")
|
||||
|
||||
async def handler(req: types.CompleteRequest):
|
||||
completion = await func(req.params.ref, req.params.argument)
|
||||
return types.ServerResult(
|
||||
types.CompleteResult(
|
||||
completion=completion
|
||||
if completion is not None
|
||||
else types.Completion(values=[], total=None, hasMore=None),
|
||||
)
|
||||
)
|
||||
|
||||
self.request_handlers[types.CompleteRequest] = handler
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
async def run(
|
||||
self,
|
||||
read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception],
|
||||
write_stream: MemoryObjectSendStream[types.JSONRPCMessage],
|
||||
initialization_options: InitializationOptions,
|
||||
# When False, exceptions are returned as messages to the client.
|
||||
# When True, exceptions are raised, which will cause the server to shut down
|
||||
# but also make tracing exceptions much easier during testing and when using
|
||||
# in-process servers.
|
||||
raise_exceptions: bool = False,
|
||||
):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
async with ServerSession(
|
||||
read_stream, write_stream, initialization_options
|
||||
) as session:
|
||||
async for message in session.incoming_messages:
|
||||
logger.debug(f"Received message: {message}")
|
||||
|
||||
match message:
|
||||
case RequestResponder(request=types.ClientRequest(root=req)):
|
||||
logger.info(
|
||||
f"Processing request of type {type(req).__name__}"
|
||||
)
|
||||
if type(req) in self.request_handlers:
|
||||
handler = self.request_handlers[type(req)]
|
||||
logger.debug(
|
||||
f"Dispatching request of type {type(req).__name__}"
|
||||
)
|
||||
|
||||
token = None
|
||||
try:
|
||||
# Set our global state that can be retrieved via
|
||||
# app.get_request_context()
|
||||
token = request_ctx.set(
|
||||
RequestContext(
|
||||
message.request_id,
|
||||
message.request_meta,
|
||||
session,
|
||||
)
|
||||
)
|
||||
response = await handler(req)
|
||||
except McpError as err:
|
||||
response = err.error
|
||||
except Exception as err:
|
||||
if raise_exceptions:
|
||||
raise err
|
||||
response = types.ErrorData(
|
||||
code=0, message=str(err), data=None
|
||||
)
|
||||
finally:
|
||||
# Reset the global state after we are done
|
||||
if token is not None:
|
||||
request_ctx.reset(token)
|
||||
|
||||
await message.respond(response)
|
||||
else:
|
||||
await message.respond(
|
||||
types.ErrorData(
|
||||
code=types.METHOD_NOT_FOUND,
|
||||
message="Method not found",
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug("Response sent")
|
||||
case types.ClientNotification(root=notify):
|
||||
if type(notify) in self.notification_handlers:
|
||||
assert type(notify) in self.notification_handlers
|
||||
|
||||
handler = self.notification_handlers[type(notify)]
|
||||
logger.debug(
|
||||
f"Dispatching notification of type "
|
||||
f"{type(notify).__name__}"
|
||||
)
|
||||
|
||||
try:
|
||||
await handler(notify)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Uncaught exception in notification handler: "
|
||||
f"{err}"
|
||||
)
|
||||
|
||||
for warning in w:
|
||||
logger.info(
|
||||
f"Warning: {warning.category.__name__}: {warning.message}"
|
||||
)
|
||||
|
||||
|
||||
async def _ping_handler(request: types.PingRequest) -> types.ServerResult:
|
||||
return types.ServerResult(types.EmptyResult())
|
||||
__all__ = ["Server", "FastMCP", "NotificationOptions"]
|
||||
|
|
|
|||
9
src/mcp/server/fastmcp/__init__.py
Normal file
9
src/mcp/server/fastmcp/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""FastMCP - A more ergonomic interface for MCP servers."""
|
||||
|
||||
from importlib.metadata import version
|
||||
|
||||
from .server import Context, FastMCP
|
||||
from .utilities.types import Image
|
||||
|
||||
__version__ = version("mcp")
|
||||
__all__ = ["FastMCP", "Context", "Image"]
|
||||
21
src/mcp/server/fastmcp/exceptions.py
Normal file
21
src/mcp/server/fastmcp/exceptions.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Custom exceptions for FastMCP."""
|
||||
|
||||
|
||||
class FastMCPError(Exception):
|
||||
"""Base error for FastMCP."""
|
||||
|
||||
|
||||
class ValidationError(FastMCPError):
|
||||
"""Error in validating parameters or return values."""
|
||||
|
||||
|
||||
class ResourceError(FastMCPError):
|
||||
"""Error in resource operations."""
|
||||
|
||||
|
||||
class ToolError(FastMCPError):
|
||||
"""Error in tool operations."""
|
||||
|
||||
|
||||
class InvalidSignature(Exception):
|
||||
"""Invalid signature for use with FastMCP."""
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue