chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Python SDK examples
- [`stories/`](stories/) — **the canonical reference.** One self-verifying
example per protocol feature, each with its own README. Start with
[`stories/tools/`](stories/tools/); the [stories README](stories/README.md)
has the full table and how to run them.
- [`snippets/`](snippets/) — short extracts that were embedded into the v1
README (now on the `v1.x` branch); superseded by `docs_src/`, which the docs
and README embed today. Retained pending consolidation into `stories/`.
- [`servers/everything-server/`](servers/everything-server/) — the conformance
target for the cross-SDK
[conformance suite](https://github.com/modelcontextprotocol/conformance).
Exercises every server capability in one process.
- [`mcpserver/`](mcpserver/) — single-file v1-era examples retained for the
migration guide; superseded by `stories/` and slated for removal.
- [`clients/`](clients/) and the remaining [`servers/`](servers/) directories
(`simple-*`, `sse-polling-demo`, `structured-output-lowlevel`) — standalone
v1-era projects retained pending consolidation into `stories/` (the
`simple-auth` pair is still linked from `docs/run/authorization.md` and `docs/client/oauth-clients.md`).
For real-world servers see the
[servers repository](https://github.com/modelcontextprotocol/servers).
@@ -0,0 +1,98 @@
# Simple Auth Client Example
A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport.
## Features
- OAuth 2.0 authentication with PKCE
- Support for both StreamableHTTP and SSE transports
- Interactive command-line interface
## Installation
```bash
cd examples/clients/simple-auth-client
uv sync --reinstall
```
## Usage
### 1. Start an MCP server with OAuth support
The simple-auth server example provides three server configurations. See [examples/servers/simple-auth/README.md](../../servers/simple-auth/README.md) for full details.
#### Option A: New Architecture (Recommended)
Separate Authorization Server and Resource Server:
```bash
# Terminal 1: Start Authorization Server on port 9000
cd examples/servers/simple-auth
uv run mcp-simple-auth-as --port=9000
# Terminal 2: Start Resource Server on port 8001
cd examples/servers/simple-auth
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
```
#### Option B: Legacy Server (Backwards Compatibility)
```bash
# Single server that acts as both AS and RS (port 8000)
cd examples/servers/simple-auth
uv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http
```
### 2. Run the client
```bash
# Connect to Resource Server (new architecture, default port 8001)
MCP_SERVER_PORT=8001 uv run mcp-simple-auth-client
# Connect to Legacy Server (port 8000)
uv run mcp-simple-auth-client
# Use SSE transport
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
```
### 3. Complete OAuth flow
The client will open your browser for authentication. After completing OAuth, you can use commands:
- `list` - List available tools
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
- `quit` - Exit
## Example
```markdown
🚀 Simple MCP Auth Client
Connecting to: http://localhost:8001/mcp
Transport type: streamable-http
🔗 Attempting to connect to http://localhost:8001/mcp...
📡 Opening StreamableHTTP transport connection with auth...
Opening browser for authorization: http://localhost:9000/authorize?...
✅ Connected to MCP server at http://localhost:8001/mcp
mcp> list
📋 Available tools:
1. get_time
Description: Get the current server time.
mcp> call get_time
🔧 Tool 'get_time' result:
{"current_time": "2024-01-15T10:30:00", "timezone": "UTC", ...}
mcp> quit
```
## Configuration
| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `MCP_SERVER_PORT` | Port number of the MCP server | `8000` |
| `MCP_TRANSPORT_TYPE` | Transport type: `streamable-http` or `sse` | `streamable-http` |
| `MCP_CLIENT_METADATA_URL` | Optional URL for client metadata (CIMD) | None |
@@ -0,0 +1 @@
"""Simple OAuth client for MCP simple-auth server."""
@@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""Simple MCP client example with OAuth authentication support.
This client connects to an MCP server using streamable HTTP transport with OAuth.
"""
from __future__ import annotations as _annotations
import asyncio
import os
import socketserver
import threading
import time
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from urllib.parse import parse_qs, urlparse
import httpx
from mcp.client._transport import ReadStream, WriteStream
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
from mcp.shared.message import SessionMessage
class InMemoryTokenStorage(TokenStorage):
"""Simple in-memory token storage implementation."""
def __init__(self):
self._tokens: OAuthToken | None = None
self._client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self._tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self._tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self._client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self._client_info = client_info
class CallbackHandler(BaseHTTPRequestHandler):
"""Simple HTTP handler to capture OAuth callback."""
def __init__(
self,
request: Any,
client_address: tuple[str, int],
server: socketserver.BaseServer,
callback_data: dict[str, Any],
):
"""Initialize with callback data storage."""
self.callback_data = callback_data
super().__init__(request, client_address, server)
def do_GET(self):
"""Handle GET request from OAuth redirect."""
parsed = urlparse(self.path)
query_params = parse_qs(parsed.query)
if "code" in query_params:
self.callback_data["authorization_code"] = query_params["code"][0]
self.callback_data["state"] = query_params.get("state", [None])[0]
self.callback_data["iss"] = query_params.get("iss", [None])[0]
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"""
<html>
<body>
<h1>Authorization Successful!</h1>
<p>You can close this window and return to the terminal.</p>
<script>setTimeout(() => window.close(), 2000);</script>
</body>
</html>
""")
elif "error" in query_params:
self.callback_data["error"] = query_params["error"][0]
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(
f"""
<html>
<body>
<h1>Authorization Failed</h1>
<p>Error: {query_params["error"][0]}</p>
<p>You can close this window and return to the terminal.</p>
</body>
</html>
""".encode()
)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format: str, *args: Any):
"""Suppress default logging."""
class CallbackServer:
"""Simple server to handle OAuth callbacks."""
def __init__(self, port: int = 3000):
self.port = port
self.server = None
self.thread = None
self.callback_data = {"authorization_code": None, "state": None, "iss": None, "error": None}
def _create_handler_with_data(self):
"""Create a handler class with access to callback data."""
callback_data = self.callback_data
class DataCallbackHandler(CallbackHandler):
def __init__(
self,
request: BaseHTTPRequestHandler,
client_address: tuple[str, int],
server: socketserver.BaseServer,
):
super().__init__(request, client_address, server, callback_data)
return DataCallbackHandler
def start(self):
"""Start the callback server in a background thread."""
handler_class = self._create_handler_with_data()
self.server = HTTPServer(("localhost", self.port), handler_class)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
print(f"🖥️ Started callback server on http://localhost:{self.port}")
def stop(self):
"""Stop the callback server."""
if self.server:
self.server.shutdown()
self.server.server_close()
if self.thread:
self.thread.join(timeout=1)
def wait_for_callback(self, timeout: int = 300):
"""Wait for OAuth callback with timeout."""
start_time = time.time()
while time.time() - start_time < timeout:
if self.callback_data["authorization_code"]:
return self.callback_data["authorization_code"]
elif self.callback_data["error"]:
raise Exception(f"OAuth error: {self.callback_data['error']}")
time.sleep(0.1)
raise Exception("Timeout waiting for OAuth callback")
@property
def state(self):
"""The received state parameter."""
return self.callback_data["state"]
@property
def iss(self):
"""The received iss parameter."""
return self.callback_data["iss"]
class SimpleAuthClient:
"""Simple MCP client with auth support."""
def __init__(
self,
server_url: str,
transport_type: str = "streamable-http",
client_metadata_url: str | None = None,
):
self.server_url = server_url
self.transport_type = transport_type
self.client_metadata_url = client_metadata_url
self.session: ClientSession | None = None
async def connect(self):
"""Connect to the MCP server."""
print(f"🔗 Attempting to connect to {self.server_url}...")
try:
callback_server = CallbackServer(port=3030)
callback_server.start()
async def callback_handler() -> AuthorizationCodeResult:
"""Wait for OAuth callback and return auth code, state, and iss."""
print("⏳ Waiting for authorization callback...")
try:
auth_code = callback_server.wait_for_callback(timeout=300)
return AuthorizationCodeResult(code=auth_code, state=callback_server.state, iss=callback_server.iss)
finally:
callback_server.stop()
client_metadata_dict = {
"client_name": "Simple Auth Client",
"redirect_uris": ["http://localhost:3030/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
}
async def _default_redirect_handler(authorization_url: str) -> None:
"""Default redirect handler that opens the URL in a browser."""
print(f"Opening browser for authorization: {authorization_url}")
webbrowser.open(authorization_url)
# Create OAuth authentication handler using the new interface
# Use client_metadata_url to enable CIMD when the server supports it
oauth_auth = OAuthClientProvider(
server_url=self.server_url.replace("/mcp", ""),
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
storage=InMemoryTokenStorage(),
redirect_handler=_default_redirect_handler,
callback_handler=callback_handler,
client_metadata_url=self.client_metadata_url,
)
# Create transport with auth handler based on transport type
if self.transport_type == "sse":
print("📡 Opening SSE transport connection with auth...")
async with sse_client(
url=self.server_url,
auth=oauth_auth,
timeout=60.0,
) as (read_stream, write_stream):
await self._run_session(read_stream, write_stream)
else:
print("📡 Opening StreamableHTTP transport connection with auth...")
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
async with streamable_http_client(url=self.server_url, http_client=custom_client) as (
read_stream,
write_stream,
):
await self._run_session(read_stream, write_stream)
except Exception as e:
print(f"❌ Failed to connect: {e}")
import traceback
traceback.print_exc()
async def _run_session(
self,
read_stream: ReadStream[SessionMessage | Exception],
write_stream: WriteStream[SessionMessage],
):
"""Run the MCP session with the given streams."""
print("🤝 Initializing MCP session...")
async with ClientSession(read_stream, write_stream) as session:
self.session = session
print("⚡ Starting session initialization...")
await session.initialize()
print("✨ Session initialization complete!")
print(f"\n✅ Connected to MCP server at {self.server_url}")
# Run interactive loop
await self.interactive_loop()
async def list_tools(self):
"""List available tools from the server."""
if not self.session:
print("❌ Not connected to server")
return
try:
result = await self.session.list_tools()
if hasattr(result, "tools") and result.tools:
print("\n📋 Available tools:")
for i, tool in enumerate(result.tools, 1):
print(f"{i}. {tool.name}")
if tool.description:
print(f" Description: {tool.description}")
print()
else:
print("No tools available")
except Exception as e:
print(f"❌ Failed to list tools: {e}")
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):
"""Call a specific tool."""
if not self.session:
print("❌ Not connected to server")
return
try:
result = await self.session.call_tool(tool_name, arguments or {})
print(f"\n🔧 Tool '{tool_name}' result:")
if hasattr(result, "content"):
for content in result.content:
if content.type == "text":
print(content.text)
else:
print(content)
else:
print(result)
except Exception as e:
print(f"❌ Failed to call tool '{tool_name}': {e}")
async def interactive_loop(self):
"""Run interactive command loop."""
print("\n🎯 Interactive MCP Client")
print("Commands:")
print(" list - List available tools")
print(" call <tool_name> [args] - Call a tool")
print(" quit - Exit the client")
print()
while True:
try:
command = input("mcp> ").strip()
if not command:
continue
if command == "quit":
break
elif command == "list":
await self.list_tools()
elif command.startswith("call "):
parts = command.split(maxsplit=2)
tool_name = parts[1] if len(parts) > 1 else ""
if not tool_name:
print("❌ Please specify a tool name")
continue
# Parse arguments (simple JSON-like format)
arguments: dict[str, Any] = {}
if len(parts) > 2:
import json
try:
arguments = json.loads(parts[2])
except json.JSONDecodeError:
print("❌ Invalid arguments format (expected JSON)")
continue
await self.call_tool(tool_name, arguments)
else:
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
except KeyboardInterrupt:
print("\n\n👋 Goodbye!")
break
except EOFError:
break
async def main():
"""Main entry point."""
# Default server URL - can be overridden with environment variable
# Most MCP streamable HTTP servers use /mcp as the endpoint
server_url = os.getenv("MCP_SERVER_PORT", 8000)
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable-http")
client_metadata_url = os.getenv("MCP_CLIENT_METADATA_URL")
server_url = (
f"http://localhost:{server_url}/mcp"
if transport_type == "streamable-http"
else f"http://localhost:{server_url}/sse"
)
print("🚀 Simple MCP Auth Client")
print(f"Connecting to: {server_url}")
print(f"Transport type: {transport_type}")
if client_metadata_url:
print(f"Client metadata URL: {client_metadata_url}")
# Start connection flow - OAuth will be handled automatically
client = SimpleAuthClient(server_url, transport_type, client_metadata_url)
await client.connect()
def cli():
"""CLI entry point for uv script."""
asyncio.run(main())
if __name__ == "__main__":
cli()
@@ -0,0 +1,43 @@
[project]
name = "mcp-simple-auth-client"
version = "0.1.0"
description = "A simple OAuth client for the MCP simple-auth server"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "oauth", "client", "auth"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["click>=8.2.0", "mcp"]
[project.scripts]
mcp-simple-auth-client = "mcp_simple_auth_client.main:cli"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_auth_client"]
[tool.pyright]
include = ["mcp_simple_auth_client"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
+113
View File
@@ -0,0 +1,113 @@
# MCP Simple Chatbot
This example demonstrates how to integrate the Model Context Protocol (MCP) into a simple CLI chatbot. The implementation showcases MCP's flexibility by supporting multiple tools through MCP servers and is compatible with any LLM provider that follows OpenAI API standards.
## Requirements
- Python 3.10
- `python-dotenv`
- `requests`
- `mcp`
- `uvicorn`
## Installation
1. **Install the dependencies:**
```bash
pip install -r requirements.txt
```
2. **Set up environment variables:**
Create a `.env` file in the root directory and add your API key:
```plaintext
LLM_API_KEY=your_api_key_here
```
**Note:** The current implementation is configured to use the Groq API endpoint (`https://api.groq.com/openai/v1/chat/completions`) with the `llama-3.2-90b-vision-preview` model. If you plan to use a different LLM provider, you'll need to modify the `LLMClient` class in `main.py` to use the appropriate endpoint URL and model parameters.
3. **Configure servers:**
The `servers_config.json` follows the same structure as Claude Desktop, allowing for easy integration of multiple servers.
Here's an example:
```json
{
"mcpServers": {
"sqlite": {
"command": "uvx",
"args": ["mcp-server-sqlite", "--db-path", "./test.db"]
},
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
```
Environment variables are supported as well. Pass them as you would with the Claude Desktop App.
Example:
```json
{
"mcpServers": {
"server_name": {
"command": "uvx",
"args": ["mcp-server-name", "--additional-args"],
"env": {
"API_KEY": "your_api_key_here"
}
}
}
}
```
## Usage
1. **Run the client:**
```bash
python main.py
```
2. **Interact with the assistant:**
The assistant will automatically detect available tools and can respond to queries based on the tools provided by the configured servers.
3. **Exit the session:**
Type `quit` or `exit` to end the session.
## Architecture
- **Tool Discovery**: Tools are automatically discovered from configured servers.
- **System Prompt**: Tools are dynamically included in the system prompt, allowing the LLM to understand available capabilities.
- **Server Integration**: Supports any MCP-compatible server, tested with various server implementations including Uvicorn and Node.js.
### Class Structure
- **Configuration**: Manages environment variables and server configurations
- **Server**: Handles MCP server initialization, tool discovery, and execution
- **Tool**: Represents individual tools with their properties and formatting
- **LLMClient**: Manages communication with the LLM provider
- **ChatSession**: Orchestrates the interaction between user, LLM, and tools
### Logic Flow
1. **Tool Integration**:
- Tools are dynamically discovered from MCP servers
- Tool descriptions are automatically included in system prompt
- Tool execution is handled through standardized MCP protocol
2. **Runtime Flow**:
- User input is received
- Input is sent to LLM with context of available tools
- LLM response is parsed:
- If it's a tool call → execute tool and return result
- If it's a direct response → return to user
- Tool results are sent back to LLM for interpretation
- Final response is presented to user
@@ -0,0 +1 @@
LLM_API_KEY=gsk_1234567890
@@ -0,0 +1,421 @@
from __future__ import annotations
import asyncio
import json
import logging
import os
import shutil
from contextlib import AsyncExitStack
from typing import Any
import httpx
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
class Configuration:
"""Manages configuration and environment variables for the MCP client."""
def __init__(self) -> None:
"""Initialize configuration with environment variables."""
self.load_env()
self.api_key = os.getenv("LLM_API_KEY")
@staticmethod
def load_env() -> None:
"""Load environment variables from .env file."""
load_dotenv()
@staticmethod
def load_config(file_path: str) -> dict[str, Any]:
"""Load server configuration from JSON file.
Args:
file_path: Path to the JSON configuration file.
Returns:
Dict containing server configuration.
Raises:
FileNotFoundError: If configuration file doesn't exist.
JSONDecodeError: If configuration file is invalid JSON.
"""
with open(file_path, "r") as f:
return json.load(f)
@property
def llm_api_key(self) -> str:
"""Get the LLM API key.
Returns:
The API key as a string.
Raises:
ValueError: If the API key is not found in environment variables.
"""
if not self.api_key:
raise ValueError("LLM_API_KEY not found in environment variables")
return self.api_key
class Server:
"""Manages MCP server connections and tool execution."""
def __init__(self, name: str, config: dict[str, Any]) -> None:
self.name: str = name
self.config: dict[str, Any] = config
self.stdio_context: Any | None = None
self.session: ClientSession | None = None
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
self.exit_stack: AsyncExitStack = AsyncExitStack()
async def initialize(self) -> None:
"""Initialize the server connection."""
command = shutil.which("npx") if self.config["command"] == "npx" else self.config["command"]
if command is None:
raise ValueError("The command must be a valid string and cannot be None.")
server_params = StdioServerParameters(
command=command,
args=self.config["args"],
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
)
try:
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
read, write = stdio_transport
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
await session.initialize()
self.session = session
except Exception as e:
logging.error(f"Error initializing server {self.name}: {e}")
await self.cleanup()
raise
async def list_tools(self) -> list[Tool]:
"""List available tools from the server.
Returns:
A list of available tools.
Raises:
RuntimeError: If the server is not initialized.
"""
if not self.session:
raise RuntimeError(f"Server {self.name} not initialized")
tools_response = await self.session.list_tools()
tools: list[Tool] = []
for item in tools_response:
if item[0] == "tools":
tools.extend(Tool(tool.name, tool.description, tool.input_schema, tool.title) for tool in item[1])
return tools
async def execute_tool(
self,
tool_name: str,
arguments: dict[str, Any],
retries: int = 2,
delay: float = 1.0,
) -> Any:
"""Execute a tool with retry mechanism.
Args:
tool_name: Name of the tool to execute.
arguments: Tool arguments.
retries: Number of retry attempts.
delay: Delay between retries in seconds.
Returns:
Tool execution result.
Raises:
RuntimeError: If server is not initialized.
Exception: If tool execution fails after all retries.
"""
if not self.session:
raise RuntimeError(f"Server {self.name} not initialized")
attempt = 0
while attempt < retries:
try:
logging.info(f"Executing {tool_name}...")
result = await self.session.call_tool(tool_name, arguments)
return result
except Exception as e:
attempt += 1
logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
if attempt < retries:
logging.info(f"Retrying in {delay} seconds...")
await asyncio.sleep(delay)
else:
logging.error("Max retries reached. Failing.")
raise
async def cleanup(self) -> None:
"""Clean up server resources."""
async with self._cleanup_lock:
try:
await self.exit_stack.aclose()
self.session = None
self.stdio_context = None
except Exception as e:
logging.error(f"Error during cleanup of server {self.name}: {e}")
class Tool:
"""Represents a tool with its properties and formatting."""
def __init__(
self,
name: str,
description: str,
input_schema: dict[str, Any],
title: str | None = None,
) -> None:
self.name: str = name
self.title: str | None = title
self.description: str = description
self.input_schema: dict[str, Any] = input_schema
def format_for_llm(self) -> str:
"""Format tool information for LLM.
Returns:
A formatted string describing the tool.
"""
args_desc: list[str] = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
if param_name in self.input_schema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
# Build the formatted output with title as a separate field
output = f"Tool: {self.name}\n"
# Add human-readable title if available
if self.title:
output += f"User-readable title: {self.title}\n"
output += f"""Description: {self.description}
Arguments:
{chr(10).join(args_desc)}
"""
return output
class LLMClient:
"""Manages communication with the LLM provider."""
def __init__(self, api_key: str) -> None:
self.api_key: str = api_key
def get_response(self, messages: list[dict[str, str]]) -> str:
"""Get a response from the LLM.
Args:
messages: A list of message dictionaries.
Returns:
The LLM's response as a string.
Raises:
httpx.RequestError: If the request to the LLM fails.
"""
url = "https://api.groq.com/openai/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
payload = {
"messages": messages,
"model": "meta-llama/llama-4-scout-17b-16e-instruct",
"temperature": 0.7,
"max_tokens": 4096,
"top_p": 1,
"stream": False,
"stop": None,
}
try:
with httpx.Client() as client:
response = client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
except httpx.RequestError as e:
error_message = f"Error getting LLM response: {str(e)}"
logging.error(error_message)
if isinstance(e, httpx.HTTPStatusError):
status_code = e.response.status_code
logging.error(f"Status code: {status_code}")
logging.error(f"Response details: {e.response.text}")
return f"I encountered an error: {error_message}. Please try again or rephrase your request."
class ChatSession:
"""Orchestrates the interaction between user, LLM, and tools."""
def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
self.servers: list[Server] = servers
self.llm_client: LLMClient = llm_client
async def cleanup_servers(self) -> None:
"""Clean up all servers properly."""
for server in reversed(self.servers):
try:
await server.cleanup()
except Exception as e:
logging.warning(f"Warning during final cleanup: {e}")
async def process_llm_response(self, llm_response: str) -> str:
"""Process the LLM response and execute tools if needed.
Args:
llm_response: The response from the LLM.
Returns:
The result of tool execution or the original response.
"""
import json
def _clean_json_string(json_string: str) -> str:
"""Remove ```json ... ``` or ``` ... ``` wrappers if the LLM response is fenced."""
import re
pattern = r"^```(?:\s*json)?\s*(.*?)\s*```$"
return re.sub(pattern, r"\1", json_string, flags=re.DOTALL | re.IGNORECASE).strip()
try:
tool_call = json.loads(_clean_json_string(llm_response))
if "tool" in tool_call and "arguments" in tool_call:
logging.info(f"Executing tool: {tool_call['tool']}")
logging.info(f"With arguments: {tool_call['arguments']}")
for server in self.servers:
tools = await server.list_tools()
if any(tool.name == tool_call["tool"] for tool in tools):
try:
result = await server.execute_tool(tool_call["tool"], tool_call["arguments"])
if isinstance(result, dict) and "progress" in result:
progress = result["progress"] # type: ignore
total = result["total"] # type: ignore
percentage = (progress / total) * 100 # type: ignore
logging.info(f"Progress: {progress}/{total} ({percentage:.1f}%)")
return f"Tool execution result: {result}"
except Exception as e:
error_msg = f"Error executing tool: {str(e)}"
logging.error(error_msg)
return error_msg
return f"No server found with tool: {tool_call['tool']}"
return llm_response
except json.JSONDecodeError:
return llm_response
async def start(self) -> None:
"""Main chat session handler."""
try:
for server in self.servers:
try:
await server.initialize()
except Exception as e:
logging.error(f"Failed to initialize server: {e}")
await self.cleanup_servers()
return
all_tools: list[Tool] = []
for server in self.servers:
tools = await server.list_tools()
all_tools.extend(tools)
tools_description = "\n".join([tool.format_for_llm() for tool in all_tools])
system_message = (
"You are a helpful assistant with access to these tools:\n\n"
f"{tools_description}\n"
"Choose the appropriate tool based on the user's question. "
"If no tool is needed, reply directly.\n\n"
"IMPORTANT: When you need to use a tool, you must ONLY respond with "
"the exact JSON object format below, nothing else:\n"
"{\n"
' "tool": "tool-name",\n'
' "arguments": {\n'
' "argument-name": "value"\n'
" }\n"
"}\n\n"
"After receiving a tool's response:\n"
"1. Transform the raw data into a natural, conversational response\n"
"2. Keep responses concise but informative\n"
"3. Focus on the most relevant information\n"
"4. Use appropriate context from the user's question\n"
"5. Avoid simply repeating the raw data\n\n"
"Please use only the tools that are explicitly defined above."
)
messages = [{"role": "system", "content": system_message}]
while True:
try:
user_input = input("You: ").strip().lower()
if user_input in ["quit", "exit"]:
logging.info("\nExiting...")
break
messages.append({"role": "user", "content": user_input})
llm_response = self.llm_client.get_response(messages)
logging.info("\nAssistant: %s", llm_response)
result = await self.process_llm_response(llm_response)
if result != llm_response:
messages.append({"role": "assistant", "content": llm_response})
messages.append({"role": "system", "content": result})
final_response = self.llm_client.get_response(messages)
logging.info("\nFinal response: %s", final_response)
messages.append({"role": "assistant", "content": final_response})
else:
messages.append({"role": "assistant", "content": llm_response})
except KeyboardInterrupt:
logging.info("\nExiting...")
break
finally:
await self.cleanup_servers()
async def run() -> None:
"""Initialize and run the chat session."""
config = Configuration()
server_config = config.load_config("servers_config.json")
servers = [Server(name, srv_config) for name, srv_config in server_config["mcpServers"].items()]
llm_client = LLMClient(config.llm_api_key)
chat_session = ChatSession(servers, llm_client)
await chat_session.start()
def main() -> None:
asyncio.run(run())
if __name__ == "__main__":
main()
@@ -0,0 +1,4 @@
python-dotenv>=1.0.0
requests>=2.31.0
mcp>=1.0.0
uvicorn>=0.32.1
@@ -0,0 +1,12 @@
{
"mcpServers": {
"sqlite": {
"command": "uvx",
"args": ["mcp-server-sqlite", "--db-path", "./test.db"]
},
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
@@ -0,0 +1,47 @@
[project]
name = "mcp-simple-chatbot"
version = "0.1.0"
description = "A simple CLI chatbot using the Model Context Protocol (MCP)"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "chatbot", "cli"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"python-dotenv>=1.0.0",
"mcp",
"uvicorn>=0.32.1",
]
[project.scripts]
mcp-simple-chatbot = "mcp_simple_chatbot.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_chatbot"]
[tool.pyright]
include = ["mcp_simple_chatbot"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1,30 @@
# MCP SSE Polling Demo Client
Demonstrates client-side auto-reconnect for the SSE polling pattern (SEP-1699).
## Features
- Connects to SSE polling demo server
- Automatically reconnects when server closes SSE stream
- Resumes from Last-Event-ID to avoid missing messages
- Respects server-provided retry interval
## Usage
```bash
# First start the server:
uv run mcp-sse-polling-demo --port 3000
# Then run this client:
uv run mcp-sse-polling-client --url http://localhost:3000/mcp
# Custom options:
uv run mcp-sse-polling-client --url http://localhost:3000/mcp --items 20 --checkpoint-every 5
```
## Options
- `--url`: Server URL (default: <http://localhost:3000/mcp>)
- `--items`: Number of items to process (default: 10)
- `--checkpoint-every`: Checkpoint interval (default: 3)
- `--log-level`: Logging level (default: DEBUG)
@@ -0,0 +1 @@
"""SSE Polling Demo Client - demonstrates auto-reconnect for long-running tasks."""
@@ -0,0 +1,102 @@
"""SSE Polling Demo Client
Demonstrates the client-side auto-reconnect for SSE polling pattern.
This client connects to the SSE Polling Demo server and calls process_batch,
which triggers periodic server-side stream closes. The client automatically
reconnects using Last-Event-ID and resumes receiving messages.
Run with:
# First start the server:
uv run mcp-sse-polling-demo --port 3000
# Then run this client:
uv run mcp-sse-polling-client --url http://localhost:3000/mcp
"""
import asyncio
import logging
import click
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
async def run_demo(url: str, items: int, checkpoint_every: int) -> None:
"""Run the SSE polling demo."""
print(f"\n{'=' * 60}")
print("SSE Polling Demo Client")
print(f"{'=' * 60}")
print(f"Server URL: {url}")
print(f"Processing {items} items with checkpoints every {checkpoint_every}")
print(f"{'=' * 60}\n")
async with streamable_http_client(url) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the connection
print("Initializing connection...")
await session.initialize()
print("Connected!\n")
# List available tools
tools = await session.list_tools()
print(f"Available tools: {[t.name for t in tools.tools]}\n")
# Call the process_batch tool
print(f"Calling process_batch(items={items}, checkpoint_every={checkpoint_every})...\n")
print("-" * 40)
result = await session.call_tool(
"process_batch",
{
"items": items,
"checkpoint_every": checkpoint_every,
},
)
print("-" * 40)
if result.content:
content = result.content[0]
text = getattr(content, "text", str(content))
print(f"\nResult: {text}")
else:
print("\nResult: No content")
print(f"{'=' * 60}\n")
@click.command()
@click.option(
"--url",
default="http://localhost:3000/mcp",
help="Server URL",
)
@click.option(
"--items",
default=10,
help="Number of items to process",
)
@click.option(
"--checkpoint-every",
default=3,
help="Checkpoint interval",
)
@click.option(
"--log-level",
default="INFO",
help="Logging level",
)
def main(url: str, items: int, checkpoint_every: int, log_level: str) -> None:
"""Run the SSE Polling Demo client."""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Suppress noisy HTTP client logging
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
asyncio.run(run_demo(url, items, checkpoint_every))
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
[project]
name = "mcp-sse-polling-client"
version = "0.1.0"
description = "Demo client for SSE polling with auto-reconnect"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "sse", "polling", "client"]
license = { text = "MIT" }
dependencies = ["click>=8.2.0", "mcp"]
[project.scripts]
mcp-sse-polling-client = "mcp_sse_polling_client.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_sse_polling_client"]
[tool.pyright]
include = ["mcp_sse_polling_client"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
+29
View File
@@ -0,0 +1,29 @@
"""MCPServer Complex inputs Example
Demonstrates validation via pydantic with complex models.
"""
from typing import Annotated
from pydantic import BaseModel, Field
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("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
+24
View File
@@ -0,0 +1,24 @@
"""MCPServer Desktop Example
A simple example that exposes the desktop directory as a resource.
"""
from pathlib import Path
from mcp.server.mcpserver import MCPServer
# Create server
mcp = MCPServer("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 sum(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@@ -0,0 +1,22 @@
"""MCPServer Echo Server with direct CallToolResult return"""
from typing import Annotated
from mcp_types import CallToolResult, TextContent
from pydantic import BaseModel
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Echo Server")
class EchoResponse(BaseModel):
text: str
@mcp.tool()
def echo(text: str) -> Annotated[CallToolResult, EchoResponse]:
"""Echo the input text with structure and metadata"""
return CallToolResult(
content=[TextContent(type="text", text=text)], structured_content={"text": text}, _meta={"some": "metadata"}
)
+28
View File
@@ -0,0 +1,28 @@
"""MCPServer Echo Server"""
from mcp.server.mcpserver import MCPServer
# Create server
mcp = MCPServer("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
+56
View File
@@ -0,0 +1,56 @@
"""MCPServer Icons Demo Server
Demonstrates using icons with tools, resources, prompts, and implementation.
"""
import base64
from pathlib import Path
from mcp.server.mcpserver import Icon, MCPServer
# Load the icon file and convert to data URI
icon_path = Path(__file__).parent / "mcp.png"
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
icon_data_uri = f"data:image/png;base64,{icon_data}"
icon_data = Icon(src=icon_data_uri, mime_type="image/png", sizes=["64x64"])
# Create server with icons in implementation
mcp = MCPServer(
"Icons Demo Server", website_url="https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data]
)
@mcp.tool(icons=[icon_data])
def demo_tool(message: str) -> str:
"""A demo tool with an icon."""
return message
@mcp.resource("demo://readme", icons=[icon_data])
def readme_resource() -> str:
"""A demo resource with an icon"""
return "This resource has an icon"
@mcp.prompt("prompt_with_icon", icons=[icon_data])
def prompt_with_icon(text: str) -> str:
"""A demo prompt with an icon"""
return text
@mcp.tool(
icons=[
Icon(src=icon_data_uri, mime_type="image/png", sizes=["16x16"]),
Icon(src=icon_data_uri, mime_type="image/png", sizes=["32x32"]),
Icon(src=icon_data_uri, mime_type="image/png", sizes=["64x64"]),
]
)
def multi_icon_tool(action: str) -> str:
"""A tool demonstrating multiple icons."""
return "multi_icon_tool"
if __name__ == "__main__":
# Run the server
mcp.run()
@@ -0,0 +1,31 @@
"""MCPServer Echo Server that sends log messages and progress updates to the client"""
import asyncio
from mcp.server.mcpserver import Context, MCPServer
# Create server
mcp = MCPServer("Echo Server with logging and progress updates")
@mcp.tool()
async def echo(text: str, ctx: Context) -> str:
"""Echo the input text sending log messages and progress updates during processing."""
await ctx.report_progress(progress=0, total=100)
await ctx.info("Starting to process echo for input: " + text)
await asyncio.sleep(2)
await ctx.info("Halfway through processing echo for input: " + text)
await ctx.report_progress(progress=50, total=100)
await asyncio.sleep(2)
await ctx.info("Finished processing echo for input: " + text)
await ctx.report_progress(progress=100, total=100)
# Progress notifications are process asynchronously by the client.
# A small delay here helps ensure the last notification is processed by the client.
await asyncio.sleep(0.1)
return text
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+324
View File
@@ -0,0 +1,324 @@
# /// 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, TypeVar
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.mcpserver import MCPServer
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"
T = TypeVar("T")
mcp = MCPServer("memory")
DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db"
# reset memory with rm ~/.mcp/{USER}/memory/*
PROFILE_DIR = (Path.home() / ".mcp" / 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(
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())
@@ -0,0 +1,19 @@
"""MCPServer Example showing parameter descriptions"""
from pydantic import Field
from mcp.server.mcpserver import MCPServer
# Create server
mcp = MCPServer("Parameter Descriptions Server")
@mcp.tool()
def greet_user(
name: str = Field(description="The name of the person to greet"),
title: str = Field(description="Optional title like Mr/Ms/Dr", default=""),
times: int = Field(description="Number of times to repeat the greeting", default=1),
) -> str:
"""Greet a user with optional title and repetition"""
greeting = f"Hello {title + ' ' if title else ''}{name}!"
return "\n".join([greeting] * times)
+18
View File
@@ -0,0 +1,18 @@
from mcp.server.mcpserver import MCPServer
# Create an MCP server
mcp = MCPServer("Demo")
# Add an addition tool
@mcp.tool()
def sum(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}!"
+27
View File
@@ -0,0 +1,27 @@
"""MCPServer Screenshot Example
Give Claude a tool to capture and view screenshots.
"""
import io
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.utilities.types import Image
# Create server
mcp = MCPServer("Screenshot Demo")
@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")
+12
View File
@@ -0,0 +1,12 @@
"""MCPServer Echo Server"""
from mcp.server.mcpserver import MCPServer
# Create server
mcp = MCPServer("Echo Server")
@mcp.tool()
def echo(text: str) -> str:
"""Echo the input text"""
return text
+67
View File
@@ -0,0 +1,67 @@
# /// script
# dependencies = []
# ///
"""MCPServer Text Me Server
--------------------------------
This defines a simple MCPServer 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.mcpserver import MCPServer
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 = MCPServer("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}"
+59
View File
@@ -0,0 +1,59 @@
"""Example MCPServer server that uses Unicode characters in various places to help test
Unicode handling in tools and inspectors.
"""
from mcp.server.mcpserver import MCPServer
mcp = MCPServer()
@mcp.tool(description="🌟 A tool that uses various Unicode characters in its description: á é í ó ú ñ 漢字 🎉")
def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str:
"""A simple tool that demonstrates Unicode handling in:
- Tool description (emojis, accents, CJK characters)
- Parameter defaults (CJK characters)
- Return values (Spanish punctuation, emojis)
"""
return f"{greeting}, {name}! 👋"
@mcp.tool(description="🎨 Tool that returns a list of emoji categories")
def list_emoji_categories() -> list[str]:
"""Returns a list of emoji categories with emoji examples."""
return [
"😀 Smileys & Emotion",
"👋 People & Body",
"🐶 Animals & Nature",
"🍎 Food & Drink",
"⚽ Activities",
"🌍 Travel & Places",
"💡 Objects",
"❤️ Symbols",
"🚩 Flags",
]
@mcp.tool(description="🔤 Tool that returns text in different scripts")
def multilingual_hello() -> str:
"""Returns hello in different scripts and writing systems."""
return "\n".join(
[
"English: Hello!",
"Spanish: ¡Hola!",
"French: Bonjour!",
"German: Grüß Gott!",
"Russian: Привет!",
"Greek: Γεια σας!",
"Hebrew: !שָׁלוֹם",
"Arabic: !مرحبا",
"Hindi: नमस्ते!",
"Chinese: 你好!",
"Japanese: こんにちは!",
"Korean: 안녕하세요!",
"Thai: สวัสดี!",
]
)
if __name__ == "__main__":
mcp.run()
+224
View File
@@ -0,0 +1,224 @@
"""MCPServer Weather Example with Structured Output
Demonstrates how to use structured output with tools to return
well-typed, validated data that clients can easily process.
"""
import asyncio
import json
import sys
from dataclasses import dataclass
from datetime import datetime
from typing import TypedDict
from pydantic import BaseModel, Field
from mcp.client import Client
from mcp.server.mcpserver import MCPServer
# Create server
mcp = MCPServer("Weather Service")
# Example 1: Using a Pydantic model for structured output
class WeatherData(BaseModel):
"""Structured weather data response"""
temperature: float = Field(description="Temperature in Celsius")
humidity: float = Field(description="Humidity percentage (0-100)")
condition: str = Field(description="Weather condition (sunny, cloudy, rainy, etc.)")
wind_speed: float = Field(description="Wind speed in km/h")
location: str = Field(description="Location name")
timestamp: datetime = Field(default_factory=datetime.now, description="Observation time")
@mcp.tool()
def get_weather(city: str) -> WeatherData:
"""Get current weather for a city with full structured data"""
# In a real implementation, this would fetch from a weather API
return WeatherData(temperature=22.5, humidity=65.0, condition="partly cloudy", wind_speed=12.3, location=city)
# Example 2: Using TypedDict for a simpler structure
class WeatherSummary(TypedDict):
"""Simple weather summary"""
city: str
temp_c: float
description: str
@mcp.tool()
def get_weather_summary(city: str) -> WeatherSummary:
"""Get a brief weather summary for a city"""
return WeatherSummary(city=city, temp_c=22.5, description="Partly cloudy with light breeze")
# Example 3: Using dict[str, Any] for flexible schemas
@mcp.tool()
def get_weather_metrics(cities: list[str]) -> dict[str, dict[str, float]]:
"""Get weather metrics for multiple cities
Returns a dictionary mapping city names to their metrics
"""
# Returns nested dictionaries with weather metrics
return {
city: {"temperature": 20.0 + i * 2, "humidity": 60.0 + i * 5, "pressure": 1013.0 + i * 0.5}
for i, city in enumerate(cities)
}
# Example 4: Using dataclass for weather alerts
@dataclass
class WeatherAlert:
"""Weather alert information"""
severity: str # "low", "medium", "high"
title: str
description: str
affected_areas: list[str]
valid_until: datetime
@mcp.tool()
def get_weather_alerts(region: str) -> list[WeatherAlert]:
"""Get active weather alerts for a region"""
# In production, this would fetch real alerts
if region.lower() == "california":
return [
WeatherAlert(
severity="high",
title="Heat Wave Warning",
description="Temperatures expected to exceed 40 degrees",
affected_areas=["Los Angeles", "San Diego", "Riverside"],
valid_until=datetime(2024, 7, 15, 18, 0),
),
WeatherAlert(
severity="medium",
title="Air Quality Advisory",
description="Poor air quality due to wildfire smoke",
affected_areas=["San Francisco Bay Area"],
valid_until=datetime(2024, 7, 14, 12, 0),
),
]
return []
# Example 5: Returning primitives with structured output
@mcp.tool()
def get_temperature(city: str, unit: str = "celsius") -> float:
"""Get just the temperature for a city
When returning primitives as structured output,
the result is wrapped in {"result": value}
"""
base_temp = 22.5
if unit.lower() == "fahrenheit":
return base_temp * 9 / 5 + 32
return base_temp
# Example 6: Weather statistics with nested models
class DailyStats(BaseModel):
"""Statistics for a single day"""
high: float
low: float
mean: float
class WeatherStats(BaseModel):
"""Weather statistics over a period"""
location: str
period_days: int
temperature: DailyStats
humidity: DailyStats
precipitation_mm: float = Field(description="Total precipitation in millimeters")
@mcp.tool()
def get_weather_stats(city: str, days: int = 7) -> WeatherStats:
"""Get weather statistics for the past N days"""
return WeatherStats(
location=city,
period_days=days,
temperature=DailyStats(high=28.5, low=15.2, mean=21.8),
humidity=DailyStats(high=85.0, low=45.0, mean=65.0),
precipitation_mm=12.4,
)
if __name__ == "__main__":
async def test() -> None:
"""Test the tools by calling them through the server as a client would"""
print("Testing Weather Service Tools (via MCP protocol)\n")
print("=" * 80)
async with Client(mcp) as client:
# Test get_weather
result = await client.call_tool("get_weather", {"city": "London"})
print("\nWeather in London:")
print(json.dumps(result.structured_content, indent=2))
# Test get_weather_summary
result = await client.call_tool("get_weather_summary", {"city": "Paris"})
print("\nWeather summary for Paris:")
print(json.dumps(result.structured_content, indent=2))
# Test get_weather_metrics
result = await client.call_tool("get_weather_metrics", {"cities": ["Tokyo", "Sydney", "Mumbai"]})
print("\nWeather metrics:")
print(json.dumps(result.structured_content, indent=2))
# Test get_weather_alerts
result = await client.call_tool("get_weather_alerts", {"region": "California"})
print("\nWeather alerts for California:")
print(json.dumps(result.structured_content, indent=2))
# Test get_temperature
result = await client.call_tool("get_temperature", {"city": "Berlin", "unit": "fahrenheit"})
print("\nTemperature in Berlin:")
print(json.dumps(result.structured_content, indent=2))
# Test get_weather_stats
result = await client.call_tool("get_weather_stats", {"city": "Seattle", "days": 30})
print("\nWeather stats for Seattle (30 days):")
print(json.dumps(result.structured_content, indent=2))
# Also show the text content for comparison
print("\nText content for last result:")
for content in result.content:
if content.type == "text":
print(content.text)
async def print_schemas() -> None:
"""Print all tool schemas"""
print("Tool Schemas for Weather Service\n")
print("=" * 80)
tools = await mcp.list_tools()
for tool in tools:
print(f"\nTool: {tool.name}")
print(f"Description: {tool.description}")
print("Input Schema:")
print(json.dumps(tool.input_schema, indent=2))
if tool.output_schema:
print("Output Schema:")
print(json.dumps(tool.output_schema, indent=2))
else:
print("Output Schema: None (returns unstructured content)")
print("-" * 80)
# Check command line arguments
if len(sys.argv) > 1 and sys.argv[1] == "--schemas":
asyncio.run(print_schemas())
else:
print("Usage:")
print(" python weather_structured.py # Run tool tests")
print(" python weather_structured.py --schemas # Print tool schemas")
print()
asyncio.run(test())
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "mcp-example-stories"
version = "0.0.0"
description = "Self-verifying example suite for the MCP Python SDK (dev-only, not published)"
requires-python = ">=3.10"
dependencies = [
"mcp",
"tomli>=2.0; python_version < '3.11'",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["stories"]
@@ -0,0 +1,42 @@
# MCP Everything Server
A comprehensive MCP server implementing all protocol features for conformance testing.
## Overview
The Everything Server is a reference implementation that demonstrates all features of the Model Context Protocol (MCP). It is designed to be used with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) to validate MCP client and server implementations.
## Installation
From the python-sdk root directory:
```bash
uv sync --frozen
```
## Usage
### Running the Server
Start the server with default settings (port 3001):
```bash
uv run -m mcp_everything_server
```
Or with custom options:
```bash
uv run -m mcp_everything_server --port 3001 --log-level DEBUG
```
The server will be available at: `http://localhost:3001/mcp`
### Command-Line Options
- `--port` - Port to listen on (default: 3001)
- `--log-level` - Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
## Running Conformance Tests
See the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) for instructions on running conformance tests against this server.
@@ -0,0 +1,3 @@
"""MCP Everything Server - Comprehensive conformance test server."""
__version__ = "0.1.0"
@@ -0,0 +1,6 @@
"""CLI entry point for the MCP Everything Server."""
from .server import main
if __name__ == "__main__":
main()
@@ -0,0 +1,787 @@
#!/usr/bin/env python3
"""MCP Everything Server - Conformance Test Server
Server implementing all MCP features for conformance testing based on Conformance Server Specification.
"""
import asyncio
import base64
import json
import logging
from typing import Annotated, Any
import click
from mcp.server import ServerRequestContext
from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
from mcp.shared.exceptions import MCPError
from mcp_types import (
AudioContent,
Completion,
CompletionArgument,
CompletionContext,
CreateMessageRequest,
CreateMessageRequestParams,
CreateMessageResult,
ElicitRequest,
ElicitRequestFormParams,
ElicitResult,
EmbeddedResource,
EmptyResult,
ImageContent,
InputRequest,
InputRequiredResult,
JSONRPCMessage,
ListRootsRequest,
ListRootsResult,
PromptReference,
ResourceTemplateReference,
SamplingMessage,
SetLevelRequestParams,
SubscribeRequestParams,
TextContent,
TextResourceContents,
UnsubscribeRequestParams,
)
from mcp_types.jsonrpc import MISSING_REQUIRED_CLIENT_CAPABILITY
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# Type aliases for event store
StreamId = str
EventId = str
class InMemoryEventStore(EventStore):
"""Simple in-memory event store for SSE resumability testing."""
def __init__(self) -> None:
self._events: list[tuple[StreamId, EventId, JSONRPCMessage | None]] = []
self._event_id_counter = 0
async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:
"""Store an event and return its ID."""
self._event_id_counter += 1
event_id = str(self._event_id_counter)
self._events.append((stream_id, event_id, message))
return event_id
async def replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None:
"""Replay events after the specified ID."""
target_stream_id = None
for stream_id, event_id, _ in self._events:
if event_id == last_event_id:
target_stream_id = stream_id
break
if target_stream_id is None:
return None
last_event_id_int = int(last_event_id)
for stream_id, event_id, message in self._events:
if stream_id == target_stream_id and int(event_id) > last_event_id_int:
# Skip priming events (None message)
if message is not None:
await send_callback(EventMessage(message, event_id))
return target_stream_id
# Test data
TEST_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
TEST_AUDIO_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
# Server state
resource_subscriptions: set[str] = set()
watched_resource_content = "Watched resource content"
# Create event store for SSE resumability (SEP-1699)
event_store = InMemoryEventStore()
# Fixed fixture key (RequestStateSecurity requires at least 32 bytes); a real deployment would load a shared secret.
_REQUEST_STATE_KEY = b"everything-server-fixture-request-state-key"
mcp = MCPServer(
name="mcp-conformance-test-server",
request_state_security=RequestStateSecurity(keys=[_REQUEST_STATE_KEY]),
)
# Tools
@mcp.tool()
def test_simple_text() -> str:
"""Tests simple text content response"""
return "This is a simple text response for testing."
@mcp.tool()
def test_image_content() -> list[ImageContent]:
"""Tests image content response"""
return [ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png")]
@mcp.tool()
def test_audio_content() -> list[AudioContent]:
"""Tests audio content response"""
return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mime_type="audio/wav")]
@mcp.tool()
def test_embedded_resource() -> list[EmbeddedResource]:
"""Tests embedded resource content response"""
return [
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri="test://embedded-resource",
mime_type="text/plain",
text="This is an embedded resource content.",
),
)
]
@mcp.tool()
def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedResource]:
"""Tests response with multiple content types (text, image, resource)"""
return [
TextContent(type="text", text="Multiple content types test:"),
ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png"),
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri="test://mixed-content-resource",
mime_type="application/json",
text='{"test": "data", "value": 123}',
),
),
]
@mcp.tool()
async def test_tool_with_logging(ctx: Context) -> str:
"""Tests tool that emits log messages during execution"""
await ctx.info("Tool execution started") # pyright: ignore[reportDeprecated]
await asyncio.sleep(0.05)
await ctx.info("Tool processing data") # pyright: ignore[reportDeprecated]
await asyncio.sleep(0.05)
await ctx.info("Tool execution completed") # pyright: ignore[reportDeprecated]
return "Tool with logging executed successfully"
@mcp.tool()
async def test_tool_with_progress(ctx: Context) -> str:
"""Tests tool that reports progress notifications"""
await ctx.report_progress(progress=0, total=100, message="Completed step 0 of 100")
await asyncio.sleep(0.05)
await ctx.report_progress(progress=50, total=100, message="Completed step 50 of 100")
await asyncio.sleep(0.05)
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
# Return progress token as string
progress_token = (
ctx.request_context.meta.get("progress_token") if ctx.request_context and ctx.request_context.meta else 0
)
return str(progress_token)
@mcp.tool()
async def test_sampling(prompt: str, ctx: Context) -> str:
"""Tests server-initiated sampling (LLM completion request)"""
try:
# Request sampling from client. Without related_request_id the request goes
# to the standalone GET stream and is silently dropped if it is not open yet.
result = await ctx.session.create_message( # pyright: ignore[reportDeprecated]
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=100,
related_request_id=ctx.request_id,
)
# Since we're not passing tools param, result.content is single content
if result.content.type == "text":
model_response = result.content.text
else:
model_response = "No response"
return f"LLM response: {model_response}"
except Exception as e:
return f"Sampling not supported or error: {str(e)}"
class UserResponse(BaseModel):
response: str = Field(description="User's response")
@mcp.tool()
async def test_elicitation(message: str, ctx: Context) -> str:
"""Tests server-initiated elicitation (user input request)"""
try:
# Request user input from client
result = await ctx.elicit(message=message, schema=UserResponse)
# Type-safe discriminated union narrowing using action field
if result.action == "accept":
content = result.data.model_dump_json()
else: # decline or cancel
content = "{}"
return f"User response: action={result.action}, content={content}"
except Exception as e:
return f"Elicitation not supported or error: {str(e)}"
class SEP1034DefaultsSchema(BaseModel):
"""Schema for testing SEP-1034 elicitation with default values for all primitive types"""
name: str = Field(default="John Doe", description="User name")
age: int = Field(default=30, description="User age")
score: float = Field(default=95.5, description="User score")
status: str = Field(
default="active",
description="User status",
json_schema_extra={"enum": ["active", "inactive", "pending"]},
)
verified: bool = Field(default=True, description="Verification status")
@mcp.tool()
async def test_elicitation_sep1034_defaults(ctx: Context) -> str:
"""Tests elicitation with default values for all primitive types (SEP-1034)"""
try:
# Request user input with defaults for all primitive types
result = await ctx.elicit(message="Please provide user information", schema=SEP1034DefaultsSchema)
# Type-safe discriminated union narrowing using action field
if result.action == "accept":
content = result.data.model_dump_json()
else: # decline or cancel
content = "{}"
return f"Elicitation result: action={result.action}, content={content}"
except Exception as e:
return f"Elicitation not supported or error: {str(e)}"
class EnumSchemasTestSchema(BaseModel):
"""Schema for testing enum schema variations (SEP-1330)"""
untitledSingle: str = Field(
description="Simple enum without titles", json_schema_extra={"enum": ["active", "inactive", "pending"]}
)
titledSingle: str = Field(
description="Enum with titled options (oneOf)",
json_schema_extra={
"oneOf": [
{"const": "low", "title": "Low Priority"},
{"const": "medium", "title": "Medium Priority"},
{"const": "high", "title": "High Priority"},
]
},
)
untitledMulti: list[str] = Field(
description="Multi-select without titles",
json_schema_extra={"items": {"type": "string", "enum": ["read", "write", "execute"]}},
)
titledMulti: list[str] = Field(
description="Multi-select with titled options",
json_schema_extra={
"items": {
"anyOf": [
{"const": "feature", "title": "New Feature"},
{"const": "bug", "title": "Bug Fix"},
{"const": "docs", "title": "Documentation"},
]
}
},
)
legacyEnum: str = Field(
description="Legacy enum with enumNames",
json_schema_extra={
"enum": ["small", "medium", "large"],
"enumNames": ["Small Size", "Medium Size", "Large Size"],
},
)
@mcp.tool()
async def test_elicitation_sep1330_enums(ctx: Context) -> str:
"""Tests elicitation with enum schema variations per SEP-1330"""
try:
result = await ctx.elicit(
message="Please select values using different enum schema types", schema=EnumSchemasTestSchema
)
if result.action == "accept":
content = result.data.model_dump_json()
else:
content = "{}"
return f"Elicitation completed: action={result.action}, content={content}"
except Exception as e:
return f"Elicitation not supported or error: {str(e)}"
@mcp.tool()
def test_error_handling() -> str:
"""Tests error response handling"""
raise RuntimeError("This tool intentionally returns an error for testing")
@mcp.tool()
def test_x_mcp_header(
region: Annotated[
str,
Field(
description="Mirrored into the Mcp-Param-Region header",
json_schema_extra={"x-mcp-header": "Region"},
),
] = "<none>",
) -> str:
"""Tests SEP-2243 Mcp-Param-* server-side validation.
Arms the http-custom-header-server-validation conformance scenario, which
skips when no tool with an `x-mcp-header` annotation is found.
"""
return f"region={region}"
@mcp.tool()
async def test_missing_capability(ctx: Context) -> str:
"""Tests that a handler-raised MISSING_REQUIRED_CLIENT_CAPABILITY surfaces as a top-level JSON-RPC error.
Requires the client to declare the ``sampling`` capability. When absent, raises
`MCPError` (which the tool dispatch re-raises rather than wrapping in
``CallToolResult.isError``) so the conformance harness observes a protocol-level
error response with ``data.requiredCapabilities``.
"""
client_params = ctx.session.client_params
sampling_declared = client_params is not None and client_params.capabilities.sampling is not None
if not sampling_declared:
raise MCPError(
code=MISSING_REQUIRED_CLIENT_CAPABILITY,
message="This tool requires the client 'sampling' capability",
data={"requiredCapabilities": ["sampling"]},
)
return "Client declared sampling capability; proceeding."
# SEP-2322 InputRequiredResult fixtures (multi-round-trip / ephemeral workflow)
NAME_SCHEMA = {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}
def _name_elicitation(message: str = "What is your name?") -> ElicitRequest:
return ElicitRequest(params=ElicitRequestFormParams(message=message, requested_schema=NAME_SCHEMA))
@mcp.tool()
async def test_input_required_result_elicitation(ctx: Context) -> str | InputRequiredResult:
"""Tests InputRequiredResult with a single elicitation request"""
responses = ctx.input_responses
if responses and "user_name" in responses:
answer = responses["user_name"]
name = answer.content.get("name", "stranger") if isinstance(answer, ElicitResult) and answer.content else "?"
return f"Hello, {name}!"
return InputRequiredResult(input_requests={"user_name": _name_elicitation()})
@mcp.tool()
async def test_input_required_result_sampling(ctx: Context) -> str | InputRequiredResult:
"""Tests InputRequiredResult with a single sampling request"""
responses = ctx.input_responses
if responses and "capital_question" in responses:
answer = responses["capital_question"]
text = answer.content.text if isinstance(answer, CreateMessageResult) and answer.content.type == "text" else "?"
return f"Model said: {text}"
return InputRequiredResult(
input_requests={
"capital_question": CreateMessageRequest(
params=CreateMessageRequestParams(
messages=[
SamplingMessage(
role="user", content=TextContent(type="text", text="What is the capital of France?")
)
],
max_tokens=100,
)
)
}
)
@mcp.tool()
async def test_input_required_result_list_roots(ctx: Context) -> str | InputRequiredResult:
"""Tests InputRequiredResult with a single roots/list request"""
responses = ctx.input_responses
if responses and "client_roots" in responses:
answer = responses["client_roots"]
count = len(answer.roots) if isinstance(answer, ListRootsResult) else 0
return f"Client exposed {count} root(s)."
return InputRequiredResult(input_requests={"client_roots": ListRootsRequest()})
@mcp.tool()
async def test_input_required_result_request_state(ctx: Context) -> str | InputRequiredResult:
"""Tests requestState round-tripping in the InputRequiredResult flow"""
responses = ctx.input_responses
if responses and "confirm" in responses and ctx.request_state == "request-state-nonce":
return "state-ok: confirmation received"
confirm = ElicitRequest(
params=ElicitRequestFormParams(
message="Please confirm",
requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"]},
)
)
return InputRequiredResult(input_requests={"confirm": confirm}, request_state="request-state-nonce")
@mcp.tool()
async def test_input_required_result_multiple_inputs(ctx: Context) -> str | InputRequiredResult:
"""Tests InputRequiredResult carrying elicitation, sampling and roots requests together"""
responses = ctx.input_responses
if responses and {"user_name", "greeting", "client_roots"} <= responses.keys():
return "All inputs received."
return InputRequiredResult(
input_requests={
"user_name": _name_elicitation(),
"greeting": CreateMessageRequest(
params=CreateMessageRequestParams(
messages=[
SamplingMessage(role="user", content=TextContent(type="text", text="Generate a greeting"))
],
max_tokens=50,
)
),
"client_roots": ListRootsRequest(),
},
request_state="multiple-inputs",
)
@mcp.tool()
async def test_input_required_result_multi_round(ctx: Context) -> str | InputRequiredResult:
"""Tests a three-round InputRequiredResult flow with evolving requestState"""
state = json.loads(ctx.request_state) if ctx.request_state else {"round": 0}
responses = ctx.input_responses or {}
if state["round"] == 0:
return InputRequiredResult(
input_requests={"step1": _name_elicitation("Step 1: What is your name?")},
request_state=json.dumps({"round": 1}),
)
if state["round"] == 1 and "step1" in responses:
step1 = responses["step1"]
name = step1.content.get("name") if isinstance(step1, ElicitResult) and step1.content else None
color_schema = {"type": "object", "properties": {"color": {"type": "string"}}, "required": ["color"]}
return InputRequiredResult(
input_requests={
"step2": ElicitRequest(
params=ElicitRequestFormParams(
message="Step 2: What is your favorite color?", requested_schema=color_schema
)
)
},
request_state=json.dumps({"round": 2, "name": name}),
)
if state["round"] == 2 and "step2" in responses:
step2 = responses["step2"]
color = step2.content.get("color") if isinstance(step2, ElicitResult) and step2.content else None
return f"{state.get('name')} likes {color}."
# Missing or out-of-order response: re-request from the start.
return InputRequiredResult(
input_requests={"step1": _name_elicitation("Step 1: What is your name?")},
request_state=json.dumps({"round": 1}),
)
@mcp.tool()
async def test_input_required_result_tampered_state(ctx: Context) -> str | InputRequiredResult:
"""Tests that the server rejects a tampered requestState echo.
The handler stays plaintext; tamper rejection happens in the SDK's request-state boundary.
"""
if ctx.request_state is None:
confirm = ElicitRequest(
params=ElicitRequestFormParams(
message="Please confirm",
requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"]},
)
)
return InputRequiredResult(input_requests={"confirm": confirm}, request_state="round-1")
return f"state-ok: {ctx.request_state}"
@mcp.tool()
async def test_input_required_result_capabilities(ctx: Context) -> InputRequiredResult:
"""Tests that inputRequests only include methods the client declared support for"""
caps = ctx.client_capabilities
requests: dict[str, InputRequest] = {}
if caps is None or caps.sampling is not None:
requests["sample"] = CreateMessageRequest(
params=CreateMessageRequestParams(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text="Say hello"))],
max_tokens=50,
)
)
if caps is None or caps.elicitation is not None:
requests["ask"] = _name_elicitation()
return InputRequiredResult(input_requests=requests, request_state="capability-gated")
# SEP-1613 / SEP-2106 JSON Schema 2020-12 fixture: a tool whose inputSchema carries
# the full set of 2020-12 keywords the conformance scenario asserts on.
JSON_SCHEMA_2020_12_INPUT_SCHEMA: dict[str, Any] = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"$defs": {
"address": {
"$anchor": "addressDef",
"type": "object",
"properties": {"street": {"type": "string"}, "city": {"type": "string"}},
}
},
"properties": {
"name": {"type": "string"},
"address": {"$ref": "#/$defs/address"},
"contactMethod": {"type": "string", "enum": ["phone", "email"]},
"phone": {"type": "string"},
"email": {"type": "string"},
},
"allOf": [{"anyOf": [{"required": ["phone"]}, {"required": ["email"]}]}],
"if": {"properties": {"contactMethod": {"const": "phone"}}, "required": ["contactMethod"]},
"then": {"required": ["phone"]},
"else": {"required": ["email"]},
"additionalProperties": False,
}
@mcp.tool(name="json_schema_2020_12_tool")
def json_schema_2020_12_tool() -> str:
"""Tests JSON Schema 2020-12 keyword preservation in tools/list (inputSchema installed below)."""
return "json_schema_2020_12_tool"
# TODO(felix): replace with a public input_schema= override once MCPServer.tool() grows one.
mcp._tool_manager._tools["json_schema_2020_12_tool"].parameters = ( # pyright: ignore[reportPrivateUsage]
JSON_SCHEMA_2020_12_INPUT_SCHEMA
)
@mcp.tool()
async def test_reconnection(ctx: Context) -> str:
"""Tests SSE polling by closing stream mid-call (SEP-1699)"""
await ctx.info("Before disconnect") # pyright: ignore[reportDeprecated]
await ctx.close_sse_stream()
await asyncio.sleep(0.2) # Wait for client to reconnect
await ctx.info("After reconnect") # pyright: ignore[reportDeprecated]
return "Reconnection test completed"
def _dynamic_tool() -> str:
"""A tool registered and removed by test_trigger_tool_change."""
return "dynamic"
def _dynamic_prompt() -> str:
"""A prompt registered and removed by test_trigger_prompt_change."""
return "dynamic"
@mcp.tool()
async def test_trigger_tool_change(ctx: Context) -> str:
"""Mutates the tool list and announces it to subscriptions/listen streams (SEP-2575)"""
mcp.add_tool(_dynamic_tool, name="test_dynamic_tool")
mcp.remove_tool("test_dynamic_tool")
await ctx.notify_tools_changed()
return "tool list changed"
@mcp.tool()
async def test_trigger_prompt_change(ctx: Context) -> str:
"""Mutates the prompt list and announces it to subscriptions/listen streams (SEP-2575)"""
mcp.add_prompt(Prompt.from_function(_dynamic_prompt, name="test_dynamic_prompt", description="dynamic"))
mcp.remove_prompt("test_dynamic_prompt")
await ctx.notify_prompts_changed()
return "prompt list changed"
# Resources
@mcp.resource("test://static-text")
def static_text_resource() -> str:
"""A static text resource for testing"""
return "This is the content of the static text resource."
@mcp.resource("test://static-binary")
def static_binary_resource() -> bytes:
"""A static binary resource (image) for testing"""
return base64.b64decode(TEST_IMAGE_BASE64)
@mcp.resource("test://template/{id}/data")
def template_resource(id: str) -> str:
"""A resource template with parameter substitution"""
return json.dumps({"id": id, "templateTest": True, "data": f"Data for ID: {id}"})
@mcp.resource("test://watched-resource")
def watched_resource() -> str:
"""A resource that can be subscribed to for updates"""
return watched_resource_content
# Prompts
@mcp.prompt()
def test_simple_prompt() -> list[UserMessage]:
"""A simple prompt without arguments"""
return [UserMessage(role="user", content=TextContent(type="text", text="This is a simple prompt for testing."))]
@mcp.prompt()
def test_prompt_with_arguments(arg1: str, arg2: str) -> list[UserMessage]:
"""A prompt with required arguments"""
return [
UserMessage(
role="user", content=TextContent(type="text", text=f"Prompt with arguments: arg1='{arg1}', arg2='{arg2}'")
)
]
@mcp.prompt()
def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
"""A prompt that includes an embedded resource"""
return [
UserMessage(
role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=resourceUri,
mime_type="text/plain",
text="Embedded resource content for testing.",
),
),
),
UserMessage(role="user", content=TextContent(type="text", text="Please process the embedded resource above.")),
]
@mcp.prompt()
def test_prompt_with_image() -> list[UserMessage]:
"""A prompt that includes image content"""
return [
UserMessage(role="user", content=ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png")),
UserMessage(role="user", content=TextContent(type="text", text="Please analyze the image above.")),
]
@mcp.prompt()
async def test_input_required_result_prompt(ctx: Context) -> list[UserMessage] | InputRequiredResult:
"""Tests InputRequiredResult from prompts/get (SEP-2322 non-tool request)"""
responses = ctx.input_responses
if responses and "user_context" in responses:
answer = responses["user_context"]
text = answer.content.get("context", "?") if isinstance(answer, ElicitResult) and answer.content else "?"
return [UserMessage(role="user", content=TextContent(type="text", text=f"Use the following context: {text}"))]
return InputRequiredResult(
input_requests={
"user_context": ElicitRequest(
params=ElicitRequestFormParams(
message="What context should the prompt use?",
requested_schema={
"type": "object",
"properties": {"context": {"type": "string"}},
"required": ["context"],
},
)
)
}
)
# Custom request handlers
# TODO(felix): Add public APIs to MCPServer for subscribe_resource, unsubscribe_resource,
# and set_logging_level to avoid accessing protected _lowlevel_server attribute.
async def handle_set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestParams) -> EmptyResult:
"""Handle logging level changes"""
logger.info(f"Log level set to: {params.level}")
return EmptyResult()
async def handle_subscribe(ctx: ServerRequestContext, params: SubscribeRequestParams) -> EmptyResult:
"""Handle resource subscription"""
resource_subscriptions.add(str(params.uri))
logger.info(f"Subscribed to resource: {params.uri}")
return EmptyResult()
async def handle_unsubscribe(ctx: ServerRequestContext, params: UnsubscribeRequestParams) -> EmptyResult:
"""Handle resource unsubscription"""
resource_subscriptions.discard(str(params.uri))
logger.info(f"Unsubscribed from resource: {params.uri}")
return EmptyResult()
mcp._lowlevel_server.add_request_handler( # pyright: ignore[reportPrivateUsage]
"logging/setLevel", SetLevelRequestParams, handle_set_logging_level
)
mcp._lowlevel_server.add_request_handler( # pyright: ignore[reportPrivateUsage]
"resources/subscribe", SubscribeRequestParams, handle_subscribe
)
mcp._lowlevel_server.add_request_handler( # pyright: ignore[reportPrivateUsage]
"resources/unsubscribe", UnsubscribeRequestParams, handle_unsubscribe
)
@mcp.completion()
async def _handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion:
"""Handle completion requests"""
# Basic completion support - returns empty array for conformance
# Real implementations would provide contextual suggestions
return Completion(values=[], total=0, has_more=False)
# CLI
@click.command()
@click.option("--port", default=3001, help="Port to listen on for HTTP")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
def main(port: int, log_level: str) -> int:
"""Run the MCP Everything Server."""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger.info(f"Starting MCP Everything Server on port {port}")
logger.info(f"Endpoint will be: http://localhost:{port}/mcp")
mcp.run(
transport="streamable-http",
port=port,
event_store=event_store,
retry_interval=100, # 100ms retry interval for SSE polling
)
return 0
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
[project]
name = "mcp-everything-server"
version = "0.1.0"
description = "Comprehensive MCP server implementing all protocol features for conformance testing"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "conformance", "testing"]
license = { text = "MIT" }
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-everything-server = "mcp_everything_server.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_everything_server"]
[tool.pyright]
include = ["mcp_everything_server"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
+135
View File
@@ -0,0 +1,135 @@
# MCP OAuth Authentication Demo
This example demonstrates OAuth 2.0 authentication with the Model Context Protocol using **separate Authorization Server (AS) and Resource Server (RS)** to comply with the new RFC 9728 specification.
---
## Running the Servers
### Step 1: Start Authorization Server
```bash
# Navigate to the simple-auth directory
cd examples/servers/simple-auth
# Start Authorization Server on port 9000
uv run mcp-simple-auth-as --port=9000
```
**What it provides:**
- OAuth 2.0 flows (registration, authorization, token exchange)
- Simple credential-based authentication (no external provider needed)
- Token introspection endpoint for Resource Servers (`/introspect`)
---
### Step 2: Start Resource Server (MCP Server)
```bash
# In another terminal, navigate to the simple-auth directory
cd examples/servers/simple-auth
# Start Resource Server on port 8001, connected to Authorization Server
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
# With RFC 8707 strict resource validation (recommended for production)
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http --oauth-strict
```
### Step 3: Test with Client
```bash
cd examples/clients/simple-auth-client
# Start client with streamable HTTP
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
```
## How It Works
### RFC 9728 Discovery
**Client → Resource Server:**
```bash
curl http://localhost:8001/.well-known/oauth-protected-resource
```
```json
{
"resource": "http://localhost:8001",
"authorization_servers": ["http://localhost:9000"]
}
```
**Client → Authorization Server:**
```bash
curl http://localhost:9000/.well-known/oauth-authorization-server
```
```json
{
"issuer": "http://localhost:9000",
"authorization_endpoint": "http://localhost:9000/authorize",
"token_endpoint": "http://localhost:9000/token"
}
```
## Legacy MCP Server as Authorization Server (Backwards Compatibility)
For backwards compatibility with older MCP implementations, a legacy server is provided that acts as an Authorization Server (following the old spec where MCP servers could optionally provide OAuth):
### Running the Legacy Server
```bash
# Start legacy server on port 8000 (the default)
cd examples/servers/simple-auth
uv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http
```
**Differences from the new architecture:**
- **MCP server acts as AS:** The MCP server itself provides OAuth endpoints (old spec behavior)
- **No separate RS:** The server handles both authentication and MCP tools
- **Local token validation:** Tokens are validated internally without introspection
- **No RFC 9728 support:** Does not provide `/.well-known/oauth-protected-resource`
- **Direct OAuth discovery:** OAuth metadata is at the MCP server's URL
### Testing with Legacy Server
```bash
# Test with client (will automatically fall back to legacy discovery)
cd examples/clients/simple-auth-client
MCP_SERVER_PORT=8000 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
```
The client will:
1. Try RFC 9728 discovery at `/.well-known/oauth-protected-resource` (404 on legacy server)
2. Fall back to direct OAuth discovery at `/.well-known/oauth-authorization-server`
3. Complete authentication with the MCP server acting as its own AS
This ensures existing MCP servers (which could optionally act as Authorization Servers under the old spec) continue to work while the ecosystem transitions to the new architecture where MCP servers are Resource Servers only.
## Manual Testing
### Test Discovery
```bash
# Test Resource Server discovery endpoint (new architecture)
curl -v http://localhost:8001/.well-known/oauth-protected-resource
# Test Authorization Server metadata
curl -v http://localhost:9000/.well-known/oauth-authorization-server
```
### Test Token Introspection
```bash
# After getting a token through OAuth flow:
curl -X POST http://localhost:9000/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=your_access_token"
```
@@ -0,0 +1 @@
"""Simple MCP server with GitHub OAuth authentication."""
@@ -0,0 +1,7 @@
"""Main entry point for simple MCP server with GitHub OAuth authentication."""
import sys
from mcp_simple_auth.server import main
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,185 @@
"""Authorization Server for MCP Split Demo.
This server handles OAuth flows, client registration, and token issuance.
Can be replaced with enterprise authorization servers like Auth0, Entra ID, etc.
NOTE: this is a simplified example for demonstration purposes.
This is not a production-ready implementation.
"""
import asyncio
import logging
import time
import click
from pydantic import AnyHttpUrl, BaseModel
from starlette.applications import Starlette
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route
from uvicorn import Config, Server
from mcp.server.auth.routes import cors_middleware, create_auth_routes
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
from .simple_auth_provider import SimpleAuthSettings, SimpleOAuthProvider
logger = logging.getLogger(__name__)
class AuthServerSettings(BaseModel):
"""Settings for the Authorization Server."""
# Server settings
host: str = "localhost"
port: int = 9000
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:9000")
auth_callback_path: str = "http://localhost:9000/login/callback"
class SimpleAuthProvider(SimpleOAuthProvider):
"""Authorization Server provider with simple demo authentication.
This provider:
1. Issues MCP tokens after simple credential authentication
2. Stores token state for introspection by Resource Servers
"""
def __init__(self, auth_settings: SimpleAuthSettings, auth_callback_path: str, server_url: str):
super().__init__(auth_settings, auth_callback_path, server_url)
def create_authorization_server(server_settings: AuthServerSettings, auth_settings: SimpleAuthSettings) -> Starlette:
"""Create the Authorization Server application."""
oauth_provider = SimpleAuthProvider(
auth_settings, server_settings.auth_callback_path, str(server_settings.server_url)
)
mcp_auth_settings = AuthSettings(
issuer_url=server_settings.server_url,
client_registration_options=ClientRegistrationOptions(
enabled=True,
valid_scopes=[auth_settings.mcp_scope],
default_scopes=[auth_settings.mcp_scope],
),
required_scopes=[auth_settings.mcp_scope],
resource_server_url=None,
)
# Create OAuth routes
routes = create_auth_routes(
provider=oauth_provider,
issuer_url=mcp_auth_settings.issuer_url,
service_documentation_url=mcp_auth_settings.service_documentation_url,
client_registration_options=mcp_auth_settings.client_registration_options,
revocation_options=mcp_auth_settings.revocation_options,
)
# Add login page route (GET)
async def login_page_handler(request: Request) -> Response:
"""Show login form."""
state = request.query_params.get("state")
if not state:
raise HTTPException(400, "Missing state parameter")
return await oauth_provider.get_login_page(state)
routes.append(Route("/login", endpoint=login_page_handler, methods=["GET"]))
# Add login callback route (POST)
async def login_callback_handler(request: Request) -> Response:
"""Handle simple authentication callback."""
return await oauth_provider.handle_login_callback(request)
routes.append(Route("/login/callback", endpoint=login_callback_handler, methods=["POST"]))
# Add token introspection endpoint (RFC 7662) for Resource Servers
async def introspect_handler(request: Request) -> Response:
"""Token introspection endpoint for Resource Servers.
Resource Servers call this endpoint to validate tokens without
needing direct access to token storage.
"""
form = await request.form()
token = form.get("token")
if not token or not isinstance(token, str):
return JSONResponse({"active": False}, status_code=400)
# Look up token in provider
access_token = await oauth_provider.load_access_token(token)
if not access_token:
return JSONResponse({"active": False})
return JSONResponse(
{
"active": True,
"client_id": access_token.client_id,
"scope": " ".join(access_token.scopes),
"exp": access_token.expires_at,
"iat": int(time.time()),
"token_type": "Bearer",
"aud": access_token.resource, # RFC 8707 audience claim
"sub": access_token.subject, # RFC 7662 subject
"iss": str(server_settings.server_url),
}
)
routes.append(
Route(
"/introspect",
endpoint=cors_middleware(introspect_handler, ["POST", "OPTIONS"]),
methods=["POST", "OPTIONS"],
)
)
return Starlette(routes=routes)
async def run_server(server_settings: AuthServerSettings, auth_settings: SimpleAuthSettings):
"""Run the Authorization Server."""
auth_server = create_authorization_server(server_settings, auth_settings)
config = Config(
auth_server,
host=server_settings.host,
port=server_settings.port,
log_level="info",
)
server = Server(config)
logger.info(f"🚀 MCP Authorization Server running on {server_settings.server_url}")
await server.serve()
@click.command()
@click.option("--port", default=9000, help="Port to listen on")
def main(port: int) -> int:
"""Run the MCP Authorization Server.
This server handles OAuth flows and can be used by multiple Resource Servers.
Uses simple hardcoded credentials for demo purposes.
"""
logging.basicConfig(level=logging.INFO)
# Load simple auth settings
auth_settings = SimpleAuthSettings()
# Create server settings
host = "localhost"
server_url = f"http://{host}:{port}"
server_settings = AuthServerSettings(
host=host,
port=port,
server_url=AnyHttpUrl(server_url),
auth_callback_path=f"{server_url}/login",
)
asyncio.run(run_server(server_settings, auth_settings))
return 0
if __name__ == "__main__":
main() # type: ignore[call-arg]
@@ -0,0 +1,137 @@
"""Legacy Combined Authorization Server + Resource Server for MCP.
This server implements the old spec where MCP servers could act as both AS and RS.
Used for backwards compatibility testing with the new split AS/RS architecture.
NOTE: this is a simplified example for demonstration purposes.
This is not a production-ready implementation.
"""
import datetime
import logging
from typing import Any, Literal
import click
from pydantic import AnyHttpUrl, BaseModel
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import Response
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
from mcp.server.mcpserver.server import MCPServer
from .simple_auth_provider import SimpleAuthSettings, SimpleOAuthProvider
logger = logging.getLogger(__name__)
class ServerSettings(BaseModel):
"""Settings for the simple auth MCP server."""
# Server settings
host: str = "localhost"
port: int = 8000
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8000")
auth_callback_path: str = "http://localhost:8000/login/callback"
class LegacySimpleOAuthProvider(SimpleOAuthProvider):
"""Simple OAuth provider for legacy MCP server."""
def __init__(self, auth_settings: SimpleAuthSettings, auth_callback_path: str, server_url: str):
super().__init__(auth_settings, auth_callback_path, server_url)
def create_simple_mcp_server(server_settings: ServerSettings, auth_settings: SimpleAuthSettings) -> MCPServer:
"""Create a simple MCPServer server with simple authentication."""
oauth_provider = LegacySimpleOAuthProvider(
auth_settings, server_settings.auth_callback_path, str(server_settings.server_url)
)
mcp_auth_settings = AuthSettings(
issuer_url=server_settings.server_url,
client_registration_options=ClientRegistrationOptions(
enabled=True,
valid_scopes=[auth_settings.mcp_scope],
default_scopes=[auth_settings.mcp_scope],
),
required_scopes=[auth_settings.mcp_scope],
# No resource_server_url parameter in legacy mode
resource_server_url=None,
)
app = MCPServer(
name="Simple Auth MCP Server",
instructions="A simple MCP server with simple credential authentication",
auth_server_provider=oauth_provider,
debug=True,
auth=mcp_auth_settings,
)
# Store server settings for later use in run()
app._server_settings = server_settings # type: ignore[attr-defined]
@app.custom_route("/login", methods=["GET"])
async def login_page_handler(request: Request) -> Response:
"""Show login form."""
state = request.query_params.get("state")
if not state:
raise HTTPException(400, "Missing state parameter")
return await oauth_provider.get_login_page(state)
@app.custom_route("/login/callback", methods=["POST"])
async def login_callback_handler(request: Request) -> Response:
"""Handle simple authentication callback."""
return await oauth_provider.handle_login_callback(request)
@app.tool()
async def get_time() -> dict[str, Any]:
"""Get the current server time.
This tool demonstrates that system information can be protected
by OAuth authentication. User must be authenticated to access it.
"""
now = datetime.datetime.now()
return {
"current_time": now.isoformat(),
"timezone": "UTC", # Simplified for demo
"timestamp": now.timestamp(),
"formatted": now.strftime("%Y-%m-%d %H:%M:%S"),
}
return app
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
@click.option(
"--transport",
default="streamable-http",
type=click.Choice(["sse", "streamable-http"]),
help="Transport protocol to use ('sse' or 'streamable-http')",
)
def main(port: int, transport: Literal["sse", "streamable-http"]) -> int:
"""Run the simple auth MCP server."""
logging.basicConfig(level=logging.INFO)
auth_settings = SimpleAuthSettings()
# Create server settings
host = "localhost"
server_url = f"http://{host}:{port}"
server_settings = ServerSettings(
host=host,
port=port,
server_url=AnyHttpUrl(server_url),
auth_callback_path=f"{server_url}/login",
)
mcp_server = create_simple_mcp_server(server_settings, auth_settings)
logger.info(f"🚀 MCP Legacy Server running on {server_url}")
mcp_server.run(transport=transport, host=host, port=port)
return 0
if __name__ == "__main__":
main() # type: ignore[call-arg]
@@ -0,0 +1,161 @@
"""MCP Resource Server with Token Introspection.
This server validates tokens via Authorization Server introspection and serves MCP resources.
Demonstrates RFC 9728 Protected Resource Metadata for AS/RS separation.
NOTE: this is a simplified example for demonstration purposes.
This is not a production-ready implementation.
"""
import datetime
import logging
from typing import Any, Literal
import click
from pydantic import AnyHttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver.server import MCPServer
from .token_verifier import IntrospectionTokenVerifier
logger = logging.getLogger(__name__)
class ResourceServerSettings(BaseSettings):
"""Settings for the MCP Resource Server."""
model_config = SettingsConfigDict(env_prefix="MCP_RESOURCE_")
# Server settings
host: str = "localhost"
port: int = 8001
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8001/mcp")
# Authorization Server settings
auth_server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:9000")
auth_server_introspection_endpoint: str = "http://localhost:9000/introspect"
# No user endpoint needed - we get user data from token introspection
# MCP settings
mcp_scope: str = "user"
# RFC 8707 resource validation
oauth_strict: bool = False
def create_resource_server(settings: ResourceServerSettings) -> MCPServer:
"""Create MCP Resource Server with token introspection.
This server:
1. Provides protected resource metadata (RFC 9728)
2. Validates tokens via Authorization Server introspection
3. Serves MCP tools and resources
"""
# Create token verifier for introspection with RFC 8707 resource validation
token_verifier = IntrospectionTokenVerifier(
introspection_endpoint=settings.auth_server_introspection_endpoint,
server_url=str(settings.server_url),
validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set
)
# Create MCPServer server as a Resource Server
app = MCPServer(
name="MCP Resource Server",
instructions="Resource Server that validates tokens via Authorization Server introspection",
debug=True,
# Auth configuration for RS mode
token_verifier=token_verifier,
auth=AuthSettings(
issuer_url=settings.auth_server_url,
required_scopes=[settings.mcp_scope],
resource_server_url=settings.server_url,
),
)
# Store settings for later use in run()
app._resource_server_settings = settings # type: ignore[attr-defined]
@app.tool()
async def get_time() -> dict[str, Any]:
"""Get the current server time.
This tool demonstrates that system information can be protected
by OAuth authentication. User must be authenticated to access it.
"""
now = datetime.datetime.now()
return {
"current_time": now.isoformat(),
"timezone": "UTC", # Simplified for demo
"timestamp": now.timestamp(),
"formatted": now.strftime("%Y-%m-%d %H:%M:%S"),
}
return app
@click.command()
@click.option("--port", default=8001, help="Port to listen on")
@click.option("--auth-server", default="http://localhost:9000", help="Authorization Server URL")
@click.option(
"--transport",
default="streamable-http",
type=click.Choice(["sse", "streamable-http"]),
help="Transport protocol to use ('sse' or 'streamable-http')",
)
@click.option(
"--oauth-strict",
is_flag=True,
help="Enable RFC 8707 resource validation",
)
def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http"], oauth_strict: bool) -> int:
"""Run the MCP Resource Server.
This server:
- Provides RFC 9728 Protected Resource Metadata
- Validates tokens via Authorization Server introspection
- Serves MCP tools requiring authentication
Must be used with a running Authorization Server.
"""
logging.basicConfig(level=logging.INFO)
try:
# Parse auth server URL
auth_server_url = AnyHttpUrl(auth_server)
# Create settings
host = "localhost"
server_url = f"http://{host}:{port}/mcp"
settings = ResourceServerSettings(
host=host,
port=port,
server_url=AnyHttpUrl(server_url),
auth_server_url=auth_server_url,
auth_server_introspection_endpoint=f"{auth_server}/introspect",
oauth_strict=oauth_strict,
)
except ValueError as e:
logger.error(f"Configuration error: {e}")
logger.error("Make sure to provide a valid Authorization Server URL")
return 1
try:
mcp_server = create_resource_server(settings)
logger.info(f"🚀 MCP Resource Server running on {settings.server_url}")
logger.info(f"🔑 Using Authorization Server: {settings.auth_server_url}")
# Run the server - this should block and keep running
mcp_server.run(transport=transport, host=host, port=port)
logger.info("Server stopped")
return 0
except Exception:
logger.exception("Server error")
return 1
if __name__ == "__main__":
main() # type: ignore[call-arg]
@@ -0,0 +1,272 @@
"""Simple OAuth provider for MCP servers.
This module contains a basic OAuth implementation using hardcoded user credentials
for demonstration purposes. No external authentication provider is required.
NOTE: this is a simplified example for demonstration purposes.
This is not a production-ready implementation.
"""
import secrets
import time
from typing import Any
from pydantic import AnyHttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse, Response
from mcp.server.auth.provider import (
AccessToken,
AuthorizationCode,
AuthorizationParams,
OAuthAuthorizationServerProvider,
RefreshToken,
construct_redirect_uri,
)
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
class SimpleAuthSettings(BaseSettings):
"""Simple OAuth settings for demo purposes."""
model_config = SettingsConfigDict(env_prefix="MCP_")
# Demo user credentials
demo_username: str = "demo_user"
demo_password: str = "demo_password"
# MCP OAuth scope
mcp_scope: str = "user"
class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
"""Simple OAuth provider for demo purposes.
This provider handles the OAuth flow by:
1. Providing a simple login form for demo credentials
2. Issuing MCP tokens after successful authentication
3. Maintaining token state for introspection
"""
def __init__(self, settings: SimpleAuthSettings, auth_callback_url: str, server_url: str):
self.settings = settings
self.auth_callback_url = auth_callback_url
self.server_url = server_url
self.clients: dict[str, OAuthClientInformationFull] = {}
self.auth_codes: dict[str, AuthorizationCode] = {}
self.tokens: dict[str, AccessToken] = {}
self.state_mapping: dict[str, dict[str, str | None]] = {}
# Store authenticated user information
self.user_data: dict[str, dict[str, Any]] = {}
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
"""Get OAuth client information."""
return self.clients.get(client_id)
async def register_client(self, client_info: OAuthClientInformationFull):
"""Register a new OAuth client."""
if not client_info.client_id:
raise ValueError("No client_id provided")
self.clients[client_info.client_id] = client_info
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
"""Generate an authorization URL for simple login flow."""
state = params.state or secrets.token_hex(16)
# Store state mapping for callback
self.state_mapping[state] = {
"redirect_uri": str(params.redirect_uri),
"code_challenge": params.code_challenge,
"redirect_uri_provided_explicitly": str(params.redirect_uri_provided_explicitly),
"client_id": client.client_id,
"resource": params.resource, # RFC 8707
}
# Build simple login URL that points to login page
auth_url = f"{self.auth_callback_url}?state={state}&client_id={client.client_id}"
return auth_url
async def get_login_page(self, state: str) -> HTMLResponse:
"""Generate login page HTML for the given state."""
if not state:
raise HTTPException(400, "Missing state parameter")
# Create simple login form HTML
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>MCP Demo Authentication</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px; }}
.form-group {{ margin-bottom: 15px; }}
input {{ width: 100%; padding: 8px; margin-top: 5px; }}
button {{ background-color: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer; }}
</style>
</head>
<body>
<h2>MCP Demo Authentication</h2>
<p>This is a simplified authentication demo. Use the demo credentials below:</p>
<p><strong>Username:</strong> demo_user<br>
<strong>Password:</strong> demo_password</p>
<form action="{self.server_url.rstrip("/")}/login/callback" method="post">
<input type="hidden" name="state" value="{state}">
<div class="form-group">
<label>Username:</label>
<input type="text" name="username" value="demo_user" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" name="password" value="demo_password" required>
</div>
<button type="submit">Sign In</button>
</form>
</body>
</html>
"""
return HTMLResponse(content=html_content)
async def handle_login_callback(self, request: Request) -> Response:
"""Handle login form submission callback."""
form = await request.form()
username = form.get("username")
password = form.get("password")
state = form.get("state")
if not username or not password or not state:
raise HTTPException(400, "Missing username, password, or state parameter")
# Ensure we have strings, not UploadFile objects
if not isinstance(username, str) or not isinstance(password, str) or not isinstance(state, str):
raise HTTPException(400, "Invalid parameter types")
redirect_uri = await self.handle_simple_callback(username, password, state)
return RedirectResponse(url=redirect_uri, status_code=302)
async def handle_simple_callback(self, username: str, password: str, state: str) -> str:
"""Handle simple authentication callback and return redirect URI."""
state_data = self.state_mapping.get(state)
if not state_data:
raise HTTPException(400, "Invalid state parameter")
redirect_uri = state_data["redirect_uri"]
code_challenge = state_data["code_challenge"]
redirect_uri_provided_explicitly = state_data["redirect_uri_provided_explicitly"] == "True"
client_id = state_data["client_id"]
resource = state_data.get("resource") # RFC 8707
# These are required values from our own state mapping
assert redirect_uri is not None
assert code_challenge is not None
assert client_id is not None
# Validate demo credentials
if username != self.settings.demo_username or password != self.settings.demo_password:
raise HTTPException(401, "Invalid credentials")
# Create MCP authorization code
new_code = f"mcp_{secrets.token_hex(16)}"
auth_code = AuthorizationCode(
code=new_code,
client_id=client_id,
redirect_uri=AnyHttpUrl(redirect_uri),
redirect_uri_provided_explicitly=redirect_uri_provided_explicitly,
expires_at=time.time() + 300,
scopes=[self.settings.mcp_scope],
code_challenge=code_challenge,
resource=resource, # RFC 8707
subject=username,
)
self.auth_codes[new_code] = auth_code
# Store user data
self.user_data[username] = {
"username": username,
"user_id": f"user_{secrets.token_hex(8)}",
"authenticated_at": time.time(),
}
del self.state_mapping[state]
return construct_redirect_uri(redirect_uri, code=new_code, state=state)
async def load_authorization_code(
self, client: OAuthClientInformationFull, authorization_code: str
) -> AuthorizationCode | None:
"""Load an authorization code."""
return self.auth_codes.get(authorization_code)
async def exchange_authorization_code(
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
) -> OAuthToken:
"""Exchange authorization code for tokens."""
if authorization_code.code not in self.auth_codes:
raise ValueError("Invalid authorization code")
if not client.client_id:
raise ValueError("No client_id provided")
# Generate MCP access token
mcp_token = f"mcp_{secrets.token_hex(32)}"
# Store MCP token
self.tokens[mcp_token] = AccessToken(
token=mcp_token,
client_id=client.client_id,
scopes=authorization_code.scopes,
expires_at=int(time.time()) + 3600,
resource=authorization_code.resource, # RFC 8707
subject=authorization_code.subject,
)
# Store user data mapping for this token
self.user_data[mcp_token] = {
"username": self.settings.demo_username,
"user_id": f"user_{secrets.token_hex(8)}",
"authenticated_at": time.time(),
}
del self.auth_codes[authorization_code.code]
return OAuthToken(
access_token=mcp_token,
token_type="Bearer",
expires_in=3600,
scope=" ".join(authorization_code.scopes),
)
async def load_access_token(self, token: str) -> AccessToken | None:
"""Load and validate an access token."""
access_token = self.tokens.get(token)
if not access_token:
return None
# Check if expired
if access_token.expires_at and access_token.expires_at < time.time():
del self.tokens[token]
return None
return access_token
async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
"""Load a refresh token - not supported in this example."""
return None
async def exchange_refresh_token(
self,
client: OAuthClientInformationFull,
refresh_token: RefreshToken,
scopes: list[str],
) -> OAuthToken:
"""Exchange refresh token - not supported in this example."""
raise NotImplementedError("Refresh tokens not supported")
# TODO(Marcelo): The type hint is wrong. We need to fix, and test to check if it works.
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None: # type: ignore
"""Revoke a token."""
if token in self.tokens:
del self.tokens[token]
@@ -0,0 +1,108 @@
"""Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
import logging
from typing import Any
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
logger = logging.getLogger(__name__)
class IntrospectionTokenVerifier(TokenVerifier):
"""Example token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
This is a simple example implementation for demonstration purposes.
Production implementations should consider:
- Connection pooling and reuse
- More sophisticated error handling
- Rate limiting and retry logic
- Comprehensive configuration options
"""
def __init__(
self,
introspection_endpoint: str,
server_url: str,
validate_resource: bool = False,
):
self.introspection_endpoint = introspection_endpoint
self.server_url = server_url
self.validate_resource = validate_resource
self.resource_url = resource_url_from_server_url(server_url)
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token via introspection endpoint."""
import httpx
# Validate URL to prevent SSRF attacks
if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
logger.warning(f"Rejecting introspection endpoint with unsafe scheme: {self.introspection_endpoint}")
return None
# Configure secure HTTP client
timeout = httpx.Timeout(10.0, connect=5.0)
limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
async with httpx.AsyncClient(
timeout=timeout,
limits=limits,
verify=True, # Enforce SSL verification
) as client:
try:
response = await client.post(
self.introspection_endpoint,
data={"token": token},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if response.status_code != 200:
logger.debug(f"Token introspection returned status {response.status_code}")
return None
data = response.json()
if not data.get("active", False):
return None
# RFC 8707 resource validation (only when --oauth-strict is set)
if self.validate_resource and not self._validate_resource(data):
logger.warning(f"Token resource validation failed. Expected: {self.resource_url}")
return None
return AccessToken(
token=token,
client_id=data.get("client_id", "unknown"),
scopes=data.get("scope", "").split() if data.get("scope") else [],
expires_at=data.get("exp"),
resource=data.get("aud"), # Include resource in token
subject=data.get("sub"), # RFC 7662 subject (resource owner)
claims=data,
)
except Exception as e:
logger.warning(f"Token introspection failed: {e}")
return None
def _validate_resource(self, token_data: dict[str, Any]) -> bool:
"""Validate token was issued for this resource server."""
if not self.server_url or not self.resource_url:
return False # Fail if strict validation requested but URLs missing
# Check 'aud' claim first (standard JWT audience)
aud: list[str] | str | None = token_data.get("aud")
if isinstance(aud, list):
for audience in aud:
if self._is_valid_resource(audience):
return True
return False
elif aud:
return self._is_valid_resource(aud)
# No resource binding - invalid per RFC 8707
return False
def _is_valid_resource(self, resource: str) -> bool:
"""Check if resource matches this server using hierarchical matching."""
if not self.resource_url:
return False
return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
@@ -0,0 +1,33 @@
[project]
name = "mcp-simple-auth"
version = "0.1.0"
description = "A simple MCP server demonstrating OAuth authentication"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
license = { text = "MIT" }
dependencies = [
"anyio>=4.5",
"click>=8.2.0",
"httpx>=0.27",
"mcp",
"pydantic>=2.0",
"pydantic-settings>=2.5.2",
"sse-starlette>=1.6.1",
"uvicorn>=0.23.1; sys_platform != 'emscripten'",
]
[project.scripts]
mcp-simple-auth-rs = "mcp_simple_auth.server:main"
mcp-simple-auth-as = "mcp_simple_auth.auth_server:main"
mcp-simple-auth-legacy = "mcp_simple_auth.legacy_as_server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_auth"]
[dependency-groups]
dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
@@ -0,0 +1,77 @@
# MCP Simple Pagination
A simple MCP server demonstrating pagination for tools, resources, and prompts using cursor-based pagination.
## Usage
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-pagination
# Using Streamable HTTP transport on custom port
uv run mcp-simple-pagination --transport streamable-http --port 8000
```
The server exposes:
- 25 tools (paginated, 5 per page)
- 30 resources (paginated, 10 per page)
- 20 prompts (paginated, 7 per page)
Each paginated list returns a `nextCursor` when more pages are available. Use this cursor in subsequent requests to retrieve the next page.
## Example
Using the MCP client, you can retrieve paginated items like this using the STDIO transport:
```python
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Get first page of tools
tools_page1 = await session.list_tools()
print(f"First page: {len(tools_page1.tools)} tools")
print(f"Next cursor: {tools_page1.nextCursor}")
# Get second page using cursor
if tools_page1.nextCursor:
tools_page2 = await session.list_tools(cursor=tools_page1.nextCursor)
print(f"Second page: {len(tools_page2.tools)} tools")
# Similarly for resources
resources_page1 = await session.list_resources()
print(f"First page: {len(resources_page1.resources)} resources")
# And for prompts
prompts_page1 = await session.list_prompts()
print(f"First page: {len(prompts_page1.prompts)} prompts")
asyncio.run(main())
```
## Pagination Details
The server uses simple numeric indices as cursors for demonstration purposes. In production scenarios, you might use:
- Database offsets or row IDs
- Timestamps for time-based pagination
- Opaque tokens encoding pagination state
The pagination implementation demonstrates:
- Handling `None` cursor for the first page
- Returning `nextCursor` when more data exists
- Gracefully handling invalid cursors
- Different page sizes for different resource types
@@ -0,0 +1,5 @@
import sys
from .server import main
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,176 @@
"""Simple MCP server demonstrating pagination for tools, resources, and prompts.
This example shows how to implement pagination with the low-level server API
to handle large lists of items that need to be split across multiple pages.
"""
from typing import TypeVar
import anyio
import click
import mcp_types as types
from mcp.server import Server, ServerRequestContext
T = TypeVar("T")
# Sample data - in real scenarios, this might come from a database
SAMPLE_TOOLS = [
types.Tool(
name=f"tool_{i}",
title=f"Tool {i}",
description=f"This is sample tool number {i}",
input_schema={"type": "object", "properties": {"input": {"type": "string"}}},
)
for i in range(1, 26) # 25 tools total
]
SAMPLE_RESOURCES = [
types.Resource(
uri=f"file:///path/to/resource_{i}.txt",
name=f"resource_{i}",
description=f"This is sample resource number {i}",
)
for i in range(1, 31) # 30 resources total
]
SAMPLE_PROMPTS = [
types.Prompt(
name=f"prompt_{i}",
description=f"This is sample prompt number {i}",
arguments=[
types.PromptArgument(name="arg1", description="First argument", required=True),
],
)
for i in range(1, 21) # 20 prompts total
]
def _paginate(cursor: str | None, items: list[T], page_size: int) -> tuple[list[T], str | None]:
"""Helper to paginate a list of items given a cursor."""
if cursor is not None:
try:
start_idx = int(cursor)
except (ValueError, TypeError):
return [], None
else:
start_idx = 0
page = items[start_idx : start_idx + page_size]
next_cursor = str(start_idx + page_size) if start_idx + page_size < len(items) else None
return page, next_cursor
# Paginated list_tools - returns 5 tools per page
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_TOOLS, page_size=5)
return types.ListToolsResult(tools=page, next_cursor=next_cursor)
# Paginated list_resources - returns 10 resources per page
async def handle_list_resources(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_RESOURCES, page_size=10)
return types.ListResourcesResult(resources=page, next_cursor=next_cursor)
# Paginated list_prompts - returns 7 prompts per page
async def handle_list_prompts(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_PROMPTS, page_size=7)
return types.ListPromptsResult(prompts=page, next_cursor=next_cursor)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
# Find the tool in our sample data
tool = next((t for t in SAMPLE_TOOLS if t.name == params.name), None)
if not tool:
raise ValueError(f"Unknown tool: {params.name}")
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"Called tool '{params.name}' with arguments: {params.arguments}",
)
]
)
async def handle_read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
resource = next((r for r in SAMPLE_RESOURCES if r.uri == str(params.uri)), None)
if not resource:
raise ValueError(f"Unknown resource: {params.uri}")
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=str(params.uri),
text=f"Content of {resource.name}: This is sample content for the resource.",
mime_type="text/plain",
)
]
)
async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
prompt = next((p for p in SAMPLE_PROMPTS if p.name == params.name), None)
if not prompt:
raise ValueError(f"Unknown prompt: {params.name}")
message_text = f"This is the prompt '{params.name}'"
if params.arguments:
message_text += f" with arguments: {params.arguments}"
return types.GetPromptResult(
description=prompt.description,
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(type="text", text=message_text),
)
],
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-simple-pagination",
on_list_tools=handle_list_tools,
on_list_resources=handle_list_resources,
on_list_prompts=handle_list_prompts,
on_call_tool=handle_call_tool,
on_read_resource=handle_read_resource,
on_get_prompt=handle_get_prompt,
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
anyio.run(arun)
return 0
@@ -0,0 +1,43 @@
[project]
name = "mcp-simple-pagination"
version = "0.1.0"
description = "A simple MCP server demonstrating pagination for tools, resources, and prompts"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "pagination", "cursor"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
[project.scripts]
mcp-simple-pagination = "mcp_simple_pagination.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_pagination"]
[tool.pyright]
include = ["mcp_simple_pagination"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
+55
View File
@@ -0,0 +1,55 @@
# MCP Simple Prompt
A simple MCP server that exposes a customizable prompt template with optional context and topic parameters.
## Usage
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-prompt
# Using Streamable HTTP transport on custom port
uv run mcp-simple-prompt --transport streamable-http --port 8000
```
The server exposes a prompt named "simple" that accepts two optional arguments:
- `context`: Additional context to consider
- `topic`: Specific topic to focus on
## Example
Using the MCP client, you can retrieve the prompt like this using the STDIO transport:
```python
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-prompt"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available prompts
prompts = await session.list_prompts()
print(prompts)
# Get the prompt with arguments
prompt = await session.get_prompt(
"simple",
{
"context": "User is a software developer",
"topic": "Python async programming",
},
)
print(prompt)
asyncio.run(main())
```
@@ -0,0 +1,5 @@
import sys
from .server import main
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,98 @@
import anyio
import click
import mcp_types as types
from mcp.server import Server, ServerRequestContext
def create_messages(context: str | None = None, topic: str | None = None) -> list[types.PromptMessage]:
"""Create the messages for the prompt."""
messages: list[types.PromptMessage] = []
# Add context if provided
if context:
messages.append(
types.PromptMessage(
role="user",
content=types.TextContent(type="text", text=f"Here is some relevant context: {context}"),
)
)
# Add the main prompt
prompt = "Please help me with "
if topic:
prompt += f"the following topic: {topic}"
else:
prompt += "whatever questions I may have."
messages.append(types.PromptMessage(role="user", content=types.TextContent(type="text", text=prompt)))
return messages
async def handle_list_prompts(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
return types.ListPromptsResult(
prompts=[
types.Prompt(
name="simple",
title="Simple Assistant Prompt",
description="A simple prompt that can take optional context and topic arguments",
arguments=[
types.PromptArgument(
name="context",
description="Additional context to consider",
required=False,
),
types.PromptArgument(
name="topic",
description="Specific topic to focus on",
required=False,
),
],
)
]
)
async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
if params.name != "simple":
raise ValueError(f"Unknown prompt: {params.name}")
arguments = params.arguments or {}
return types.GetPromptResult(
messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")),
description="A simple prompt with optional context and topic arguments",
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-simple-prompt",
on_list_prompts=handle_list_prompts,
on_get_prompt=handle_get_prompt,
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
anyio.run(arun)
return 0
@@ -0,0 +1,43 @@
[project]
name = "mcp-simple-prompt"
version = "0.1.0"
description = "A simple MCP server exposing a customizable prompt"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "web", "fetch"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
[project.scripts]
mcp-simple-prompt = "mcp_simple_prompt.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_prompt"]
[tool.pyright]
include = ["mcp_simple_prompt"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1,48 @@
# MCP Simple Resource
A simple MCP server that exposes sample text files as resources.
## Usage
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-resource
# Using Streamable HTTP transport on custom port
uv run mcp-simple-resource --transport streamable-http --port 8000
```
The server exposes some basic text file resources that can be read by clients.
## Example
Using the MCP client, you can retrieve resources like this using the STDIO transport:
```python
import asyncio
from pydantic import AnyUrl
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-resource"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available resources
resources = await session.list_resources()
print(resources)
# Get a specific resource
resource = await session.read_resource(AnyUrl("file:///greeting.txt"))
print(resource)
asyncio.run(main())
```
@@ -0,0 +1,5 @@
import sys
from .server import main
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,91 @@
from urllib.parse import urlparse
import anyio
import click
import mcp_types as types
from mcp.server import Server, ServerRequestContext
SAMPLE_RESOURCES = {
"greeting": {
"content": "Hello! This is a sample text resource.",
"title": "Welcome Message",
},
"help": {
"content": "This server provides a few sample text resources for testing.",
"title": "Help Documentation",
},
"about": {
"content": "This is the simple-resource MCP server implementation.",
"title": "About This Server",
},
}
async def handle_list_resources(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
return types.ListResourcesResult(
resources=[
types.Resource(
uri=f"file:///{name}.txt",
name=name,
title=SAMPLE_RESOURCES[name]["title"],
description=f"A sample text resource named {name}",
mime_type="text/plain",
)
for name in SAMPLE_RESOURCES.keys()
]
)
async def handle_read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
parsed = urlparse(str(params.uri))
if not parsed.path:
raise ValueError(f"Invalid resource path: {params.uri}")
name = parsed.path.replace(".txt", "").lstrip("/")
if name not in SAMPLE_RESOURCES:
raise ValueError(f"Unknown resource: {params.uri}")
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=str(params.uri),
text=SAMPLE_RESOURCES[name]["content"],
mime_type="text/plain",
)
]
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-simple-resource",
on_list_resources=handle_list_resources,
on_read_resource=handle_read_resource,
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
anyio.run(arun)
return 0
@@ -0,0 +1,43 @@
[project]
name = "mcp-simple-resource"
version = "0.1.0"
description = "A simple MCP server exposing sample text resources"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "web", "fetch"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
[project.scripts]
mcp-simple-resource = "mcp_simple_resource.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_resource"]
[tool.pyright]
include = ["mcp_simple_resource"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1,38 @@
# MCP Simple StreamableHttp Stateless Server Example
A stateless MCP server example demonstrating the StreamableHttp transport without maintaining session state. This example is ideal for understanding how to deploy MCP servers in multi-node environments where requests can be routed to any instance.
## Features
- Uses the StreamableHTTP transport in stateless mode (mcp_session_id=None)
- Each request creates a new ephemeral connection
- No session state maintained between requests
- Suitable for deployment in multi-node environments
## Usage
Start the server:
```bash
# Using default port 3000
uv run mcp-simple-streamablehttp-stateless
# Using custom port
uv run mcp-simple-streamablehttp-stateless --port 3000
# Custom logging level
uv run mcp-simple-streamablehttp-stateless --log-level DEBUG
# Enable JSON responses instead of SSE streams
uv run mcp-simple-streamablehttp-stateless --json-response
```
The server exposes a tool named "start-notification-stream" that accepts three arguments:
- `interval`: Time between notifications in seconds (e.g., 1.0)
- `count`: Number of notifications to send (e.g., 5)
- `caller`: Identifier string for the caller
## Client
You can connect to this server using an HTTP client. For now, only the TypeScript SDK has streamable HTTP client examples, or you can use [Inspector](https://github.com/modelcontextprotocol/inspector) for testing.
@@ -0,0 +1,7 @@
from .server import main
if __name__ == "__main__":
# Click will handle CLI arguments
import sys
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,116 @@
import logging
import anyio
import click
import mcp_types as types
import uvicorn
from mcp.server import Server, ServerRequestContext
from starlette.middleware.cors import CORSMiddleware
logger = logging.getLogger(__name__)
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="start-notification-stream",
description=("Sends a stream of notifications with configurable count and interval"),
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": ("Identifier of the caller to include in notifications"),
},
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
arguments = params.arguments or {}
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
await ctx.session.send_log_message( # pyright: ignore[reportDeprecated]
level="info",
data=f"Notification {i + 1}/{count} from caller: {caller}",
logger="notification_stream",
related_request_id=ctx.request_id,
)
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
)
@click.command()
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
@click.option(
"--json-response",
is_flag=True,
default=False,
help="Enable JSON responses instead of SSE streams",
)
def main(
port: int,
log_level: str,
json_response: bool,
) -> None:
# Configure logging
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server(
"mcp-streamable-http-stateless-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
starlette_app = app.streamable_http_app(
stateless_http=True,
json_response=json_response,
debug=True,
)
# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header
# for browser-based clients (ensures 500 errors get proper CORS headers)
starlette_app = CORSMiddleware(
starlette_app,
allow_origins=["*"], # Note: streamable_http_app() enforces localhost-only Origin by default
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
@@ -0,0 +1,36 @@
[project]
name = "mcp-simple-streamablehttp-stateless"
version = "0.1.0"
description = "A simple MCP server exposing a StreamableHttp transport in stateless mode"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "web", "fetch", "http", "streamable", "stateless"]
license = { text = "MIT" }
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-simple-streamablehttp-stateless = "mcp_simple_streamablehttp_stateless.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_streamablehttp_stateless"]
[tool.pyright]
include = ["mcp_simple_streamablehttp_stateless"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1,51 @@
# MCP Simple StreamableHttp Server Example
A simple MCP server example demonstrating the StreamableHttp transport, which enables HTTP-based communication with MCP servers using streaming.
## Features
- Uses the StreamableHTTP transport for server-client communication
- Supports REST API operations (POST, GET, DELETE) for `/mcp` endpoint
- Ability to send multiple notifications over time to the client
- Resumability support via InMemoryEventStore
## Usage
Start the server on the default or custom port:
```bash
# Using custom port
uv run mcp-simple-streamablehttp --port 3000
# Custom logging level
uv run mcp-simple-streamablehttp --log-level DEBUG
# Enable JSON responses instead of SSE streams
uv run mcp-simple-streamablehttp --json-response
```
The server exposes a tool named "start-notification-stream" that accepts three arguments:
- `interval`: Time between notifications in seconds (e.g., 1.0)
- `count`: Number of notifications to send (e.g., 5)
- `caller`: Identifier string for the caller
## Resumability Support
This server includes resumability support through the InMemoryEventStore. This enables clients to:
- Reconnect to the server after a disconnection
- Resume event streaming from where they left off using the Last-Event-ID header
The server will:
- Generate unique event IDs for each SSE message
- Store events in memory for later replay
- Replay missed events when a client reconnects with a Last-Event-ID header
Note: The InMemoryEventStore is designed for demonstration purposes only. For production use, consider implementing a persistent storage solution.
## Client
You can connect to this server using an HTTP client, for now only Typescript SDK has streamable HTTP client examples or you can use [Inspector](https://github.com/modelcontextprotocol/inspector)
@@ -0,0 +1,4 @@
from .server import main
if __name__ == "__main__":
main() # type: ignore[call-arg]
@@ -0,0 +1,93 @@
"""In-memory event store for demonstrating resumability functionality.
This is a simple implementation intended for examples and testing,
not for production use where a persistent storage solution would be more appropriate.
"""
import logging
from collections import deque
from dataclasses import dataclass
from uuid import uuid4
from mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId
from mcp_types import JSONRPCMessage
logger = logging.getLogger(__name__)
@dataclass
class EventEntry:
"""Represents an event entry in the event store."""
event_id: EventId
stream_id: StreamId
message: JSONRPCMessage | None
class InMemoryEventStore(EventStore):
"""Simple in-memory implementation of the EventStore interface for resumability.
This is primarily intended for examples and testing, not for production use
where a persistent storage solution would be more appropriate.
This implementation keeps only the last N events per stream for memory efficiency.
"""
def __init__(self, max_events_per_stream: int = 100):
"""Initialize the event store.
Args:
max_events_per_stream: Maximum number of events to keep per stream
"""
self.max_events_per_stream = max_events_per_stream
# for maintaining last N events per stream
self.streams: dict[StreamId, deque[EventEntry]] = {}
# event_id -> EventEntry for quick lookup
self.event_index: dict[EventId, EventEntry] = {}
async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:
"""Stores an event with a generated event ID."""
event_id = str(uuid4())
event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)
# Get or create deque for this stream
if stream_id not in self.streams:
self.streams[stream_id] = deque(maxlen=self.max_events_per_stream)
# If deque is full, the oldest event will be automatically removed
# We need to remove it from the event_index as well
if len(self.streams[stream_id]) == self.max_events_per_stream:
oldest_event = self.streams[stream_id][0]
self.event_index.pop(oldest_event.event_id, None)
# Add new event
self.streams[stream_id].append(event_entry)
self.event_index[event_id] = event_entry
return event_id
async def replay_events_after(
self,
last_event_id: EventId,
send_callback: EventCallback,
) -> StreamId | None:
"""Replays events that occurred after the specified event ID."""
if last_event_id not in self.event_index:
logger.warning(f"Event ID {last_event_id} not found in store")
return None
# Get the stream and find events after the last one
last_event = self.event_index[last_event_id]
stream_id = last_event.stream_id
stream_events = self.streams.get(last_event.stream_id, deque())
# Events in deque are already in chronological order
found_last = False
for event in stream_events:
if found_last:
# Skip priming events (None message)
if event.message is not None:
await send_callback(EventMessage(event.message, event.event_id))
elif event.event_id == last_event_id:
found_last = True
return stream_id
@@ -0,0 +1,142 @@
import logging
import anyio
import click
import mcp_types as types
import uvicorn
from mcp.server import Server, ServerRequestContext
from starlette.middleware.cors import CORSMiddleware
from .event_store import InMemoryEventStore
# Configure logging
logger = logging.getLogger(__name__)
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="start-notification-stream",
description="Sends a stream of notifications with configurable count and interval",
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": "Identifier of the caller to include in notifications",
},
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
arguments = params.arguments or {}
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
# Include more detailed message for resumability demonstration
notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected"
await ctx.session.send_log_message( # pyright: ignore[reportDeprecated]
level="info",
data=notification_msg,
logger="notification_stream",
# Associates this notification with the original request
# Ensures notifications are sent to the correct response stream
# Without this, notifications will either go to:
# - a standalone SSE stream (if GET request is supported)
# - nowhere (if GET request isn't supported)
related_request_id=ctx.request_id,
)
logger.debug(f"Sent notification {i + 1}/{count} for caller: {caller}")
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
# This will send a resource notification through standalone SSE
# established by GET request
await ctx.session.send_resource_updated(uri="http:///test_resource")
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
)
@click.command()
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
@click.option(
"--json-response",
is_flag=True,
default=False,
help="Enable JSON responses instead of SSE streams",
)
def main(
port: int,
log_level: str,
json_response: bool,
) -> int:
# Configure logging
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server(
"mcp-streamable-http-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Create event store for resumability
# The InMemoryEventStore enables resumability support for StreamableHTTP transport.
# It stores SSE events with unique IDs, allowing clients to:
# 1. Receive event IDs for each SSE message
# 2. Resume streams by sending Last-Event-ID in GET requests
# 3. Replay missed events after reconnection
# Note: This in-memory implementation is for demonstration ONLY.
# For production, use a persistent storage solution.
event_store = InMemoryEventStore()
starlette_app = app.streamable_http_app(
event_store=event_store,
json_response=json_response,
debug=True,
)
# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header
# for browser-based clients (ensures 500 errors get proper CORS headers)
starlette_app = CORSMiddleware(
starlette_app,
allow_origins=["*"], # Note: streamable_http_app() enforces localhost-only Origin by default
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0
@@ -0,0 +1,36 @@
[project]
name = "mcp-simple-streamablehttp"
version = "0.1.0"
description = "A simple MCP server exposing a StreamableHttp transport for testing"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "web", "fetch", "http", "streamable"]
license = { text = "MIT" }
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-simple-streamablehttp = "mcp_simple_streamablehttp.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_streamablehttp"]
[tool.pyright]
include = ["mcp_simple_streamablehttp"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
+48
View File
@@ -0,0 +1,48 @@
A simple MCP server that exposes a website fetching tool.
## Usage
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-tool
# Using Streamable HTTP transport on custom port
uv run mcp-simple-tool --transport streamable-http --port 8000
```
The server exposes a tool named "fetch" that accepts one required argument:
- `url`: The URL of the website to fetch
## Example
Using the MCP client, you can use the tool like this using the STDIO transport:
```python
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-tool"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
print(tools)
# Call the fetch tool
result = await session.call_tool("fetch", {"url": "https://example.com"})
print(result)
asyncio.run(main())
```
@@ -0,0 +1,5 @@
import sys
from .server import main
sys.exit(main()) # type: ignore[call-arg]
@@ -0,0 +1,80 @@
import anyio
import click
import mcp_types as types
from mcp.server import Server, ServerRequestContext
from mcp.shared._httpx_utils import create_mcp_http_client
async def fetch_website(
url: str,
) -> list[types.ContentBlock]:
headers = {"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"}
async with create_mcp_http_client(headers=headers) as client:
response = await client.get(url)
response.raise_for_status()
return [types.TextContent(type="text", text=response.text)]
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="fetch",
title="Website Fetcher",
description="Fetches a website and returns its content",
input_schema={
"type": "object",
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "URL to fetch",
}
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
if params.name != "fetch":
raise ValueError(f"Unknown tool: {params.name}")
arguments = params.arguments or {}
if "url" not in arguments:
raise ValueError("Missing required argument 'url'")
content = await fetch_website(arguments["url"])
return types.CallToolResult(content=content)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-website-fetcher",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
anyio.run(arun)
return 0
@@ -0,0 +1,43 @@
[project]
name = "mcp-simple-tool"
version = "0.1.0"
description = "A simple MCP server exposing a website fetching tool"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "automation", "web", "fetch"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
[project.scripts]
mcp-simple-tool = "mcp_simple_tool.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_tool"]
[tool.pyright]
include = ["mcp_simple_tool"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1,36 @@
# MCP SSE Polling Demo Server
Demonstrates the SSE polling pattern with server-initiated stream close for long-running tasks (SEP-1699).
## Features
- Priming events (automatic with EventStore)
- Server-initiated stream close via `close_sse_stream()` callback
- Client auto-reconnect with Last-Event-ID
- Progress notifications during long-running tasks
- Configurable retry interval
## Usage
```bash
# Start server on default port
uv run mcp-sse-polling-demo --port 3000
# Custom retry interval (milliseconds)
uv run mcp-sse-polling-demo --port 3000 --retry-interval 100
```
## Tool: process_batch
Processes items with periodic checkpoints that trigger SSE stream closes:
- `items`: Number of items to process (1-100, default: 10)
- `checkpoint_every`: Close stream after this many items (1-20, default: 3)
## Client
Use the companion `mcp-sse-polling-client` to test:
```bash
uv run mcp-sse-polling-client --url http://localhost:3000/mcp
```
@@ -0,0 +1 @@
"""SSE Polling Demo Server - demonstrates close_sse_stream for long-running tasks."""
@@ -0,0 +1,6 @@
"""Entry point for the SSE Polling Demo server."""
from .server import main
if __name__ == "__main__":
main()
@@ -0,0 +1,98 @@
"""In-memory event store for demonstrating resumability functionality.
This is a simple implementation intended for examples and testing,
not for production use where a persistent storage solution would be more appropriate.
"""
import logging
from collections import deque
from dataclasses import dataclass
from uuid import uuid4
from mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId
from mcp_types import JSONRPCMessage
logger = logging.getLogger(__name__)
@dataclass
class EventEntry:
"""Represents an event entry in the event store."""
event_id: EventId
stream_id: StreamId
message: JSONRPCMessage | None # None for priming events
class InMemoryEventStore(EventStore):
"""Simple in-memory implementation of the EventStore interface for resumability.
This is primarily intended for examples and testing, not for production use
where a persistent storage solution would be more appropriate.
This implementation keeps only the last N events per stream for memory efficiency.
"""
def __init__(self, max_events_per_stream: int = 100):
"""Initialize the event store.
Args:
max_events_per_stream: Maximum number of events to keep per stream
"""
self.max_events_per_stream = max_events_per_stream
# for maintaining last N events per stream
self.streams: dict[StreamId, deque[EventEntry]] = {}
# event_id -> EventEntry for quick lookup
self.event_index: dict[EventId, EventEntry] = {}
async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:
"""Stores an event with a generated event ID.
Args:
stream_id: ID of the stream the event belongs to
message: The message to store, or None for priming events
"""
event_id = str(uuid4())
event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)
# Get or create deque for this stream
if stream_id not in self.streams:
self.streams[stream_id] = deque(maxlen=self.max_events_per_stream)
# If deque is full, the oldest event will be automatically removed
# We need to remove it from the event_index as well
if len(self.streams[stream_id]) == self.max_events_per_stream:
oldest_event = self.streams[stream_id][0]
self.event_index.pop(oldest_event.event_id, None)
# Add new event
self.streams[stream_id].append(event_entry)
self.event_index[event_id] = event_entry
return event_id
async def replay_events_after(
self,
last_event_id: EventId,
send_callback: EventCallback,
) -> StreamId | None:
"""Replays events that occurred after the specified event ID."""
if last_event_id not in self.event_index:
logger.warning(f"Event ID {last_event_id} not found in store")
return None
# Get the stream and find events after the last one
last_event = self.event_index[last_event_id]
stream_id = last_event.stream_id
stream_events = self.streams.get(last_event.stream_id, deque())
# Events in deque are already in chronological order
found_last = False
for event in stream_events:
if found_last:
# Skip priming events (None messages) during replay
if event.message is not None:
await send_callback(EventMessage(event.message, event.event_id))
elif event.event_id == last_event_id:
found_last = True
return stream_id
@@ -0,0 +1,160 @@
"""SSE Polling Demo Server
Demonstrates the SSE polling pattern with close_sse_stream() for long-running tasks.
Features demonstrated:
- Priming events (automatic with EventStore)
- Server-initiated stream close via close_sse_stream callback
- Client auto-reconnect with Last-Event-ID
- Progress notifications during long-running tasks
Run with:
uv run mcp-sse-polling-demo --port 3000
"""
import logging
import anyio
import click
import mcp_types as types
import uvicorn
from mcp.server import Server, ServerRequestContext
from .event_store import InMemoryEventStore
logger = logging.getLogger(__name__)
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
return types.ListToolsResult(
tools=[
types.Tool(
name="process_batch",
description=(
"Process a batch of items with periodic checkpoints. "
"Demonstrates SSE polling where server closes stream periodically."
),
input_schema={
"type": "object",
"properties": {
"items": {
"type": "integer",
"description": "Number of items to process (1-100)",
"default": 10,
},
"checkpoint_every": {
"type": "integer",
"description": "Close stream after this many items (1-20)",
"default": 3,
},
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls."""
arguments = params.arguments or {}
if params.name == "process_batch":
items = arguments.get("items", 10)
checkpoint_every = arguments.get("checkpoint_every", 3)
if items < 1 or items > 100:
return types.CallToolResult(
content=[types.TextContent(type="text", text="Error: items must be between 1 and 100")]
)
if checkpoint_every < 1 or checkpoint_every > 20:
return types.CallToolResult(
content=[types.TextContent(type="text", text="Error: checkpoint_every must be between 1 and 20")]
)
await ctx.session.send_log_message( # pyright: ignore[reportDeprecated]
level="info",
data=f"Starting batch processing of {items} items...",
logger="process_batch",
related_request_id=ctx.request_id,
)
for i in range(1, items + 1):
# Simulate work
await anyio.sleep(0.5)
# Report progress
await ctx.session.send_log_message( # pyright: ignore[reportDeprecated]
level="info",
data=f"[{i}/{items}] Processing item {i}",
logger="process_batch",
related_request_id=ctx.request_id,
)
# Checkpoint: close stream to trigger client reconnect
if i % checkpoint_every == 0 and i < items:
await ctx.session.send_log_message( # pyright: ignore[reportDeprecated]
level="info",
data=f"Checkpoint at item {i} - closing SSE stream for polling",
logger="process_batch",
related_request_id=ctx.request_id,
)
if ctx.close_sse_stream:
logger.info(f"Closing SSE stream at checkpoint {i}")
await ctx.close_sse_stream()
# Wait for client to reconnect (must be > retry_interval of 100ms)
await anyio.sleep(0.2)
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"Successfully processed {items} items with checkpoints every {checkpoint_every} items",
)
]
)
return types.CallToolResult(content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")])
@click.command()
@click.option("--port", default=3000, help="Port to listen on")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR)",
)
@click.option(
"--retry-interval",
default=100,
help="SSE retry interval in milliseconds (sent to client)",
)
def main(port: int, log_level: str, retry_interval: int) -> int:
"""Run the SSE Polling Demo server."""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server(
"sse-polling-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
starlette_app = app.streamable_http_app(
event_store=InMemoryEventStore(),
retry_interval=retry_interval,
debug=True,
)
logger.info(f"SSE Polling Demo server starting on port {port}")
logger.info("Try: POST /mcp with tools/call for 'process_batch'")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
[project]
name = "mcp-sse-polling-demo"
version = "0.1.0"
description = "Demo server showing SSE polling with close_sse_stream for long-running tasks"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "sse", "polling", "streamable", "http"]
license = { text = "MIT" }
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-sse-polling-demo = "mcp_sse_polling_demo.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_sse_polling_demo"]
[tool.pyright]
include = ["mcp_sse_polling_demo"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
@@ -0,0 +1 @@
"""Example of structured output with low-level MCP server."""
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Example low-level MCP server demonstrating structured output support.
This example shows how to use the low-level server API to return
structured data from tools.
"""
import asyncio
import json
import random
from datetime import datetime
import mcp_types as types
import mcp.server.stdio
from mcp.server import Server, ServerRequestContext
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools with their schemas."""
return types.ListToolsResult(
tools=[
types.Tool(
name="get_weather",
description="Get weather information (simulated)",
input_schema={
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
output_schema={
"type": "object",
"properties": {
"temperature": {"type": "number"},
"conditions": {"type": "string"},
"humidity": {"type": "integer", "minimum": 0, "maximum": 100},
"wind_speed": {"type": "number"},
"timestamp": {"type": "string", "format": "date-time"},
},
"required": ["temperature", "conditions", "humidity", "wind_speed", "timestamp"],
},
),
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool call with structured output."""
if params.name == "get_weather":
# Simulate weather data (in production, call a real weather API)
weather_conditions = ["sunny", "cloudy", "rainy", "partly cloudy", "foggy"]
weather_data = {
"temperature": round(random.uniform(0, 35), 1),
"conditions": random.choice(weather_conditions),
"humidity": random.randint(30, 90),
"wind_speed": round(random.uniform(0, 30), 1),
"timestamp": datetime.now().isoformat(),
}
return types.CallToolResult(
content=[types.TextContent(type="text", text=json.dumps(weather_data, indent=2))],
structured_content=weather_data,
)
raise ValueError(f"Unknown tool: {params.name}")
server = Server(
"structured-output-lowlevel-example",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
async def run():
"""Run the low-level server using stdio transport."""
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options(),
)
if __name__ == "__main__":
asyncio.run(run())
@@ -0,0 +1,6 @@
[project]
name = "mcp-structured-output-lowlevel"
version = "0.1.0"
description = "Example of structured output with low-level MCP server"
requires-python = ">=3.10"
dependencies = ["mcp"]
@@ -0,0 +1,78 @@
"""cd to the `examples/snippets` directory and run:
uv run completion-client
"""
import asyncio
import os
from mcp_types import PromptReference, ResourceTemplateReference
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="uv", # Using uv to run the server
args=["run", "server", "completion", "stdio"], # Server with completion support
env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)
async def run():
"""Run the completion client example."""
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()
# List available resource templates
templates = await session.list_resource_templates()
print("Available resource templates:")
for template in templates.resource_templates:
print(f" - {template.uri_template}")
# List available prompts
prompts = await session.list_prompts()
print("\nAvailable prompts:")
for prompt in prompts.prompts:
print(f" - {prompt.name}")
# Complete resource template arguments
if templates.resource_templates:
template = templates.resource_templates[0]
print(f"\nCompleting arguments for resource template: {template.uri_template}")
# Complete without context
result = await session.complete(
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
argument={"name": "owner", "value": "model"},
)
print(f"Completions for 'owner' starting with 'model': {result.completion.values}")
# Complete with context - repo suggestions based on owner
result = await session.complete(
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
argument={"name": "repo", "value": ""},
context_arguments={"owner": "modelcontextprotocol"},
)
print(f"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}")
# Complete prompt arguments
if prompts.prompts:
prompt_name = prompts.prompts[0].name
print(f"\nCompleting arguments for prompt: {prompt_name}")
result = await session.complete(
ref=PromptReference(type="ref/prompt", name=prompt_name),
argument={"name": "style", "value": ""},
)
print(f"Completions for 'style' argument: {result.completion.values}")
def main():
"""Entry point for the completion client."""
asyncio.run(run())
if __name__ == "__main__":
main()
@@ -0,0 +1,66 @@
"""cd to the `examples/snippets` directory and run:
uv run display-utilities-client
"""
import asyncio
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.shared.metadata_utils import get_display_name
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="uv", # Using uv to run the server
args=["run", "server", "mcpserver_quickstart", "stdio"],
env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)
async def display_tools(session: ClientSession):
"""Display available tools with human-readable names"""
tools_response = await session.list_tools()
for tool in tools_response.tools:
# get_display_name() returns the title if available, otherwise the name
display_name = get_display_name(tool)
print(f"Tool: {display_name}")
if tool.description:
print(f" {tool.description}")
async def display_resources(session: ClientSession):
"""Display available resources with human-readable names"""
resources_response = await session.list_resources()
for resource in resources_response.resources:
display_name = get_display_name(resource)
print(f"Resource: {display_name} ({resource.uri})")
templates_response = await session.list_resource_templates()
for template in templates_response.resource_templates:
display_name = get_display_name(template)
print(f"Resource Template: {display_name}")
async def run():
"""Run the display utilities example."""
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()
print("=== Available Tools ===")
await display_tools(session)
print("\n=== Available Resources ===")
await display_resources(session)
def main():
"""Entry point for the display utilities client."""
asyncio.run(run())
if __name__ == "__main__":
main()
@@ -0,0 +1,82 @@
"""Client side of SEP-990 (enterprise IdP policy controls).
`IdentityAssertionOAuthProvider` presents an Identity Assertion Authorization Grant (ID-JAG) issued
by the enterprise IdP to the MCP authorization server using the RFC 7523 jwt-bearer grant
(`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, ID-JAG as `assertion`), and receives an
MCP access token. No browser redirect or dynamic client registration is involved.
Obtaining the ID-JAG (logging into the IdP and the leg-1 exchange against it) is deployment-specific
and out of scope for the SDK; supply it through the `assertion_provider` callback. The callback
receives the authorization server's issuer (the ID-JAG `aud`) and the MCP server's resource
identifier (the ID-JAG `resource` claim). SEP-990 requires a confidential client, so a client secret
is mandatory, and `issuer` is the authorization server the credentials are provisioned for - the
provider fetches metadata from that issuer's well-known and never asks the resource server which AS
to use.
"""
import asyncio
import httpx
from mcp import ClientSession
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
class InMemoryTokenStorage:
"""Demo in-memory token storage."""
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
async def fetch_id_jag(audience: str, resource: str) -> str:
"""Return the ID-JAG to present.
`audience` is the MCP authorization server's issuer (the ID-JAG `aud` claim); `resource` is the
MCP server's RFC 9728 identifier (the ID-JAG `resource` claim, which the AS audience-restricts
the issued token against). In production this exchanges the user's IdP ID token for an ID-JAG
against the enterprise identity provider.
"""
raise NotImplementedError("Obtain the ID-JAG from your enterprise identity provider")
async def main() -> None:
oauth_auth = IdentityAssertionOAuthProvider(
server_url="http://localhost:8001/mcp",
storage=InMemoryTokenStorage(),
client_id="enterprise-mcp-client",
client_secret="enterprise-mcp-secret",
issuer="http://localhost:8001",
assertion_provider=fetch_id_jag,
scope="user",
)
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client:
async with streamable_http_client("http://localhost:8001/mcp", http_client=http_client) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
def run() -> None:
asyncio.run(main())
if __name__ == "__main__":
run()
+92
View File
@@ -0,0 +1,92 @@
"""Before running, specify running MCP RS server URL.
To spin up RS server locally, see
examples/servers/simple-auth/README.md
cd to the `examples/snippets` directory and run:
uv run oauth-client
"""
import asyncio
from urllib.parse import parse_qs, urlparse
import httpx
from pydantic import AnyUrl
from mcp import ClientSession
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
class InMemoryTokenStorage(TokenStorage):
"""Demo In-memory token storage implementation."""
def __init__(self):
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
"""Get stored tokens."""
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
"""Store tokens."""
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
"""Get stored client information."""
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
"""Store client information."""
self.client_info = client_info
async def handle_redirect(auth_url: str) -> None:
print(f"Visit: {auth_url}")
async def handle_callback() -> AuthorizationCodeResult:
callback_url = input("Paste callback URL: ")
params = parse_qs(urlparse(callback_url).query)
return AuthorizationCodeResult(
code=params["code"][0],
state=params.get("state", [None])[0],
iss=params.get("iss", [None])[0],
)
async def main():
"""Run the OAuth client example."""
oauth_auth = OAuthClientProvider(
server_url="http://localhost:8001",
client_metadata=OAuthClientMetadata(
client_name="Example MCP Client",
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
scope="user",
),
storage=InMemoryTokenStorage(),
redirect_handler=handle_redirect,
callback_handler=handle_callback,
)
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
resources = await session.list_resources()
print(f"Available resources: {[r.uri for r in resources.resources]}")
def run():
asyncio.run(main())
if __name__ == "__main__":
run()
@@ -0,0 +1,40 @@
"""Example of consuming paginated MCP endpoints from a client."""
import asyncio
from mcp_types import PaginatedRequestParams, Resource
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def list_all_resources() -> None:
"""Fetch all resources using pagination."""
async with stdio_client(StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])) as (
read,
write,
):
async with ClientSession(read, write) as session:
await session.initialize()
all_resources: list[Resource] = []
cursor = None
while True:
# Fetch a page of resources
result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))
all_resources.extend(result.resources)
print(f"Fetched {len(result.resources)} resources")
# Check if there are more pages
if result.next_cursor:
cursor = result.next_cursor
else:
break
print(f"Total resources: {len(all_resources)}")
if __name__ == "__main__":
asyncio.run(list_all_resources())
@@ -0,0 +1,62 @@
"""examples/snippets/clients/parsing_tool_results.py"""
import asyncio
import mcp_types as types
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def parse_tool_results():
"""Demonstrates how to parse different types of content in CallToolResult."""
server_params = StdioServerParameters(command="python", args=["path/to/mcp_server.py"])
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Example 1: Parsing text content
result = await session.call_tool("get_data", {"format": "text"})
for content in result.content:
if isinstance(content, types.TextContent):
print(f"Text: {content.text}")
# Example 2: Parsing structured content from JSON tools
result = await session.call_tool("get_user", {"id": "123"})
if hasattr(result, "structured_content") and result.structured_content:
# Access structured data directly
user_data = result.structured_content
print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")
# Example 3: Parsing embedded resources
result = await session.call_tool("read_config", {})
for content in result.content:
if isinstance(content, types.EmbeddedResource):
resource = content.resource
if isinstance(resource, types.TextResourceContents):
print(f"Config from {resource.uri}: {resource.text}")
else:
print(f"Binary data from {resource.uri}")
# Example 4: Parsing image content
result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
for content in result.content:
if isinstance(content, types.ImageContent):
print(f"Image ({content.mime_type}): {len(content.data)} bytes")
# Example 5: Handling errors
result = await session.call_tool("failing_tool", {})
if result.is_error:
print("Tool execution failed!")
for content in result.content:
if isinstance(content, types.TextContent):
print(f"Error: {content.text}")
async def main():
await parse_tool_results()
if __name__ == "__main__":
asyncio.run(main())
+82
View File
@@ -0,0 +1,82 @@
"""cd to the `examples/snippets/clients` directory and run:
uv run client
"""
import asyncio
import os
import mcp_types as types
from mcp import ClientSession, StdioServerParameters
from mcp.client.context import ClientRequestContext
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="uv", # Using uv to run the server
args=["run", "server", "mcpserver_quickstart", "stdio"], # We're already in snippets dir
env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)
# Optional: create a sampling callback
async def handle_sampling_message(
context: ClientRequestContext, params: types.CreateMessageRequestParams
) -> types.CreateMessageResult:
print(f"Sampling request: {params.messages}")
return types.CreateMessageResult(
role="assistant",
content=types.TextContent(
type="text",
text="Hello, world! from model",
),
model="gpt-3.5-turbo",
stop_reason="endTurn",
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:
# Initialize the connection
await session.initialize()
# List available prompts
prompts = await session.list_prompts()
print(f"Available prompts: {[p.name for p in prompts.prompts]}")
# Get a prompt (greet_user prompt from mcpserver_quickstart)
if prompts.prompts:
prompt = await session.get_prompt("greet_user", arguments={"name": "Alice", "style": "friendly"})
print(f"Prompt result: {prompt.messages[0].content}")
# List available resources
resources = await session.list_resources()
print(f"Available resources: {[r.uri for r in resources.resources]}")
# List available tools
tools = await session.list_tools()
print(f"Available tools: {[t.name for t in tools.tools]}")
# Read a resource (greeting resource from mcpserver_quickstart)
resource_content = await session.read_resource("greeting://World")
content_block = resource_content.contents[0]
if isinstance(content_block, types.TextResourceContents):
print(f"Resource content: {content_block.text}")
# Call a tool (add tool from mcpserver_quickstart)
result = await session.call_tool("add", arguments={"a": 5, "b": 3})
result_unstructured = result.content[0]
if isinstance(result_unstructured, types.TextContent):
print(f"Tool result: {result_unstructured.text}")
result_structured = result.structured_content
print(f"Structured tool result: {result_structured}")
def main():
"""Entry point for the client script."""
asyncio.run(run())
if __name__ == "__main__":
main()
@@ -0,0 +1,24 @@
"""Run from the repository root:
uv run examples/snippets/clients/streamable_basic.py
"""
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
async def main():
# Connect to a streamable HTTP server
async with streamable_http_client("http://localhost:8000/mcp") as (read_stream, write_stream):
# Create a session using the client streams
async with ClientSession(read_stream, write_stream) as session:
# Initialize the connection
await session.initialize()
# List available tools
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,318 @@
"""URL Elicitation Client Example.
Demonstrates how clients handle URL elicitation requests from servers.
This is the Python equivalent of TypeScript SDK's elicitationUrlExample.ts,
focused on URL elicitation patterns without OAuth complexity.
Features demonstrated:
1. Client elicitation capability declaration
2. Handling elicitation requests from servers via callback
3. Catching UrlElicitationRequiredError from tool calls
4. Browser interaction with security warnings
5. Interactive CLI for testing
Run with:
cd examples/snippets
uv run elicitation-client
Requires a server with URL elicitation tools running. Start the elicitation
server first:
uv run server elicitation sse
"""
from __future__ import annotations
import asyncio
import json
import webbrowser
from typing import Any
from urllib.parse import urlparse
import mcp_types as types
from mcp_types import URL_ELICITATION_REQUIRED
from mcp import ClientSession
from mcp.client.context import ClientRequestContext
from mcp.client.sse import sse_client
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
async def handle_elicitation(
context: ClientRequestContext,
params: types.ElicitRequestParams,
) -> types.ElicitResult | types.ErrorData:
"""Handle elicitation requests from the server.
This callback is invoked when the server sends an elicitation/request.
For URL mode, we prompt the user and optionally open their browser.
"""
if params.mode == "url":
return await handle_url_elicitation(params)
else:
# We only support URL mode in this example
return types.ErrorData(
code=types.INVALID_REQUEST,
message=f"Unsupported elicitation mode: {params.mode}",
)
ALLOWED_SCHEMES = {"http", "https"}
async def handle_url_elicitation(
params: types.ElicitRequestParams,
) -> types.ElicitResult:
"""Handle URL mode elicitation - show security warning and optionally open browser.
This function demonstrates the security-conscious approach to URL elicitation:
1. Validate the URL scheme before prompting the user
2. Display the full URL and domain for user inspection
3. Show the server's reason for requesting this interaction
4. Require explicit user consent before opening any URL
"""
# Extract URL parameters - these are available on URL mode requests
url = getattr(params, "url", None)
elicitation_id = getattr(params, "elicitationId", None)
message = params.message
if not url:
print("Error: No URL provided in elicitation request")
return types.ElicitResult(action="cancel")
# Reject dangerous URL schemes before prompting the user
parsed = urlparse(str(url))
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
print(f"\nRejecting URL with disallowed scheme '{parsed.scheme}': {url}")
return types.ElicitResult(action="decline")
# Extract domain for security display
domain = extract_domain(url)
# Security warning - always show the user what they're being asked to do
print("\n" + "=" * 60)
print("SECURITY WARNING: External URL Request")
print("=" * 60)
print("\nThe server is requesting you to open an external URL.")
print(f"\n Domain: {domain}")
print(f" Full URL: {url}")
print("\n Server's reason:")
print(f" {message}")
print(f"\n Elicitation ID: {elicitation_id}")
print("\n" + "-" * 60)
# Get explicit user consent
try:
response = input("\nOpen this URL in your browser? (y/n): ").strip().lower()
except EOFError:
return types.ElicitResult(action="cancel")
if response in ("n", "no"):
print("URL navigation declined.")
return types.ElicitResult(action="decline")
elif response not in ("y", "yes"):
print("Invalid response. Cancelling.")
return types.ElicitResult(action="cancel")
# Open the browser
print(f"\nOpening browser to: {url}")
try:
webbrowser.open(url)
except Exception as e:
print(f"Failed to open browser: {e}")
print(f"Please manually open: {url}")
print("Waiting for you to complete the interaction in your browser...")
print("(The server will continue once you've finished)")
return types.ElicitResult(action="accept")
def extract_domain(url: str) -> str:
"""Extract domain from URL for security display."""
try:
return urlparse(url).netloc
except Exception:
return "unknown"
async def call_tool_with_error_handling(
session: ClientSession,
tool_name: str,
arguments: dict[str, Any],
) -> types.CallToolResult | None:
"""Call a tool, handling UrlElicitationRequiredError if raised.
When a server tool needs URL elicitation before it can proceed,
it can either:
1. Send an elicitation request directly (handled by elicitation_callback)
2. Return an error with code -32042 (URL_ELICITATION_REQUIRED)
This function demonstrates handling case 2 - catching the error
and processing the required URL elicitations.
"""
try:
result = await session.call_tool(tool_name, arguments)
# Check if the tool returned an error in the result
if result.is_error:
print(f"Tool returned error: {result.content}")
return None
return result
except MCPError as e:
# Check if this is a URL elicitation required error
if e.code == URL_ELICITATION_REQUIRED:
print("\n[Tool requires URL elicitation to proceed]")
# Convert to typed error to access elicitations
url_error = UrlElicitationRequiredError.from_error(e.error)
# Process each required elicitation
for elicitation in url_error.elicitations:
await handle_url_elicitation(elicitation)
return None
else:
# Re-raise other MCP errors
print(f"MCP Error: {e.error.message} (code: {e.error.code})")
return None
def print_help() -> None:
"""Print available commands."""
print("\nAvailable commands:")
print(" list-tools - List available tools")
print(" call <name> [json-args] - Call a tool with optional JSON arguments")
print(" secure-payment - Test URL elicitation via ctx.elicit_url()")
print(" connect-service - Test URL elicitation via UrlElicitationRequiredError")
print(" help - Show this help")
print(" quit - Exit the program")
def print_tool_result(result: types.CallToolResult | None) -> None:
"""Print a tool call result."""
if not result:
return
print("\nTool result:")
for content in result.content:
if isinstance(content, types.TextContent):
print(f" {content.text}")
else:
print(f" [{content.type}]")
async def handle_list_tools(session: ClientSession) -> None:
"""Handle the list-tools command."""
tools = await session.list_tools()
if tools.tools:
print("\nAvailable tools:")
for tool in tools.tools:
print(f" - {tool.name}: {tool.description or 'No description'}")
else:
print("No tools available")
async def handle_call_command(session: ClientSession, command: str) -> None:
"""Handle the call command."""
parts = command.split(maxsplit=2)
if len(parts) < 2:
print("Usage: call <tool-name> [json-args]")
return
tool_name = parts[1]
args: dict[str, Any] = {}
if len(parts) > 2:
try:
args = json.loads(parts[2])
except json.JSONDecodeError as e:
print(f"Invalid JSON arguments: {e}")
return
print(f"\nCalling tool '{tool_name}' with args: {args}")
result = await call_tool_with_error_handling(session, tool_name, args)
print_tool_result(result)
async def process_command(session: ClientSession, command: str) -> bool:
"""Process a single command. Returns False if should exit."""
if command in {"quit", "exit"}:
print("Goodbye!")
return False
if command == "help":
print_help()
elif command == "list-tools":
await handle_list_tools(session)
elif command.startswith("call "):
await handle_call_command(session, command)
elif command == "secure-payment":
print("\nTesting secure_payment tool (uses ctx.elicit_url())...")
result = await call_tool_with_error_handling(session, "secure_payment", {"amount": 99.99})
print_tool_result(result)
elif command == "connect-service":
print("\nTesting connect_service tool (raises UrlElicitationRequiredError)...")
result = await call_tool_with_error_handling(session, "connect_service", {"service_name": "github"})
print_tool_result(result)
else:
print(f"Unknown command: {command}")
print("Type 'help' for available commands.")
return True
async def run_command_loop(session: ClientSession) -> None:
"""Run the interactive command loop."""
while True:
try:
command = input("> ").strip()
except EOFError:
break
except KeyboardInterrupt:
print("\n")
break
if not command:
continue
if not await process_command(session, command):
break
async def main() -> None:
"""Run the interactive URL elicitation client."""
server_url = "http://localhost:8000/sse"
print("=" * 60)
print("URL Elicitation Client Example")
print("=" * 60)
print(f"\nConnecting to: {server_url}")
print("(Start server with: cd examples/snippets && uv run server elicitation sse)")
try:
async with sse_client(server_url) as (read, write):
async with ClientSession(
read,
write,
elicitation_callback=handle_elicitation,
) as session:
await session.initialize()
print("\nConnected! Type 'help' for available commands.\n")
await run_command_loop(session)
except ConnectionRefusedError:
print(f"\nError: Could not connect to {server_url}")
print("Make sure the elicitation server is running:")
print(" cd examples/snippets && uv run server elicitation sse")
except Exception as e:
print(f"\nError: {e}")
raise
def run() -> None:
"""Entry point for the client script."""
asyncio.run(main())
if __name__ == "__main__":
run()
+25
View File
@@ -0,0 +1,25 @@
[project]
name = "mcp-snippets"
version = "0.1.0"
description = "MCP Example Snippets"
requires-python = ">=3.10"
dependencies = [
"mcp",
]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["servers", "clients"]
[project.scripts]
server = "servers:run_server"
client = "clients.stdio_client:main"
completion-client = "clients.completion_client:main"
direct-execution-server = "servers.direct_execution:main"
display-utilities-client = "clients.display_utilities:main"
oauth-client = "clients.oauth_client:run"
identity-assertion-client = "clients.identity_assertion_client:run"
elicitation-client = "clients.url_elicitation_client:run"
+37
View File
@@ -0,0 +1,37 @@
"""MCP Snippets.
This package contains simple examples of MCP server features.
Each server demonstrates a single feature and can be run as a standalone server.
To run a server, use the command:
uv run server basic_tool sse
"""
import importlib
import sys
from typing import Literal, cast
def run_server():
"""Run a server by name with optional transport.
Usage: server <server-name> [transport]
Example: server basic_tool sse
"""
if len(sys.argv) < 2:
print("Usage: server <server-name> [transport]")
print("Available servers: basic_tool, basic_resource, basic_prompt, tool_progress,")
print(" sampling, elicitation, completion, notifications,")
print(" mcpserver_quickstart, structured_output, images")
print("Available transports: stdio (default), sse, streamable-http")
sys.exit(1)
server_name = sys.argv[1]
transport = sys.argv[2] if len(sys.argv) > 2 else "stdio"
try:
module = importlib.import_module(f".{server_name}", package=__name__)
module.mcp.run(cast(Literal["stdio", "sse", "streamable-http"], transport))
except ImportError:
print(f"Error: Server '{server_name}' not found")
sys.exit(1)
+18
View File
@@ -0,0 +1,18 @@
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.prompts import base
mcp = MCPServer(name="Prompt Example")
@mcp.prompt(title="Code Review")
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"
@mcp.prompt(title="Debug Assistant")
def debug_error(error: str) -> list[base.Message]:
return [
base.UserMessage("I'm seeing this error:"),
base.UserMessage(error),
base.AssistantMessage("I'll help debug that. What have you tried so far?"),
]

Some files were not shown because too many files have changed in this diff Show More