diff --git a/examples/README.md b/examples/README.md index 5ed4dd55f5..7e3a2c04dc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,43 @@ # Python SDK Examples -This folders aims to provide simple examples of using the Python SDK. Please refer to the -[servers repository](https://github.com/modelcontextprotocol/servers) -for real-world servers. +Examples are now single self-contained Python scripts that declare their dependencies inline using PEP 723 metadata. You can run them directly with uv without creating a virtualenv or installing anything globally. + +Run any example like this: + +```bash +uv run examples// - - - """) - 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""" - - -

Authorization Failed

-

Error: {query_params["error"][0]}

-

You can close this window and return to the terminal.

- - - """.encode() - ) - else: - self.send_response(404) - self.end_headers() - - def log_message(self, format, *args): - """Suppress default logging.""" - pass - - -class CallbackServer: - """Simple server to handle OAuth callbacks.""" - - def __init__(self, port=3000): - self.port = port - self.server = None - self.thread = None - self.callback_data = {"authorization_code": None, "state": 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, client_address, server): - 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=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") - - def get_state(self): - """Get the received state parameter.""" - return self.callback_data["state"] - - -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() -> tuple[str, str | None]: - """Wait for OAuth callback and return auth code and state.""" - print("ā³ Waiting for authorization callback...") - try: - auth_code = callback_server.wait_for_callback(timeout=300) - return auth_code, callback_server.get_state() - 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, - 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, - ) as (read_stream, write_stream): - await self._run_session(read_stream, write_stream, None) - else: - print("šŸ“” Opening StreamableHTTP transport connection with auth...") - async with streamablehttp_client( - url=self.server_url, - auth=oauth_auth, - timeout=timedelta(seconds=60), - ) as (read_stream, write_stream, get_session_id): - await self._run_session(read_stream, write_stream, get_session_id) - - except Exception as e: - print(f"āŒ Failed to connect: {e}") - import traceback - - traceback.print_exc() - - async def _run_session(self, read_stream, write_stream, get_session_id): - """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}") - if get_session_id: - session_id = get_session_id() - if session_id: - print(f"Session ID: {session_id}") - - # 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 [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 = {} - 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 ', 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() diff --git a/examples/clients/simple-auth-client/pyproject.toml b/examples/clients/simple-auth-client/pyproject.toml deleted file mode 100644 index 46aba8dc12..0000000000 --- a/examples/clients/simple-auth-client/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[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 = "Anthropic" }] -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"] diff --git a/examples/clients/simple-chatbot/.python-version b/examples/clients/simple-chatbot/.python-version deleted file mode 100644 index c8cfe39591..0000000000 --- a/examples/clients/simple-chatbot/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/examples/clients/simple-chatbot/README.MD b/examples/clients/simple-chatbot/README.MD deleted file mode 100644 index 482109f97b..0000000000 --- a/examples/clients/simple-chatbot/README.MD +++ /dev/null @@ -1,113 +0,0 @@ -# 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 diff --git a/examples/clients/simple-chatbot/mcp_simple_chatbot/.env.example b/examples/clients/simple-chatbot/mcp_simple_chatbot/.env.example deleted file mode 100644 index 39be363c20..0000000000 --- a/examples/clients/simple-chatbot/mcp_simple_chatbot/.env.example +++ /dev/null @@ -1 +0,0 @@ -LLM_API_KEY=gsk_1234567890 diff --git a/examples/clients/simple-chatbot/mcp_simple_chatbot/main.py b/examples/clients/simple-chatbot/mcp_simple_chatbot/main.py deleted file mode 100644 index 78a81a4d9f..0000000000 --- a/examples/clients/simple-chatbot/mcp_simple_chatbot/main.py +++ /dev/null @@ -1,419 +0,0 @@ -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[Any]: - """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 = [] - - for item in tools_response: - if isinstance(item, tuple) and item[0] == "tools": - tools.extend(Tool(tool.name, tool.description, tool.inputSchema, 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 = [] - 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"] - total = result["total"] - percentage = (progress / total) * 100 - 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 = [] - 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() diff --git a/examples/clients/simple-chatbot/mcp_simple_chatbot/requirements.txt b/examples/clients/simple-chatbot/mcp_simple_chatbot/requirements.txt deleted file mode 100644 index 2292072ffa..0000000000 --- a/examples/clients/simple-chatbot/mcp_simple_chatbot/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -python-dotenv>=1.0.0 -requests>=2.31.0 -mcp>=1.0.0 -uvicorn>=0.32.1 diff --git a/examples/clients/simple-chatbot/mcp_simple_chatbot/servers_config.json b/examples/clients/simple-chatbot/mcp_simple_chatbot/servers_config.json deleted file mode 100644 index 3a92d05d1e..0000000000 --- a/examples/clients/simple-chatbot/mcp_simple_chatbot/servers_config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "sqlite": { - "command": "uvx", - "args": ["mcp-server-sqlite", "--db-path", "./test.db"] - }, - "puppeteer": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-puppeteer"] - } - } -} diff --git a/examples/clients/simple-chatbot/mcp_simple_chatbot/test.db b/examples/clients/simple-chatbot/mcp_simple_chatbot/test.db deleted file mode 100644 index d08dabc936..0000000000 Binary files a/examples/clients/simple-chatbot/mcp_simple_chatbot/test.db and /dev/null differ diff --git a/examples/clients/simple-chatbot/pyproject.toml b/examples/clients/simple-chatbot/pyproject.toml deleted file mode 100644 index 564b42df33..0000000000 --- a/examples/clients/simple-chatbot/pyproject.toml +++ /dev/null @@ -1,48 +0,0 @@ -[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 = "Edoardo Cilia" }] -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", - "requests>=2.31.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"] diff --git a/examples/clients/simple-task-client/README.md b/examples/clients/simple-task-client/README.md deleted file mode 100644 index 103be0f1fb..0000000000 --- a/examples/clients/simple-task-client/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Simple Task Client - -A minimal MCP client demonstrating polling for task results over streamable HTTP. - -## Running - -First, start the simple-task server in another terminal: - -```bash -cd examples/servers/simple-task -uv run mcp-simple-task -``` - -Then run the client: - -```bash -cd examples/clients/simple-task-client -uv run mcp-simple-task-client -``` - -Use `--url` to connect to a different server. - -## What it does - -1. Connects to the server via streamable HTTP -2. Calls the `long_running_task` tool as a task -3. Polls the task status until completion -4. Retrieves and prints the result - -## Expected output - -```text -Available tools: ['long_running_task'] - -Calling tool as a task... -Task created: - Status: working - Starting work... - Status: working - Processing step 1... - Status: working - Processing step 2... - Status: completed - - -Result: Task completed! -``` diff --git a/examples/clients/simple-task-client/mcp_simple_task_client/__init__.py b/examples/clients/simple-task-client/mcp_simple_task_client/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/clients/simple-task-client/mcp_simple_task_client/__main__.py b/examples/clients/simple-task-client/mcp_simple_task_client/__main__.py deleted file mode 100644 index 2fc2cda8d9..0000000000 --- a/examples/clients/simple-task-client/mcp_simple_task_client/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .main import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/clients/simple-task-client/mcp_simple_task_client/main.py b/examples/clients/simple-task-client/mcp_simple_task_client/main.py deleted file mode 100644 index 12691162ab..0000000000 --- a/examples/clients/simple-task-client/mcp_simple_task_client/main.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Simple task client demonstrating MCP tasks polling over streamable HTTP.""" - -import asyncio - -import click -from mcp import ClientSession -from mcp.client.streamable_http import streamablehttp_client -from mcp.types import CallToolResult, TextContent - - -async def run(url: str) -> None: - async with streamablehttp_client(url) as (read, write, _): - async with ClientSession(read, write) as session: - await session.initialize() - - # List tools - tools = await session.list_tools() - print(f"Available tools: {[t.name for t in tools.tools]}") - - # Call the tool as a task - print("\nCalling tool as a task...") - - result = await session.experimental.call_tool_as_task( - "long_running_task", - arguments={}, - ttl=60000, - ) - task_id = result.task.taskId - print(f"Task created: {task_id}") - - # Poll until done (respects server's pollInterval hint) - async for status in session.experimental.poll_task(task_id): - print(f" Status: {status.status} - {status.statusMessage or ''}") - - # Check final status - if status.status != "completed": - print(f"Task ended with status: {status.status}") - return - - # Get the result - task_result = await session.experimental.get_task_result(task_id, CallToolResult) - content = task_result.content[0] - if isinstance(content, TextContent): - print(f"\nResult: {content.text}") - - -@click.command() -@click.option("--url", default="http://localhost:8000/mcp", help="Server URL") -def main(url: str) -> int: - asyncio.run(run(url)) - return 0 - - -if __name__ == "__main__": - main() diff --git a/examples/clients/simple-task-client/pyproject.toml b/examples/clients/simple-task-client/pyproject.toml deleted file mode 100644 index da10392e3c..0000000000 --- a/examples/clients/simple-task-client/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "mcp-simple-task-client" -version = "0.1.0" -description = "A simple MCP client demonstrating task polling" -readme = "README.md" -requires-python = ">=3.10" -authors = [{ name = "Anthropic, PBC." }] -keywords = ["mcp", "llm", "tasks", "client"] -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.0", "mcp"] - -[project.scripts] -mcp-simple-task-client = "mcp_simple_task_client.main:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["mcp_simple_task_client"] - -[tool.pyright] -include = ["mcp_simple_task_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", "ruff>=0.6.9"] diff --git a/examples/clients/simple-task-interactive-client/README.md b/examples/clients/simple-task-interactive-client/README.md deleted file mode 100644 index ac73d2bc12..0000000000 --- a/examples/clients/simple-task-interactive-client/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Simple Interactive Task Client - -A minimal MCP client demonstrating responses to interactive tasks (elicitation and sampling). - -## Running - -First, start the interactive task server in another terminal: - -```bash -cd examples/servers/simple-task-interactive -uv run mcp-simple-task-interactive -``` - -Then run the client: - -```bash -cd examples/clients/simple-task-interactive-client -uv run mcp-simple-task-interactive-client -``` - -Use `--url` to connect to a different server. - -## What it does - -1. Connects to the server via streamable HTTP -2. Calls `confirm_delete` - server asks for confirmation, client responds via terminal -3. Calls `write_haiku` - server requests LLM completion, client returns a hardcoded haiku - -## Key concepts - -### Elicitation callback - -```python -async def elicitation_callback(context, params) -> ElicitResult: - # Handle user input request from server - return ElicitResult(action="accept", content={"confirm": True}) -``` - -### Sampling callback - -```python -async def sampling_callback(context, params) -> CreateMessageResult: - # Handle LLM completion request from server - return CreateMessageResult(model="...", role="assistant", content=...) -``` - -### Using call_tool_as_task - -```python -# Call a tool as a task (returns immediately with task reference) -result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) -task_id = result.task.taskId - -# Get result - this delivers elicitation/sampling requests and blocks until complete -final = await session.experimental.get_task_result(task_id, CallToolResult) -``` - -**Important**: The `get_task_result()` call is what triggers the delivery of elicitation -and sampling requests to your callbacks. It blocks until the task completes and returns -the final result. - -## Expected output - -```text -Available tools: ['confirm_delete', 'write_haiku'] - ---- Demo 1: Elicitation --- -Calling confirm_delete tool... -Task created: - -[Elicitation] Server asks: Are you sure you want to delete 'important.txt'? -Your response (y/n): y -[Elicitation] Responding with: confirm=True -Result: Deleted 'important.txt' - ---- Demo 2: Sampling --- -Calling write_haiku tool... -Task created: - -[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves -[Sampling] Responding with haiku -Result: -Haiku: -Cherry blossoms fall -Softly on the quiet pond -Spring whispers goodbye -``` diff --git a/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__init__.py b/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__main__.py b/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__main__.py deleted file mode 100644 index 2fc2cda8d9..0000000000 --- a/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .main import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py b/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py deleted file mode 100644 index a8a47dc57c..0000000000 --- a/examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Simple interactive task client demonstrating elicitation and sampling responses. - -This example demonstrates the spec-compliant polling pattern: -1. Poll tasks/get watching for status changes -2. On input_required, call tasks/result to receive elicitation/sampling requests -3. Continue until terminal status, then retrieve final result -""" - -import asyncio -from typing import Any - -import click -from mcp import ClientSession -from mcp.client.streamable_http import streamablehttp_client -from mcp.shared.context import RequestContext -from mcp.types import ( - CallToolResult, - CreateMessageRequestParams, - CreateMessageResult, - ElicitRequestParams, - ElicitResult, - TextContent, -) - - -async def elicitation_callback( - context: RequestContext[ClientSession, Any], - params: ElicitRequestParams, -) -> ElicitResult: - """Handle elicitation requests from the server.""" - print(f"\n[Elicitation] Server asks: {params.message}") - - # Simple terminal prompt - response = input("Your response (y/n): ").strip().lower() - confirmed = response in ("y", "yes", "true", "1") - - print(f"[Elicitation] Responding with: confirm={confirmed}") - return ElicitResult(action="accept", content={"confirm": confirmed}) - - -async def sampling_callback( - context: RequestContext[ClientSession, Any], - params: CreateMessageRequestParams, -) -> CreateMessageResult: - """Handle sampling requests from the server.""" - # Get the prompt from the first message - prompt = "unknown" - if params.messages: - content = params.messages[0].content - if isinstance(content, TextContent): - prompt = content.text - - print(f"\n[Sampling] Server requests LLM completion for: {prompt}") - - # Return a hardcoded haiku (in real use, call your LLM here) - haiku = """Cherry blossoms fall -Softly on the quiet pond -Spring whispers goodbye""" - - print("[Sampling] Responding with haiku") - return CreateMessageResult( - model="mock-haiku-model", - role="assistant", - content=TextContent(type="text", text=haiku), - ) - - -def get_text(result: CallToolResult) -> str: - """Extract text from a CallToolResult.""" - if result.content and isinstance(result.content[0], TextContent): - return result.content[0].text - return "(no text)" - - -async def run(url: str) -> None: - async with streamablehttp_client(url) as (read, write, _): - async with ClientSession( - read, - write, - elicitation_callback=elicitation_callback, - sampling_callback=sampling_callback, - ) as session: - await session.initialize() - - # List tools - tools = await session.list_tools() - print(f"Available tools: {[t.name for t in tools.tools]}") - - # Demo 1: Elicitation (confirm_delete) - print("\n--- Demo 1: Elicitation ---") - print("Calling confirm_delete tool...") - - elicit_task = await session.experimental.call_tool_as_task("confirm_delete", {"filename": "important.txt"}) - elicit_task_id = elicit_task.task.taskId - print(f"Task created: {elicit_task_id}") - - # Poll until terminal, calling tasks/result on input_required - async for status in session.experimental.poll_task(elicit_task_id): - print(f"[Poll] Status: {status.status}") - if status.status == "input_required": - # Server needs input - tasks/result delivers the elicitation request - elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult) - break - else: - # poll_task exited due to terminal status - elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult) - - print(f"Result: {get_text(elicit_result)}") - - # Demo 2: Sampling (write_haiku) - print("\n--- Demo 2: Sampling ---") - print("Calling write_haiku tool...") - - sampling_task = await session.experimental.call_tool_as_task("write_haiku", {"topic": "autumn leaves"}) - sampling_task_id = sampling_task.task.taskId - print(f"Task created: {sampling_task_id}") - - # Poll until terminal, calling tasks/result on input_required - async for status in session.experimental.poll_task(sampling_task_id): - print(f"[Poll] Status: {status.status}") - if status.status == "input_required": - sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult) - break - else: - sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult) - - print(f"Result:\n{get_text(sampling_result)}") - - -@click.command() -@click.option("--url", default="http://localhost:8000/mcp", help="Server URL") -def main(url: str) -> int: - asyncio.run(run(url)) - return 0 - - -if __name__ == "__main__": - main() diff --git a/examples/clients/simple-task-interactive-client/pyproject.toml b/examples/clients/simple-task-interactive-client/pyproject.toml deleted file mode 100644 index 224bbc5917..0000000000 --- a/examples/clients/simple-task-interactive-client/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "mcp-simple-task-interactive-client" -version = "0.1.0" -description = "A simple MCP client demonstrating interactive task responses" -readme = "README.md" -requires-python = ">=3.10" -authors = [{ name = "Anthropic, PBC." }] -keywords = ["mcp", "llm", "tasks", "client", "elicitation", "sampling"] -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.0", "mcp"] - -[project.scripts] -mcp-simple-task-interactive-client = "mcp_simple_task_interactive_client.main:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["mcp_simple_task_interactive_client"] - -[tool.pyright] -include = ["mcp_simple_task_interactive_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", "ruff>=0.6.9"] diff --git a/examples/clients/sse-polling-client/README.md b/examples/clients/sse-polling-client/README.md deleted file mode 100644 index 78449aa832..0000000000 --- a/examples/clients/sse-polling-client/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# 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: ) -- `--items`: Number of items to process (default: 10) -- `--checkpoint-every`: Checkpoint interval (default: 3) -- `--log-level`: Logging level (default: DEBUG) diff --git a/examples/clients/sse-polling-client/mcp_sse_polling_client/__init__.py b/examples/clients/sse-polling-client/mcp_sse_polling_client/__init__.py deleted file mode 100644 index ee69b32c96..0000000000 --- a/examples/clients/sse-polling-client/mcp_sse_polling_client/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""SSE Polling Demo Client - demonstrates auto-reconnect for long-running tasks.""" diff --git a/examples/clients/sse-polling-client/mcp_sse_polling_client/main.py b/examples/clients/sse-polling-client/mcp_sse_polling_client/main.py deleted file mode 100644 index 1defd8eaa4..0000000000 --- a/examples/clients/sse-polling-client/mcp_sse_polling_client/main.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -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 streamablehttp_client - -logger = logging.getLogger(__name__) - - -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 streamablehttp_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() diff --git a/examples/clients/sse-polling-client/pyproject.toml b/examples/clients/sse-polling-client/pyproject.toml deleted file mode 100644 index ae896708d4..0000000000 --- a/examples/clients/sse-polling-client/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/everything-server/README.md b/examples/servers/everything-server/README.md deleted file mode 100644 index 3512665cb9..0000000000 --- a/examples/servers/everything-server/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# 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. diff --git a/examples/servers/everything-server/mcp_everything_server/__init__.py b/examples/servers/everything-server/mcp_everything_server/__init__.py deleted file mode 100644 index d539062d4f..0000000000 --- a/examples/servers/everything-server/mcp_everything_server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""MCP Everything Server - Comprehensive conformance test server.""" - -__version__ = "0.1.0" diff --git a/examples/servers/everything-server/mcp_everything_server/__main__.py b/examples/servers/everything-server/mcp_everything_server/__main__.py deleted file mode 100644 index 2eff688f02..0000000000 --- a/examples/servers/everything-server/mcp_everything_server/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""CLI entry point for the MCP Everything Server.""" - -from .server import main - -if __name__ == "__main__": - main() diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py deleted file mode 100644 index 1f1ee7ecc4..0000000000 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ /dev/null @@ -1,458 +0,0 @@ -#!/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 - -import click -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.fastmcp.prompts.base import UserMessage -from mcp.server.session import ServerSession -from mcp.server.streamable_http import EventCallback, EventMessage, EventStore -from mcp.types import ( - AudioContent, - Completion, - CompletionArgument, - CompletionContext, - EmbeddedResource, - ImageContent, - JSONRPCMessage, - PromptReference, - ResourceTemplateReference, - SamplingMessage, - TextContent, - TextResourceContents, -) -from pydantic import AnyUrl, 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() - -mcp = FastMCP( - name="mcp-conformance-test-server", - event_store=event_store, - retry_interval=100, # 100ms retry interval for SSE polling -) - - -# 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, mimeType="image/png")] - - -@mcp.tool() -def test_audio_content() -> list[AudioContent]: - """Tests audio content response""" - return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mimeType="audio/wav")] - - -@mcp.tool() -def test_embedded_resource() -> list[EmbeddedResource]: - """Tests embedded resource content response""" - return [ - EmbeddedResource( - type="resource", - resource=TextResourceContents( - uri=AnyUrl("test://embedded-resource"), - mimeType="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, mimeType="image/png"), - EmbeddedResource( - type="resource", - resource=TextResourceContents( - uri=AnyUrl("test://mixed-content-resource"), - mimeType="application/json", - text='{"test": "data", "value": 123}', - ), - ), - ] - - -@mcp.tool() -async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str: - """Tests tool that emits log messages during execution""" - await ctx.info("Tool execution started") - await asyncio.sleep(0.05) - - await ctx.info("Tool processing data") - await asyncio.sleep(0.05) - - await ctx.info("Tool execution completed") - return "Tool with logging executed successfully" - - -@mcp.tool() -async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> 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.progressToken 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[ServerSession, None]) -> str: - """Tests server-initiated sampling (LLM completion request)""" - try: - # Request sampling from client - result = await ctx.session.create_message( - messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))], - max_tokens=100, - ) - - # 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[ServerSession, None]) -> 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[ServerSession, None]) -> 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[ServerSession, None]) -> 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() -async def test_reconnection(ctx: Context[ServerSession, None]) -> str: - """Tests SSE polling by closing stream mid-call (SEP-1699)""" - await ctx.info("Before disconnect") - - await ctx.close_sse_stream() - - await asyncio.sleep(0.2) # Wait for client to reconnect - - await ctx.info("After reconnect") - return "Reconnection test completed" - - -# 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=AnyUrl(resourceUri), - mimeType="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, mimeType="image/png")), - UserMessage(role="user", content=TextContent(type="text", text="Please analyze the image above.")), - ] - - -# Custom request handlers -# TODO(felix): Add public APIs to FastMCP for subscribe_resource, unsubscribe_resource, -# and set_logging_level to avoid accessing protected _mcp_server attribute. -@mcp._mcp_server.set_logging_level() # pyright: ignore[reportPrivateUsage] -async def handle_set_logging_level(level: str) -> None: - """Handle logging level changes""" - logger.info(f"Log level set to: {level}") - # In a real implementation, you would adjust the logging level here - # For conformance testing, we just acknowledge the request - - -async def handle_subscribe(uri: AnyUrl) -> None: - """Handle resource subscription""" - resource_subscriptions.add(str(uri)) - logger.info(f"Subscribed to resource: {uri}") - - -async def handle_unsubscribe(uri: AnyUrl) -> None: - """Handle resource unsubscription""" - resource_subscriptions.discard(str(uri)) - logger.info(f"Unsubscribed from resource: {uri}") - - -mcp._mcp_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage] -mcp._mcp_server.unsubscribe_resource()(handle_unsubscribe) # pyright: ignore[reportPrivateUsage] - - -@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, hasMore=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.settings.port = port - mcp.run(transport="streamable-http") - - return 0 - - -if __name__ == "__main__": - main() diff --git a/examples/servers/everything-server/pyproject.toml b/examples/servers/everything-server/pyproject.toml deleted file mode 100644 index ff67bf5577..0000000000 --- a/examples/servers/everything-server/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/simple-auth/README.md b/examples/servers/simple-auth/README.md deleted file mode 100644 index b80e98a047..0000000000 --- a/examples/servers/simple-auth/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# 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 authorization server on port 8002 -uv run mcp-simple-auth-legacy --port=8002 -``` - -**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=8002 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" -``` diff --git a/examples/servers/simple-auth/mcp_simple_auth/__init__.py b/examples/servers/simple-auth/mcp_simple_auth/__init__.py deleted file mode 100644 index 3e12b31832..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Simple MCP server with GitHub OAuth authentication.""" diff --git a/examples/servers/simple-auth/mcp_simple_auth/__main__.py b/examples/servers/simple-auth/mcp_simple_auth/__main__.py deleted file mode 100644 index 2365ff5a1b..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""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] diff --git a/examples/servers/simple-auth/mcp_simple_auth/auth_server.py b/examples/servers/simple-auth/mcp_simple_auth/auth_server.py deleted file mode 100644 index 80a2e8b8a3..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/auth_server.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -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 - } - ) - - 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] diff --git a/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py b/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py deleted file mode 100644 index b0455c3e89..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -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.fastmcp.server import FastMCP - -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) -> FastMCP: - """Create a simple FastMCP 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 = FastMCP( - name="Simple Auth MCP Server", - instructions="A simple MCP server with simple credential authentication", - auth_server_provider=oauth_provider, - host=server_settings.host, - port=server_settings.port, - debug=True, - auth=mcp_auth_settings, - ) - - @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) - return 0 - - -if __name__ == "__main__": - main() # type: ignore[call-arg] diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py deleted file mode 100644 index 5d88505708..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -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.fastmcp.server import FastMCP - -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) -> FastMCP: - """ - 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 FastMCP server as a Resource Server - app = FastMCP( - name="MCP Resource Server", - instructions="Resource Server that validates tokens via Authorization Server introspection", - host=settings.host, - port=settings.port, - 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, - ), - ) - - @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) - logger.info("Server stopped") - return 0 - except Exception: - logger.exception("Server error") - return 1 - - -if __name__ == "__main__": - main() # type: ignore[call-arg] diff --git a/examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py b/examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py deleted file mode 100644 index e3a25d3e8c..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py +++ /dev/null @@ -1,275 +0,0 @@ -""" -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 logging -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 - -logger = logging.getLogger(__name__) - - -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""" - - - - MCP Demo Authentication - - - -

