Learn/MCP/Building an MCP Server
MCP

Building an MCP Server

Step-by-step guide to building your own MCP server and exposing tools to AI models.

Prerequisites

This guide uses the official Python MCP SDK. You'll need Python 3.10+, pip, and Claude Desktop installed for local testing. The TypeScript SDK follows the same concepts with different syntax — the official docs at [modelcontextprotocol.io](https://modelcontextprotocol.io) cover both.

You can write MCP servers in any language that can communicate over stdio or HTTP. The official SDKs handle the protocol details; you focus on the tool logic.

Step 1 — Install the SDK

bash pip install mcp

For a project, use a virtual environment:

bash python -m venv .venv source .venv/bin/activate pip install mcp

Step 2 — Create the Server File

Create a file called calculator_server.py. This will be a simple MCP server exposing two arithmetic tools.

```python from mcp.server import Server from mcp.server.stdio import stdio_server from mcp import types

# Initialize the server with a name app = Server("calculator")

# Declare the tools this server exposes @app.list_tools() async def list_tools() -> list[types.Tool]: return [ types.Tool( name="add", description="Add two numbers together", inputSchema={ "type": "object", "properties": { "a": {"type": "number", "description": "First number"}, "b": {"type": "number", "description": "Second number"}, }, "required": ["a", "b"], }, ), types.Tool( name="multiply", description="Multiply two numbers together", inputSchema={ "type": "object", "properties": { "a": {"type": "number", "description": "First number"}, "b": {"type": "number", "description": "Second number"}, }, "required": ["a", "b"], }, ), ]

# Handle tool execution @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: a = arguments["a"] b = arguments["b"]

if name == "add": result = a + b elif name == "multiply": result = a * b else: raise ValueError(f"Unknown tool: {name}")

return [types.TextContent(type="text", text=str(result))]

# Start the server using stdio transport if __name__ == "__main__": import asyncio asyncio.run(stdio_server(app)) ```

Step 3 — Understand the Message Flow

When Claude Desktop (or any MCP host) connects to your server, here's what happens:

  1. 1.Host launches your server as a subprocess (for stdio transport)
  2. 2.Host sends `tools/list` — your list_tools handler responds with the tool definitions
  3. 3.User asks Claude something that triggers tool use — e.g., "What's 47 times 83?"
  4. 4.Claude decides to call `multiply` with {"a": 47, "b": 83}
  5. 5.Host sends `tools/call` to your server with the tool name and arguments
  6. 6.Your `call_tool` handler runs the multiplication and returns the result
  7. 7.Claude receives the result and incorporates it into its response

The SDK handles all the JSON-RPC 2.0 serialization, transport management, and error handling. Your code only needs to implement the two handlers.

Step 4 — Connect to Claude Desktop

Claude Desktop reads its MCP server configuration from a JSON file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server to the configuration:

json { "mcpServers": { "calculator": { "command": "/path/to/.venv/bin/python", "args": ["/path/to/calculator_server.py"] } } }

Replace the paths with absolute paths to your virtual environment's Python interpreter and your server file. Restart Claude Desktop after saving.

Step 5 — Test It

After restarting Claude Desktop, open a new conversation. You should see a tools indicator showing your server is connected. Ask Claude: "What is 144 divided by 12?" — note that our server only has add and multiply, so Claude will handle division itself. Try "What is 47 times 83?" and Claude should invoke the multiply tool and return 3901.

If the server doesn't connect, check Claude Desktop's logs: - macOS: ~/Library/Logs/Claude/mcp*.log

Going Further

Add error handling. Real tools should validate inputs and return meaningful error messages rather than raising Python exceptions.

Add a resource. Resources expose data the model can read. Add a @app.list_resources() handler and a @app.read_resource() handler to expose static or dynamic content.

Use environment variables for secrets. If your tool calls an external API, pass credentials via environment variables in the Claude Desktop config:

json { "mcpServers": { "my-api-tool": { "command": "python", "args": ["server.py"], "env": { "API_KEY": "your-key-here" } } } }

Consider SSE transport for remote servers. The stdio transport works for local development. For a server you want to host remotely and share across a team, switch to the SSE transport — the SDK supports both with minimal configuration changes.

The official documentation at [modelcontextprotocol.io](https://modelcontextprotocol.io) includes more advanced examples, the full SDK reference, and guides for TypeScript. The GitHub repository also contains reference server implementations for common integrations that you can use as starting points.

Have a follow-up question about this topic?

Ask AI