Skip to main content
Glama
ARCHITECTURE.md15.2 kB
# MCP Demo Server - Architecture Documentation This document provides a detailed explanation of the MCP Demo Server architecture, design decisions, and code organization. ## Table of Contents - [Overview](#overview) - [Architecture Diagram](#architecture-diagram) - [Core Components](#core-components) - [Code Organization](#code-organization) - [Design Patterns](#design-patterns) - [Data Flow](#data-flow) - [Error Handling Strategy](#error-handling-strategy) - [Extension Points](#extension-points) ## Overview The MCP Demo Server is built using the Model Context Protocol (MCP) SDK for Python. It follows a clean, modular architecture that separates concerns and makes it easy to extend with new tools, resources, and prompts. ### Key Design Principles 1. **Type Safety**: Full type hints with Pydantic validation 2. **Async First**: All operations use async/await patterns 3. **Single Responsibility**: Each component has one clear purpose 4. **Error Resilience**: Comprehensive error handling at all levels 5. **Testability**: Easy to test with dependency injection 6. **Documentation**: Self-documenting code with clear docstrings ## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────┐ │ MCP Client │ │ (Claude Desktop, Custom) │ └─────────────────────┬───────────────────────────────────────┘ │ JSON-RPC over stdio │ ┌─────────────────────▼───────────────────────────────────────┐ │ MCP Server (stdio) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ MCPDemoServer Class │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │ │ │ │ Tools │ │ Resources │ │ Prompts │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ - Calculator │ │ - Config │ │ - Review │ │ │ │ │ │ - Files │ │ - SysInfo │ │ - Docs │ │ │ │ │ │ - Weather │ │ - Docs │ │ - Debug │ │ │ │ │ │ - Timestamp │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ Handler Registration │ │ │ │ │ │ - list_tools / call_tool │ │ │ │ │ │ - list_resources / read_resource │ │ │ │ │ │ - list_prompts / get_prompt │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Logging & Error Handling │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## Core Components ### 1. MCPDemoServer Class The main server class that orchestrates all functionality. ```python class MCPDemoServer: def __init__(self, name: str): """Initialize server with name and setup handlers.""" self.server = Server(name) self._setup_handlers() ``` **Responsibilities:** - Initialize MCP server instance - Register all request handlers - Coordinate tool execution - Manage resources and prompts - Handle errors and logging ### 2. Tools System Tools are executable functions that the LLM can call. **Input Validation:** ```python class CalculatorInput(BaseModel): operation: str = Field(description="...") a: float = Field(description="...") b: float = Field(description="...") ``` **Tool Registration:** ```python async def list_tools(self) -> list[Tool]: return [ Tool( name="calculator", description="Perform math operations", inputSchema=CalculatorInput.model_json_schema(), ) ] ``` **Tool Execution:** ```python async def call_tool(self, name: str, arguments: Any) -> list[TextContent]: if name == "calculator": return await self._calculator_tool(arguments) ``` ### 3. Resources System Resources provide read-only data to the LLM. **Resource Definition:** ```python async def list_resources(self) -> list[Resource]: return [ Resource( uri=AnyUrl("config://server/settings"), name="Server Configuration", description="Current server configuration", mimeType="application/json", ) ] ``` **Resource Reading:** ```python async def read_resource(self, uri: AnyUrl) -> str: if str(uri) == "config://server/settings": return json.dumps(config_data, indent=2) ``` ### 4. Prompts System Prompts provide reusable templates for common tasks. **Prompt Definition:** ```python async def list_prompts(self) -> list[Prompt]: return [ Prompt( name="code-review", description="Generate code review template", arguments=[...], ) ] ``` **Prompt Retrieval:** ```python async def get_prompt(self, name: str, arguments: dict) -> GetPromptResult: if name == "code-review": language = arguments.get("language") # Generate and return prompt return GetPromptResult(messages=[...]) ``` ## Code Organization ``` src/mcp_demo/ ├── __init__.py # Package metadata and exports └── server.py # Main server implementation Structure within server.py: ├── Imports ├── Logging Configuration ├── Input Models (Pydantic) │ ├── CalculatorInput │ ├── FileOperationInput │ └── WeatherInput ├── MCPDemoServer Class │ ├── __init__ │ ├── _setup_handlers │ ├── list_tools │ ├── call_tool │ │ ├── _calculator_tool │ │ ├── _file_operations_tool │ │ ├── _weather_tool │ │ └── _timestamp_tool │ ├── list_resources │ ├── read_resource │ ├── list_prompts │ └── get_prompt └── main() / entry point ``` ## Design Patterns ### 1. Registry Pattern Tools, resources, and prompts are registered with the MCP server: ```python def _setup_handlers(self): self.server.list_tools()(self.list_tools) self.server.call_tool()(self.call_tool) # ... more registrations ``` ### 2. Dispatcher Pattern Tool calls are dispatched to specific handlers: ```python async def call_tool(self, name: str, arguments: Any): if name == "calculator": return await self._calculator_tool(arguments) elif name == "file_operations": return await self._file_operations_tool(arguments) # ... more dispatches ``` ### 3. Template Method Pattern Prompts use template method pattern for generation: ```python async def get_prompt(self, name: str, arguments: dict): if name == "code-review": # Fill in template with arguments message = generate_code_review_template(arguments) return GetPromptResult(messages=[...]) ``` ### 4. Strategy Pattern Different operations (calculator, file ops) are encapsulated: ```python operations = { "add": lambda a, b: a + b, "subtract": lambda a, b: a - b, # ... } result = operations[operation](a, b) ``` ## Data Flow ### Tool Execution Flow ``` 1. Client sends tool call request └─> {"name": "calculator", "arguments": {...}} 2. Server receives via stdio transport └─> Parsed as JSON-RPC message 3. call_tool() handler invoked └─> Validates tool name └─> Dispatches to specific tool method 4. Tool method executes └─> Validates input with Pydantic └─> Performs operation └─> Catches and handles errors └─> Returns TextContent result 5. Result serialized and sent to client └─> [TextContent(type="text", text="Result: ...")] ``` ### Resource Reading Flow ``` 1. Client requests resource └─> {"uri": "config://server/settings"} 2. read_resource() handler invoked └─> Checks URI against known resources └─> Generates or retrieves data └─> Returns as string (usually JSON) 3. Data returned to client ``` ### Prompt Generation Flow ``` 1. Client requests prompt └─> {"name": "code-review", "arguments": {...}} 2. get_prompt() handler invoked └─> Validates prompt name └─> Extracts arguments └─> Generates prompt from template └─> Returns GetPromptResult 3. Prompt sent to client as messages ``` ## Error Handling Strategy ### Layered Error Handling **Level 1: Input Validation** ```python try: calc_input = CalculatorInput(**arguments) except ValidationError as e: # Pydantic validation error return error_response(str(e)) ``` **Level 2: Business Logic** ```python if operation not in operations: raise ValueError(f"Invalid operation: {operation}") result = operations[operation](a, b) if result is None: return [TextContent(type="text", text="Error: Division by zero")] ``` **Level 3: Top-Level Handler** ```python try: return await self._calculator_tool(arguments) except Exception as e: logger.error(f"Error: {e}", exc_info=True) return [TextContent(type="text", text=f"Error: {str(e)}")] ``` ### Error Response Format All errors return consistent format: ```python [TextContent(type="text", text="Error: <description>")] ``` ### Logging Strategy ```python # Initialization logger.info(f"Initialized {name}") # Request handling logger.info(f"Calling tool: {name} with arguments: {arguments}") # Errors logger.error(f"Error executing tool {name}: {e}", exc_info=True) ``` Logs go to: - File: `mcp_server.log` - stderr: For client visibility ## Extension Points ### Adding a New Tool **Step 1: Define Input Model** ```python class MyToolInput(BaseModel): param: str = Field(description="Parameter description") ``` **Step 2: Implement Tool Method** ```python async def _my_tool(self, arguments: dict) -> list[TextContent]: tool_input = MyToolInput(**arguments) # Your logic here return [TextContent(type="text", text="Result")] ``` **Step 3: Register in list_tools()** ```python Tool( name="my_tool", description="What it does", inputSchema=MyToolInput.model_json_schema(), ) ``` **Step 4: Add to Dispatcher** ```python elif name == "my_tool": return await self._my_tool(arguments) ``` ### Adding a New Resource **Step 1: Define in list_resources()** ```python Resource( uri=AnyUrl("my://resource"), name="My Resource", description="Description", mimeType="application/json", ) ``` **Step 2: Implement Handler** ```python elif uri_str == "my://resource": data = generate_my_resource_data() return json.dumps(data, indent=2) ``` ### Adding a New Prompt **Step 1: Define in list_prompts()** ```python Prompt( name="my-prompt", description="What it does", arguments=[...], ) ``` **Step 2: Implement Handler** ```python elif name == "my-prompt": message = generate_prompt_template(arguments) return GetPromptResult(messages=[...]) ``` ## Testing Strategy ### Unit Test Structure ```python @pytest.fixture async def server(): """Create test server instance.""" return MCPDemoServer(name="test-server") @pytest.mark.asyncio async def test_calculator_add(server): """Test calculator addition.""" result = await server.call_tool( "calculator", {"operation": "add", "a": 10, "b": 20} ) assert "30" in result[0].text ``` ### Test Coverage - **Tools**: Test each operation, error cases, edge cases - **Resources**: Test each resource URI, error cases - **Prompts**: Test with various arguments - **Validation**: Test input validation with Pydantic - **Error Handling**: Test error paths ## Performance Considerations ### Async Operations All operations are async to prevent blocking: ```python async def call_tool(...) # Non-blocking async def read_resource(...) # Non-blocking ``` ### Resource Caching Resources could be cached for performance: ```python @lru_cache(maxsize=128) def get_system_info(): # Expensive operation return system_info ``` ### Logging Optimization Logging is async-friendly and uses appropriate levels: - `DEBUG`: Detailed information - `INFO`: General operations - `ERROR`: Errors with stack traces ## Security Considerations ### File Operations - Path validation to prevent directory traversal - Safe path handling with `pathlib.Path` - Existence checks before operations ### Input Validation - All inputs validated with Pydantic - Type checking enforced - Bounds checking where applicable ### Error Messages - Don't expose sensitive information - Generic error messages for clients - Detailed errors in logs only ## Future Enhancements Potential areas for extension: 1. **Authentication**: Add API key or OAuth support 2. **Rate Limiting**: Prevent abuse 3. **Metrics**: Collect usage statistics 4. **Database**: Add persistent storage 5. **Configuration**: External config file support 6. **Multi-transport**: Support HTTP, WebSocket 7. **Plugin System**: Dynamic tool loading 8. **Caching**: Response caching layer --- This architecture provides a solid foundation for building production MCP servers while remaining simple enough to understand and extend.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joohnnie/mcp-agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server