MCP Demo Authentication

-

This is a simplified authentication demo. Use the demo credentials below:

-

Username: demo_user
- Password: demo_password

- -
- -
- - -
-
- - -
- -
- - - """ - - 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 - ) - 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 - ) - - # 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] diff --git a/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py b/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py deleted file mode 100644 index 5228d034e4..0000000000 --- a/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py +++ /dev/null @@ -1,106 +0,0 @@ -"""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 - ) - 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) diff --git a/examples/servers/simple-auth/pyproject.toml b/examples/servers/simple-auth/pyproject.toml deleted file mode 100644 index eb2b18561b..0000000000 --- a/examples/servers/simple-auth/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/simple-pagination/README.md b/examples/servers/simple-pagination/README.md deleted file mode 100644 index e732b8efbe..0000000000 --- a/examples/servers/simple-pagination/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# 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 SSE transport: - -```bash -# Using stdio transport (default) -uv run mcp-simple-pagination - -# Using SSE transport on custom port -uv run mcp-simple-pagination --transport sse --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 diff --git a/examples/servers/simple-pagination/mcp_simple_pagination/__init__.py b/examples/servers/simple-pagination/mcp_simple_pagination/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-pagination/mcp_simple_pagination/__main__.py b/examples/servers/simple-pagination/mcp_simple_pagination/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-pagination/mcp_simple_pagination/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-pagination/mcp_simple_pagination/server.py b/examples/servers/simple-pagination/mcp_simple_pagination/server.py deleted file mode 100644 index 360cbc3cff..0000000000 --- a/examples/servers/simple-pagination/mcp_simple_pagination/server.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -Simple MCP server demonstrating pagination for tools, resources, and prompts. - -This example shows how to use the paginated decorators to handle large lists -of items that need to be split across multiple pages. -""" - -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from pydantic import AnyUrl -from starlette.requests import Request - -# 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}", - inputSchema={"type": "object", "properties": {"input": {"type": "string"}}}, - ) - for i in range(1, 26) # 25 tools total -] - -SAMPLE_RESOURCES = [ - types.Resource( - uri=AnyUrl(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 -] - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option( - "--transport", - type=click.Choice(["stdio", "sse"]), - default="stdio", - help="Transport type", -) -def main(port: int, transport: str) -> int: - app = Server("mcp-simple-pagination") - - # Paginated list_tools - returns 5 tools per page - @app.list_tools() - async def list_tools_paginated(request: types.ListToolsRequest) -> types.ListToolsResult: - page_size = 5 - - cursor = request.params.cursor if request.params is not None else None - if cursor is None: - # First page - start_idx = 0 - else: - # Parse cursor to get the start index - try: - start_idx = int(cursor) - except (ValueError, TypeError): - # Invalid cursor, return empty - return types.ListToolsResult(tools=[], nextCursor=None) - - # Get the page of tools - page_tools = SAMPLE_TOOLS[start_idx : start_idx + page_size] - - # Determine if there are more pages - next_cursor = None - if start_idx + page_size < len(SAMPLE_TOOLS): - next_cursor = str(start_idx + page_size) - - return types.ListToolsResult(tools=page_tools, nextCursor=next_cursor) - - # Paginated list_resources - returns 10 resources per page - @app.list_resources() - async def list_resources_paginated( - request: types.ListResourcesRequest, - ) -> types.ListResourcesResult: - page_size = 10 - - cursor = request.params.cursor if request.params is not None else None - if cursor is None: - # First page - start_idx = 0 - else: - # Parse cursor to get the start index - try: - start_idx = int(cursor) - except (ValueError, TypeError): - # Invalid cursor, return empty - return types.ListResourcesResult(resources=[], nextCursor=None) - - # Get the page of resources - page_resources = SAMPLE_RESOURCES[start_idx : start_idx + page_size] - - # Determine if there are more pages - next_cursor = None - if start_idx + page_size < len(SAMPLE_RESOURCES): - next_cursor = str(start_idx + page_size) - - return types.ListResourcesResult(resources=page_resources, nextCursor=next_cursor) - - # Paginated list_prompts - returns 7 prompts per page - @app.list_prompts() - async def list_prompts_paginated( - request: types.ListPromptsRequest, - ) -> types.ListPromptsResult: - page_size = 7 - - cursor = request.params.cursor if request.params is not None else None - if cursor is None: - # First page - start_idx = 0 - else: - # Parse cursor to get the start index - try: - start_idx = int(cursor) - except (ValueError, TypeError): - # Invalid cursor, return empty - return types.ListPromptsResult(prompts=[], nextCursor=None) - - # Get the page of prompts - page_prompts = SAMPLE_PROMPTS[start_idx : start_idx + page_size] - - # Determine if there are more pages - next_cursor = None - if start_idx + page_size < len(SAMPLE_PROMPTS): - next_cursor = str(start_idx + page_size) - - return types.ListPromptsResult(prompts=page_prompts, nextCursor=next_cursor) - - # Implement call_tool handler - @app.call_tool() - async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - # Find the tool in our sample data - tool = next((t for t in SAMPLE_TOOLS if t.name == name), None) - if not tool: - raise ValueError(f"Unknown tool: {name}") - - # Simple mock response - return [ - types.TextContent( - type="text", - text=f"Called tool '{name}' with arguments: {arguments}", - ) - ] - - # Implement read_resource handler - @app.read_resource() - async def read_resource(uri: AnyUrl) -> str: - # Find the resource in our sample data - resource = next((r for r in SAMPLE_RESOURCES if r.uri == uri), None) - if not resource: - raise ValueError(f"Unknown resource: {uri}") - - # Return a simple string - the decorator will convert it to TextResourceContents - return f"Content of {resource.name}: This is sample content for the resource." - - # Implement get_prompt handler - @app.get_prompt() - async def get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: - # Find the prompt in our sample data - prompt = next((p for p in SAMPLE_PROMPTS if p.name == name), None) - if not prompt: - raise ValueError(f"Unknown prompt: {name}") - - # Simple mock response - message_text = f"This is the prompt '{name}'" - if arguments: - message_text += f" with arguments: {arguments}" - - return types.GetPromptResult( - description=prompt.description, - messages=[ - types.PromptMessage( - role="user", - content=types.TextContent(type="text", text=message_text), - ) - ], - ) - - if transport == "sse": - from mcp.server.sse import SseServerTransport - from starlette.applications import Starlette - from starlette.responses import Response - from starlette.routing import Mount, Route - - sse = SseServerTransport("/messages/") - - async def handle_sse(request: Request): - async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage] - await app.run(streams[0], streams[1], app.create_initialization_options()) - return Response() - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse, methods=["GET"]), - Mount("/messages/", app=sse.handle_post_message), - ], - ) - - import uvicorn - - uvicorn.run(starlette_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 diff --git a/examples/servers/simple-pagination/pyproject.toml b/examples/servers/simple-pagination/pyproject.toml deleted file mode 100644 index 14de502574..0000000000 --- a/examples/servers/simple-pagination/pyproject.toml +++ /dev/null @@ -1,47 +0,0 @@ -[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 = "Anthropic, PBC." }] -maintainers = [ - { name = "David Soria Parra", email = "davidsp@anthropic.com" }, - { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, -] -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"] diff --git a/examples/servers/simple-prompt/.python-version b/examples/servers/simple-prompt/.python-version deleted file mode 100644 index c8cfe39591..0000000000 --- a/examples/servers/simple-prompt/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/examples/servers/simple-prompt/README.md b/examples/servers/simple-prompt/README.md deleted file mode 100644 index 48e796e198..0000000000 --- a/examples/servers/simple-prompt/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 SSE transport: - -```bash -# Using stdio transport (default) -uv run mcp-simple-prompt - -# Using SSE transport on custom port -uv run mcp-simple-prompt --transport sse --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()) -``` diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py b/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py b/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py deleted file mode 100644 index 76b598f931..0000000000 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ /dev/null @@ -1,112 +0,0 @@ -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from starlette.requests import Request - - -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 - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option( - "--transport", - type=click.Choice(["stdio", "sse"]), - default="stdio", - help="Transport type", -) -def main(port: int, transport: str) -> int: - app = Server("mcp-simple-prompt") - - @app.list_prompts() - async def list_prompts() -> list[types.Prompt]: - return [ - 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, - ), - ], - ) - ] - - @app.get_prompt() - async def get_prompt(name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult: - if name != "simple": - raise ValueError(f"Unknown prompt: {name}") - - if arguments is None: - arguments = {} - - return types.GetPromptResult( - messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")), - description="A simple prompt with optional context and topic arguments", - ) - - if transport == "sse": - from mcp.server.sse import SseServerTransport - from starlette.applications import Starlette - from starlette.responses import Response - from starlette.routing import Mount, Route - - sse = SseServerTransport("/messages/") - - async def handle_sse(request: Request): - async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage] - await app.run(streams[0], streams[1], app.create_initialization_options()) - return Response() - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse), - Mount("/messages/", app=sse.handle_post_message), - ], - ) - - import uvicorn - - uvicorn.run(starlette_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 diff --git a/examples/servers/simple-prompt/pyproject.toml b/examples/servers/simple-prompt/pyproject.toml deleted file mode 100644 index 28fe265746..0000000000 --- a/examples/servers/simple-prompt/pyproject.toml +++ /dev/null @@ -1,47 +0,0 @@ -[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 = "Anthropic, PBC." }] -maintainers = [ - { name = "David Soria Parra", email = "davidsp@anthropic.com" }, - { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, -] -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"] diff --git a/examples/servers/simple-resource/.python-version b/examples/servers/simple-resource/.python-version deleted file mode 100644 index c8cfe39591..0000000000 --- a/examples/servers/simple-resource/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/examples/servers/simple-resource/README.md b/examples/servers/simple-resource/README.md deleted file mode 100644 index df674e91e4..0000000000 --- a/examples/servers/simple-resource/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# MCP Simple Resource - -A simple MCP server that exposes sample text files as resources. - -## Usage - -Start the server using either stdio (default) or SSE transport: - -```bash -# Using stdio transport (default) -uv run mcp-simple-resource - -# Using SSE transport on custom port -uv run mcp-simple-resource --transport sse --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 mcp.types 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()) - -``` diff --git a/examples/servers/simple-resource/mcp_simple_resource/__init__.py b/examples/servers/simple-resource/mcp_simple_resource/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-resource/mcp_simple_resource/__main__.py b/examples/servers/simple-resource/mcp_simple_resource/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-resource/mcp_simple_resource/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py deleted file mode 100644 index 151a23eab4..0000000000 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ /dev/null @@ -1,93 +0,0 @@ -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.server.lowlevel.helper_types import ReadResourceContents -from pydantic import AnyUrl, FileUrl -from starlette.requests import Request - -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", - }, -} - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option( - "--transport", - type=click.Choice(["stdio", "sse"]), - default="stdio", - help="Transport type", -) -def main(port: int, transport: str) -> int: - app = Server("mcp-simple-resource") - - @app.list_resources() - async def list_resources() -> list[types.Resource]: - return [ - types.Resource( - uri=FileUrl(f"file:///{name}.txt"), - name=name, - title=SAMPLE_RESOURCES[name]["title"], - description=f"A sample text resource named {name}", - mimeType="text/plain", - ) - for name in SAMPLE_RESOURCES.keys() - ] - - @app.read_resource() - async def read_resource(uri: AnyUrl): - if uri.path is None: - raise ValueError(f"Invalid resource path: {uri}") - name = uri.path.replace(".txt", "").lstrip("/") - - if name not in SAMPLE_RESOURCES: - raise ValueError(f"Unknown resource: {uri}") - - return [ReadResourceContents(content=SAMPLE_RESOURCES[name]["content"], mime_type="text/plain")] - - if transport == "sse": - from mcp.server.sse import SseServerTransport - from starlette.applications import Starlette - from starlette.responses import Response - from starlette.routing import Mount, Route - - sse = SseServerTransport("/messages/") - - async def handle_sse(request: Request): - async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage] - await app.run(streams[0], streams[1], app.create_initialization_options()) - return Response() - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse, methods=["GET"]), - Mount("/messages/", app=sse.handle_post_message), - ], - ) - - import uvicorn - - uvicorn.run(starlette_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 diff --git a/examples/servers/simple-resource/pyproject.toml b/examples/servers/simple-resource/pyproject.toml deleted file mode 100644 index 14c2bd38cc..0000000000 --- a/examples/servers/simple-resource/pyproject.toml +++ /dev/null @@ -1,47 +0,0 @@ -[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 = "Anthropic, PBC." }] -maintainers = [ - { name = "David Soria Parra", email = "davidsp@anthropic.com" }, - { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, -] -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"] diff --git a/examples/servers/simple-streamablehttp-stateless/README.md b/examples/servers/simple-streamablehttp-stateless/README.md deleted file mode 100644 index b87250b353..0000000000 --- a/examples/servers/simple-streamablehttp-stateless/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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 -- Task lifecycle scoped to individual 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. diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__init__.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__main__.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__main__.py deleted file mode 100644 index 1664737e3a..0000000000 --- a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .server import main - -if __name__ == "__main__": - # Click will handle CLI arguments - import sys - - sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py deleted file mode 100644 index f1b3987d28..0000000000 --- a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py +++ /dev/null @@ -1,140 +0,0 @@ -import contextlib -import logging -from collections.abc import AsyncIterator -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.applications import Starlette -from starlette.middleware.cors import CORSMiddleware -from starlette.routing import Mount -from starlette.types import Receive, Scope, Send - -logger = logging.getLogger(__name__) - - -@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-stateless-demo") - - @app.call_tool() - async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - ctx = app.request_context - 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( - 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.TextContent( - type="text", - text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"), - ) - ] - - @app.list_tools() - async def list_tools() -> list[types.Tool]: - return [ - types.Tool( - name="start-notification-stream", - description=("Sends a stream of notifications with configurable count and interval"), - inputSchema={ - "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"), - }, - }, - }, - ) - ] - - # Create the session manager with true stateless mode - session_manager = StreamableHTTPSessionManager( - app=app, - event_store=None, - json_response=json_response, - stateless=True, - ) - - async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None: - await session_manager.handle_request(scope, receive, send) - - @contextlib.asynccontextmanager - async def lifespan(app: Starlette) -> AsyncIterator[None]: - """Context manager for session manager.""" - async with session_manager.run(): - logger.info("Application started with StreamableHTTP session manager!") - try: - yield - finally: - logger.info("Application shutting down...") - - # Create an ASGI application using the transport - starlette_app = Starlette( - debug=True, - routes=[ - Mount("/mcp", app=handle_streamable_http), - ], - lifespan=lifespan, - ) - - # 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=["*"], # Allow all origins - adjust as needed for production - allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods - expose_headers=["Mcp-Session-Id"], - ) - - import uvicorn - - uvicorn.run(starlette_app, host="127.0.0.1", port=port) - - return 0 diff --git a/examples/servers/simple-streamablehttp-stateless/pyproject.toml b/examples/servers/simple-streamablehttp-stateless/pyproject.toml deleted file mode 100644 index 0e695695cb..0000000000 --- a/examples/servers/simple-streamablehttp-stateless/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/simple-streamablehttp/README.md b/examples/servers/simple-streamablehttp/README.md deleted file mode 100644 index 9836367170..0000000000 --- a/examples/servers/simple-streamablehttp/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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 -- Task management with anyio task groups -- Ability to send multiple notifications over time to the client -- Proper resource cleanup and lifespan management -- 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) diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__init__.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__main__.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__main__.py deleted file mode 100644 index 21862e45fb..0000000000 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .server import main - -if __name__ == "__main__": - main() # type: ignore[call-arg] diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py deleted file mode 100644 index 0c3081ed64..0000000000 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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 diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py deleted file mode 100644 index 4b2604b9af..0000000000 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ /dev/null @@ -1,165 +0,0 @@ -import contextlib -import logging -from collections.abc import AsyncIterator -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from pydantic import AnyUrl -from starlette.applications import Starlette -from starlette.middleware.cors import CORSMiddleware -from starlette.routing import Mount -from starlette.types import Receive, Scope, Send - -from .event_store import InMemoryEventStore - -# Configure logging -logger = logging.getLogger(__name__) - - -@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") - - @app.call_tool() - async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - ctx = app.request_context - 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( - 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 notificaiton though standalone SSE - # established by GET request - await ctx.session.send_resource_updated(uri=AnyUrl("http:///test_resource")) - return [ - types.TextContent( - type="text", - text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"), - ) - ] - - @app.list_tools() - async def list_tools() -> list[types.Tool]: - return [ - types.Tool( - name="start-notification-stream", - description=("Sends a stream of notifications with configurable count and interval"), - inputSchema={ - "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"), - }, - }, - }, - ) - ] - - # 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() - - # Create the session manager with our app and event store - session_manager = StreamableHTTPSessionManager( - app=app, - event_store=event_store, # Enable resumability - json_response=json_response, - ) - - # ASGI handler for streamable HTTP connections - async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None: - await session_manager.handle_request(scope, receive, send) - - @contextlib.asynccontextmanager - async def lifespan(app: Starlette) -> AsyncIterator[None]: - """Context manager for managing session manager lifecycle.""" - async with session_manager.run(): - logger.info("Application started with StreamableHTTP session manager!") - try: - yield - finally: - logger.info("Application shutting down...") - - # Create an ASGI application using the transport - starlette_app = Starlette( - debug=True, - routes=[ - Mount("/mcp", app=handle_streamable_http), - ], - lifespan=lifespan, - ) - - # 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=["*"], # Allow all origins - adjust as needed for production - allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods - expose_headers=["Mcp-Session-Id"], - ) - - import uvicorn - - uvicorn.run(starlette_app, host="127.0.0.1", port=port) - - return 0 diff --git a/examples/servers/simple-streamablehttp/pyproject.toml b/examples/servers/simple-streamablehttp/pyproject.toml deleted file mode 100644 index f0404fb7dd..0000000000 --- a/examples/servers/simple-streamablehttp/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/simple-task-interactive/README.md b/examples/servers/simple-task-interactive/README.md deleted file mode 100644 index b8f384cb48..0000000000 --- a/examples/servers/simple-task-interactive/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Simple Interactive Task Server - -A minimal MCP server demonstrating interactive tasks with elicitation and sampling. - -## Running - -```bash -cd examples/servers/simple-task-interactive -uv run mcp-simple-task-interactive -``` - -The server starts on `http://localhost:8000/mcp` by default. Use `--port` to change. - -## What it does - -This server exposes two tools: - -### `confirm_delete` (demonstrates elicitation) - -Asks the user for confirmation before "deleting" a file. - -- Uses `task.elicit()` to request user input -- Shows the elicitation flow: task -> input_required -> response -> complete - -### `write_haiku` (demonstrates sampling) - -Asks the LLM to write a haiku about a topic. - -- Uses `task.create_message()` to request LLM completion -- Shows the sampling flow: task -> input_required -> response -> complete - -## Usage with the client - -In one terminal, start the server: - -```bash -cd examples/servers/simple-task-interactive -uv run mcp-simple-task-interactive -``` - -In another terminal, run the interactive client: - -```bash -cd examples/clients/simple-task-interactive-client -uv run mcp-simple-task-interactive-client -``` - -## Expected server output - -When a client connects and calls the tools, you'll see: - -```text -Starting server on http://localhost:8000/mcp - -[Server] confirm_delete called for 'important.txt' -[Server] Task created: -[Server] Sending elicitation request to client... -[Server] Received elicitation response: action=accept, content={'confirm': True} -[Server] Completing task with result: Deleted 'important.txt' - -[Server] write_haiku called for topic 'autumn leaves' -[Server] Task created: -[Server] Sending sampling request to client... -[Server] Received sampling response: Cherry blossoms fall -Softly on the quiet pon... -[Server] Completing task with haiku -``` - -## Key concepts - -1. **ServerTaskContext**: Provides `elicit()` and `create_message()` for user interaction -2. **run_task()**: Spawns background work, auto-completes/fails, returns immediately -3. **TaskResultHandler**: Delivers queued messages and routes responses -4. **Response routing**: Responses are routed back to waiting resolvers diff --git a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/__init__.py b/examples/servers/simple-task-interactive/mcp_simple_task_interactive/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/__main__.py b/examples/servers/simple-task-interactive/mcp_simple_task_interactive/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py b/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py deleted file mode 100644 index 4d35ca8094..0000000000 --- a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Simple interactive task server demonstrating elicitation and sampling. - -This example shows the simplified task API where: -- server.experimental.enable_tasks() sets up all infrastructure -- ctx.experimental.run_task() handles task lifecycle automatically -- ServerTaskContext.elicit() and ServerTaskContext.create_message() queue requests properly -""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from typing import Any - -import click -import mcp.types as types -import uvicorn -from mcp.server.experimental.task_context import ServerTaskContext -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.applications import Starlette -from starlette.routing import Mount - -server = Server("simple-task-interactive") - -# Enable task support - this auto-registers all handlers -server.experimental.enable_tasks() - - -@server.list_tools() -async def list_tools() -> list[types.Tool]: - return [ - types.Tool( - name="confirm_delete", - description="Asks for confirmation before deleting (demonstrates elicitation)", - inputSchema={ - "type": "object", - "properties": {"filename": {"type": "string"}}, - }, - execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED), - ), - types.Tool( - name="write_haiku", - description="Asks LLM to write a haiku (demonstrates sampling)", - inputSchema={"type": "object", "properties": {"topic": {"type": "string"}}}, - execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED), - ), - ] - - -async def handle_confirm_delete(arguments: dict[str, Any]) -> types.CreateTaskResult: - """Handle the confirm_delete tool - demonstrates elicitation.""" - ctx = server.request_context - ctx.experimental.validate_task_mode(types.TASK_REQUIRED) - - filename = arguments.get("filename", "unknown.txt") - print(f"\n[Server] confirm_delete called for '{filename}'") - - async def work(task: ServerTaskContext) -> types.CallToolResult: - print(f"[Server] Task {task.task_id} starting elicitation...") - - result = await task.elicit( - message=f"Are you sure you want to delete '{filename}'?", - requestedSchema={ - "type": "object", - "properties": {"confirm": {"type": "boolean"}}, - "required": ["confirm"], - }, - ) - - print(f"[Server] Received elicitation response: action={result.action}, content={result.content}") - - if result.action == "accept" and result.content: - confirmed = result.content.get("confirm", False) - text = f"Deleted '{filename}'" if confirmed else "Deletion cancelled" - else: - text = "Deletion cancelled" - - print(f"[Server] Completing task with result: {text}") - return types.CallToolResult(content=[types.TextContent(type="text", text=text)]) - - return await ctx.experimental.run_task(work) - - -async def handle_write_haiku(arguments: dict[str, Any]) -> types.CreateTaskResult: - """Handle the write_haiku tool - demonstrates sampling.""" - ctx = server.request_context - ctx.experimental.validate_task_mode(types.TASK_REQUIRED) - - topic = arguments.get("topic", "nature") - print(f"\n[Server] write_haiku called for topic '{topic}'") - - async def work(task: ServerTaskContext) -> types.CallToolResult: - print(f"[Server] Task {task.task_id} starting sampling...") - - result = await task.create_message( - messages=[ - types.SamplingMessage( - role="user", - content=types.TextContent(type="text", text=f"Write a haiku about {topic}"), - ) - ], - max_tokens=50, - ) - - haiku = "No response" - if isinstance(result.content, types.TextContent): - haiku = result.content.text - - print(f"[Server] Received sampling response: {haiku[:50]}...") - return types.CallToolResult(content=[types.TextContent(type="text", text=f"Haiku:\n{haiku}")]) - - return await ctx.experimental.run_task(work) - - -@server.call_tool() -async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult: - """Dispatch tool calls to their handlers.""" - if name == "confirm_delete": - return await handle_confirm_delete(arguments) - elif name == "write_haiku": - return await handle_write_haiku(arguments) - else: - return types.CallToolResult( - content=[types.TextContent(type="text", text=f"Unknown tool: {name}")], - isError=True, - ) - - -def create_app(session_manager: StreamableHTTPSessionManager) -> Starlette: - @asynccontextmanager - async def app_lifespan(app: Starlette) -> AsyncIterator[None]: - async with session_manager.run(): - yield - - return Starlette( - routes=[Mount("/mcp", app=session_manager.handle_request)], - lifespan=app_lifespan, - ) - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on") -def main(port: int) -> int: - session_manager = StreamableHTTPSessionManager(app=server) - starlette_app = create_app(session_manager) - print(f"Starting server on http://localhost:{port}/mcp") - uvicorn.run(starlette_app, host="127.0.0.1", port=port) - return 0 diff --git a/examples/servers/simple-task-interactive/pyproject.toml b/examples/servers/simple-task-interactive/pyproject.toml deleted file mode 100644 index 492345ff52..0000000000 --- a/examples/servers/simple-task-interactive/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "mcp-simple-task-interactive" -version = "0.1.0" -description = "A simple MCP server demonstrating interactive tasks (elicitation & sampling)" -readme = "README.md" -requires-python = ">=3.10" -authors = [{ name = "Anthropic, PBC." }] -keywords = ["mcp", "llm", "tasks", "elicitation", "sampling"] -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.0", "mcp", "starlette", "uvicorn"] - -[project.scripts] -mcp-simple-task-interactive = "mcp_simple_task_interactive.server:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["mcp_simple_task_interactive"] - -[tool.pyright] -include = ["mcp_simple_task_interactive"] -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", "ruff>=0.6.9"] diff --git a/examples/servers/simple-task/README.md b/examples/servers/simple-task/README.md deleted file mode 100644 index 6914e0414f..0000000000 --- a/examples/servers/simple-task/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Simple Task Server - -A minimal MCP server demonstrating the experimental tasks feature over streamable HTTP. - -## Running - -```bash -cd examples/servers/simple-task -uv run mcp-simple-task -``` - -The server starts on `http://localhost:8000/mcp` by default. Use `--port` to change. - -## What it does - -This server exposes a single tool `long_running_task` that: - -1. Must be called as a task (with `task` metadata in the request) -2. Takes ~3 seconds to complete -3. Sends status updates during execution -4. Returns a result when complete - -## Usage with the client - -In one terminal, start the server: - -```bash -cd examples/servers/simple-task -uv run mcp-simple-task -``` - -In another terminal, run the client: - -```bash -cd examples/clients/simple-task-client -uv run mcp-simple-task-client -``` diff --git a/examples/servers/simple-task/mcp_simple_task/__init__.py b/examples/servers/simple-task/mcp_simple_task/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-task/mcp_simple_task/__main__.py b/examples/servers/simple-task/mcp_simple_task/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-task/mcp_simple_task/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-task/mcp_simple_task/server.py b/examples/servers/simple-task/mcp_simple_task/server.py deleted file mode 100644 index d0681b8423..0000000000 --- a/examples/servers/simple-task/mcp_simple_task/server.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Simple task server demonstrating MCP tasks over streamable HTTP.""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from typing import Any - -import anyio -import click -import mcp.types as types -import uvicorn -from mcp.server.experimental.task_context import ServerTaskContext -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.applications import Starlette -from starlette.routing import Mount - -server = Server("simple-task-server") - -# One-line setup: auto-registers get_task, get_task_result, list_tasks, cancel_task -server.experimental.enable_tasks() - - -@server.list_tools() -async def list_tools() -> list[types.Tool]: - return [ - types.Tool( - name="long_running_task", - description="A task that takes a few seconds to complete with status updates", - inputSchema={"type": "object", "properties": {}}, - execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED), - ) - ] - - -async def handle_long_running_task(arguments: dict[str, Any]) -> types.CreateTaskResult: - """Handle the long_running_task tool - demonstrates status updates.""" - ctx = server.request_context - ctx.experimental.validate_task_mode(types.TASK_REQUIRED) - - async def work(task: ServerTaskContext) -> types.CallToolResult: - await task.update_status("Starting work...") - await anyio.sleep(1) - - await task.update_status("Processing step 1...") - await anyio.sleep(1) - - await task.update_status("Processing step 2...") - await anyio.sleep(1) - - return types.CallToolResult(content=[types.TextContent(type="text", text="Task completed!")]) - - return await ctx.experimental.run_task(work) - - -@server.call_tool() -async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult: - """Dispatch tool calls to their handlers.""" - if name == "long_running_task": - return await handle_long_running_task(arguments) - else: - return types.CallToolResult( - content=[types.TextContent(type="text", text=f"Unknown tool: {name}")], - isError=True, - ) - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on") -def main(port: int) -> int: - session_manager = StreamableHTTPSessionManager(app=server) - - @asynccontextmanager - async def app_lifespan(app: Starlette) -> AsyncIterator[None]: - async with session_manager.run(): - yield - - starlette_app = Starlette( - routes=[Mount("/mcp", app=session_manager.handle_request)], - lifespan=app_lifespan, - ) - - print(f"Starting server on http://localhost:{port}/mcp") - uvicorn.run(starlette_app, host="127.0.0.1", port=port) - return 0 diff --git a/examples/servers/simple-task/pyproject.toml b/examples/servers/simple-task/pyproject.toml deleted file mode 100644 index a8fba8bdc1..0000000000 --- a/examples/servers/simple-task/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "mcp-simple-task" -version = "0.1.0" -description = "A simple MCP server demonstrating tasks" -readme = "README.md" -requires-python = ">=3.10" -authors = [{ name = "Anthropic, PBC." }] -keywords = ["mcp", "llm", "tasks"] -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.0", "mcp", "starlette", "uvicorn"] - -[project.scripts] -mcp-simple-task = "mcp_simple_task.server:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["mcp_simple_task"] - -[tool.pyright] -include = ["mcp_simple_task"] -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", "ruff>=0.6.9"] diff --git a/examples/servers/simple-tool/.python-version b/examples/servers/simple-tool/.python-version deleted file mode 100644 index c8cfe39591..0000000000 --- a/examples/servers/simple-tool/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/examples/servers/simple-tool/README.md b/examples/servers/simple-tool/README.md deleted file mode 100644 index 06020b4b0e..0000000000 --- a/examples/servers/simple-tool/README.md +++ /dev/null @@ -1,48 +0,0 @@ - -A simple MCP server that exposes a website fetching tool. - -## Usage - -Start the server using either stdio (default) or SSE transport: - -```bash -# Using stdio transport (default) -uv run mcp-simple-tool - -# Using SSE transport on custom port -uv run mcp-simple-tool --transport sse --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()) - -``` diff --git a/examples/servers/simple-tool/mcp_simple_tool/__init__.py b/examples/servers/simple-tool/mcp_simple_tool/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/servers/simple-tool/mcp_simple_tool/__main__.py b/examples/servers/simple-tool/mcp_simple_tool/__main__.py deleted file mode 100644 index e7ef16530b..0000000000 --- a/examples/servers/simple-tool/mcp_simple_tool/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - -from .server import main - -sys.exit(main()) # type: ignore[call-arg] diff --git a/examples/servers/simple-tool/mcp_simple_tool/server.py b/examples/servers/simple-tool/mcp_simple_tool/server.py deleted file mode 100644 index 5b2b7d068d..0000000000 --- a/examples/servers/simple-tool/mcp_simple_tool/server.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.shared._httpx_utils import create_mcp_http_client -from starlette.requests import Request - - -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)] - - -@click.command() -@click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option( - "--transport", - type=click.Choice(["stdio", "sse"]), - default="stdio", - help="Transport type", -) -def main(port: int, transport: str) -> int: - app = Server("mcp-website-fetcher") - - @app.call_tool() - async def fetch_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - if name != "fetch": - raise ValueError(f"Unknown tool: {name}") - if "url" not in arguments: - raise ValueError("Missing required argument 'url'") - return await fetch_website(arguments["url"]) - - @app.list_tools() - async def list_tools() -> list[types.Tool]: - return [ - types.Tool( - name="fetch", - title="Website Fetcher", - description="Fetches a website and returns its content", - inputSchema={ - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "description": "URL to fetch", - } - }, - }, - ) - ] - - if transport == "sse": - from mcp.server.sse import SseServerTransport - from starlette.applications import Starlette - from starlette.responses import Response - from starlette.routing import Mount, Route - - sse = SseServerTransport("/messages/") - - async def handle_sse(request: Request): - async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage] - await app.run(streams[0], streams[1], app.create_initialization_options()) - return Response() - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse, methods=["GET"]), - Mount("/messages/", app=sse.handle_post_message), - ], - ) - - import uvicorn - - uvicorn.run(starlette_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 diff --git a/examples/servers/simple-tool/pyproject.toml b/examples/servers/simple-tool/pyproject.toml deleted file mode 100644 index c3944f3146..0000000000 --- a/examples/servers/simple-tool/pyproject.toml +++ /dev/null @@ -1,47 +0,0 @@ -[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 = "Anthropic, PBC." }] -maintainers = [ - { name = "David Soria Parra", email = "davidsp@anthropic.com" }, - { name = "Justin Spahr-Summers", email = "justin@anthropic.com" }, -] -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"] diff --git a/examples/servers/sse-polling-demo/README.md b/examples/servers/sse-polling-demo/README.md deleted file mode 100644 index e9d4446e1f..0000000000 --- a/examples/servers/sse-polling-demo/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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 -``` diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__init__.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__init__.py deleted file mode 100644 index 46af2fdeed..0000000000 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""SSE Polling Demo Server - demonstrates close_sse_stream for long-running tasks.""" diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__main__.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__main__.py deleted file mode 100644 index 23cfc85e11..0000000000 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Entry point for the SSE Polling Demo server.""" - -from .server import main - -if __name__ == "__main__": - main() diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/event_store.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/event_store.py deleted file mode 100644 index 75f98cdd49..0000000000 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/event_store.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -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 diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py deleted file mode 100644 index e4bdcaa396..0000000000 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -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 contextlib -import logging -from collections.abc import AsyncIterator -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.applications import Starlette -from starlette.routing import Mount -from starlette.types import Receive, Scope, Send - -from .event_store import InMemoryEventStore - -logger = logging.getLogger(__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", - ) - - # Create the lowlevel server - app = Server("sse-polling-demo") - - @app.call_tool() - async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - """Handle tool calls.""" - ctx = app.request_context - - if name == "process_batch": - items = arguments.get("items", 10) - checkpoint_every = arguments.get("checkpoint_every", 3) - - if items < 1 or items > 100: - return [types.TextContent(type="text", text="Error: items must be between 1 and 100")] - if checkpoint_every < 1 or checkpoint_every > 20: - return [types.TextContent(type="text", text="Error: checkpoint_every must be between 1 and 20")] - - await ctx.session.send_log_message( - 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( - 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( - 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.TextContent( - type="text", - text=f"Successfully processed {items} items with checkpoints every {checkpoint_every} items", - ) - ] - - return [types.TextContent(type="text", text=f"Unknown tool: {name}")] - - @app.list_tools() - async def list_tools() -> list[types.Tool]: - """List available tools.""" - return [ - types.Tool( - name="process_batch", - description=( - "Process a batch of items with periodic checkpoints. " - "Demonstrates SSE polling where server closes stream periodically." - ), - inputSchema={ - "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, - }, - }, - }, - ) - ] - - # Create event store for resumability - event_store = InMemoryEventStore() - - # Create session manager with event store and retry interval - session_manager = StreamableHTTPSessionManager( - app=app, - event_store=event_store, - retry_interval=retry_interval, - ) - - async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None: - await session_manager.handle_request(scope, receive, send) - - @contextlib.asynccontextmanager - async def lifespan(starlette_app: Starlette) -> AsyncIterator[None]: - async with session_manager.run(): - logger.info(f"SSE Polling Demo server started on port {port}") - logger.info("Try: POST /mcp with tools/call for 'process_batch'") - yield - logger.info("Server shutting down...") - - starlette_app = Starlette( - debug=True, - routes=[ - Mount("/mcp", app=handle_streamable_http), - ], - lifespan=lifespan, - ) - - import uvicorn - - uvicorn.run(starlette_app, host="127.0.0.1", port=port) - return 0 - - -if __name__ == "__main__": - main() diff --git a/examples/servers/sse-polling-demo/pyproject.toml b/examples/servers/sse-polling-demo/pyproject.toml deleted file mode 100644 index f7ad89217c..0000000000 --- a/examples/servers/sse-polling-demo/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[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 = "Anthropic, PBC." }] -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"] diff --git a/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__init__.py b/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__init__.py deleted file mode 100644 index c65905675b..0000000000 --- a/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Example of structured output with low-level MCP server.""" diff --git a/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__main__.py b/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__main__.py deleted file mode 100644 index 7f102ff8b5..0000000000 --- a/examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__main__.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/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, with automatic validation against output -schemas. -""" - -import asyncio -from datetime import datetime -from typing import Any - -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -# Create low-level server instance -server = Server("structured-output-lowlevel-example") - - -@server.list_tools() -async def list_tools() -> list[types.Tool]: - """List available tools with their schemas.""" - return [ - types.Tool( - name="get_weather", - description="Get weather information (simulated)", - inputSchema={ - "type": "object", - "properties": {"city": {"type": "string", "description": "City name"}}, - "required": ["city"], - }, - outputSchema={ - "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"], - }, - ), - ] - - -@server.call_tool() -async def call_tool(name: str, arguments: dict[str, Any]) -> Any: - """ - Handle tool call with structured output. - """ - - if name == "get_weather": - # city = arguments["city"] # Would be used with real weather API - - # Simulate weather data (in production, call a real weather API) - import random - - 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 structured data only - # The low-level server will serialize this to JSON content automatically - return weather_data - - else: - raise ValueError(f"Unknown tool: {name}") - - -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, - InitializationOptions( - server_name="structured-output-lowlevel-example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - asyncio.run(run()) diff --git a/examples/servers/structured-output-lowlevel/pyproject.toml b/examples/servers/structured-output-lowlevel/pyproject.toml deleted file mode 100644 index 554efc6145..0000000000 --- a/examples/servers/structured-output-lowlevel/pyproject.toml +++ /dev/null @@ -1,6 +0,0 @@ -[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"]