MCP Server Basics
8 examples to get you started with Building MCP Servers - 5 basic and 3 intermediate.
Prerequisites
- Python 3.10+ and the official MCP Python SDK:
pip install mcp. - Or Node.js 18+ and the official MCP TypeScript SDK:
npm install @modelcontextprotocol/sdk. - A client to talk to your server. Claude Desktop, Claude Code, or a custom client built on the Agent SDK all work for local testing over stdio.
- No network setup is required for the examples below. They all run over stdio, the standard local transport for development.
Basic Examples
1. Minimal Server Skeleton
The smallest possible MCP server: create an instance and run it over stdio.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo-server")
if __name__ == "__main__":
mcp.run(transport="stdio")FastMCPis the high-level server class in the Python SDK; it handles the connection and capability negotiation stages for you.- The string
"demo-server"is the server's name, which a client may display when listing connected servers. mcp.run(transport="stdio")starts the event loop and blocks, reading requests from stdin and writing responses to stdout.- This server has no tools yet, a client can connect and negotiate, but there's nothing to call.
2. Registering a Tool
Add a callable tool using the @mcp.tool() decorator.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str) -> str:
"""Return a short weather summary for a city."""
return f"It is sunny in {city}."
if __name__ == "__main__":
mcp.run(transport="stdio")- The decorator registers
get_weatherin the server's capability manifest, so it appears during negotiation. - The type hint (
city: str) becomes the tool's input schema, the client uses this to know what arguments to send. - The docstring becomes the tool's description, shown to the client and to Claude when deciding whether to call it.
- Without the decorator, this would just be a Python function, invisible to any connected client.
3. Registering a Resource
Expose read-only content at a URI using @mcp.resource().
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("config-server")
@mcp.resource("config://settings")
def get_settings() -> str:
return "theme=dark\ntimezone=UTC"- Resources are read-only, a client fetches them by URI, they don't take arguments the way tools do.
- The URI scheme (
config://) is arbitrary, choose one that describes what the resource represents. - Use resources for data a client should be able to read directly, and tools for actions that do work or have side effects.
- A common mistake is exposing read-only data as a tool when a resource is the better fit.
4. Registering a Prompt
Provide a reusable prompt template with @mcp.prompt().
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("prompt-server")
@mcp.prompt()
def summarize_request(topic: str) -> str:
return f"Please summarize the latest updates about {topic} in three bullet points."- Prompts are templates the client can fetch and fill in, not data and not an action.
- Arguments to a prompt function work the same way as tool arguments, they define the template's input schema.
- Prompts are useful for standardizing common requests across every client that connects to this server.
- A server can mix tools, resources, and prompts freely, they are all registered the same way, with a different decorator.
5. Running the Server Locally
Start the server as a subprocess a client can spawn.
python server.py- Over stdio, the client is responsible for starting this process, you rarely run it standalone in production use.
- During local development, many clients (like Claude Desktop) are configured to launch this command automatically when needed.
- If the process exits immediately, check for an exception at import time. Errors during startup often look like a client-side "server not responding" instead of a clear Python traceback.
- stdio is the right transport for local, single-machine development. Multiple remote clients call for HTTP/SSE instead.
Related: stdio vs HTTP/SSE MCP Deployment Comparison - when to move beyond stdio
Intermediate Examples
6. A Server with Tool, Resource, and Prompt Together
Combine all three capability types in one server, mirroring a small real deployment.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tickets-server")
TICKETS = {"T-1": "Open: printer offline", "T-2": "Closed: VPN issue"}
@mcp.tool()
def get_ticket(ticket_id: str) -> str:
"""Look up a support ticket by ID."""
return TICKETS.get(ticket_id, "Ticket not found.")
@mcp.resource("tickets://open")
def list_open_tickets() -> str:
return "\n".join(k for k, v in TICKETS.items() if v.startswith("Open"))
@mcp.prompt()
def triage_prompt(ticket_id: str) -> str:
return f"Review ticket {ticket_id} and suggest a next action."
if __name__ == "__main__":
mcp.run(transport="stdio")get_ticketperforms a lookup, so it's a tool.list_open_ticketsis read-only data, so it's a resource.- All three capabilities are registered independently and appear together in the manifest a client sees during negotiation.
- Real servers rarely expose only one capability type, most useful integrations mix actions and readable state.
- This in-memory
TICKETSdict is fine for a demo, a production server would back this with a real datastore.
7. Minimal TypeScript Equivalent
The same tool-registration pattern using the official TypeScript SDK.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "weather-server", version: "1.0.0" });
server.tool(
"get_weather",
{ city: z.string() },
async ({ city }) => ({
content: [{ type: "text", text: `It is sunny in ${city}.` }],
})
);
await server.connect(new StdioServerTransport());McpServerplays the same role asFastMCP, it manages connection and negotiation for you.zodschemas define the tool's input shape, the TypeScript equivalent of Python's type hints.server.connect(new StdioServerTransport())is the TypeScript SDK's version ofmcp.run(transport="stdio").- Both SDKs implement the same protocol, so a client can't tell whether a server was written in Python or TypeScript.
Related: Building an MCP Server with the TypeScript SDK - full walkthrough of the TypeScript SDK
8. Verifying the Lifecycle Locally
A quick manual check that your server negotiates correctly before wiring it into a real client.
npx @modelcontextprotocol/inspector python server.py- The MCP Inspector is a general-purpose tool for connecting to any local server and manually exercising its tools, resources, and prompts.
- It performs the same connect and negotiate steps a real client would, then lets you call tools interactively.
- Running this before pointing a real client at your server catches registration mistakes early, a tool missing from the manifest is obvious here.
- This is a development aid only, it isn't part of the request lifecycle itself, just a way to observe it.
Related: How MCP Servers Handle Requests - the lifecycle this inspection is checking | Testing MCP Servers Before Deployment - automated tests for the same handlers
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the current Model Context Protocol Python/TypeScript SDKs. Model names, SDK versions, and the MCP spec move quickly - verify current specifics at platform.claude.com/docs and modelcontextprotocol.io before relying on them.