Building & Deploying MCP Basics
8 examples to get you started with a minimal MCP server - 5 basic and 3 intermediate.
You will install the Python MCP SDK, expose one tool over stdio, keep logs off stdout, list tools from a tiny client, and sketch the jump to HTTP.
Prerequisites
- Python 3.10+ (3.11+ recommended)
uvorpipfor installs- Optional: Node 18+ if you mirror steps in TypeScript later
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"
# or: uv add "mcp[cli]"Basic Examples
1. Create a FastMCP Server With One Tool
Start with a named server and a single health tool.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-mcp")
@mcp.tool()
def echo(message: str) -> str:
"""Echo a short message back to the client."""
return message
if __name__ == "__main__":
mcp.run(transport="stdio")- FastMCP builds the tool schema from the signature and docstring.
transport="stdio"is the local default for desktop hosts.- Save as
server.pyand keep the process free of accidentalprint()calls.
Related: Anatomy of an MCP Server: Handlers, Schemas, and Transport
2. Run the Server on stdio
Hosts spawn your process; you can also run it by hand to confirm it starts.
python server.py
# Process waits on stdin for JSON-RPC lines; Ctrl+C to stop.- No banner text on stdout - that would break the protocol.
- Prefer logging to stderr if you need visibility.
- Absolute paths in host config avoid cwd surprises.
3. Log Safely on stdio
Never write diagnostics to stdout when using stdio transport.
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
log = logging.getLogger("hello-mcp")
@mcp.tool()
def echo(message: str) -> str:
"""Echo a short message back to the client."""
log.info("echo called, len=%s", len(message))
return messageprint(..., file=sys.stderr)is also safe.- HTTP transports can use normal stdout logging later.
- Broken framing from stray stdout is the most common first-day failure.
4. Register the Server in a Host Config
Point Claude Desktop (or similar) at an absolute command.
{
"mcpServers": {
"hello-mcp": {
"command": "python",
"args": ["/ABSOLUTE/PATH/TO/server.py"]
}
}
}- Use the venv's
pythonpath if dependencies are not global. - Restart the host after config edits.
- If the host supports it, open the tool list and confirm
echoappears.
5. Add a Second Tool With Typed Args
Schemas get richer when parameters are constrained.
from typing import Literal
@mcp.tool()
def greet(name: str, style: Literal["short", "formal"] = "short") -> str:
"""Greet a person in a short or formal style."""
if style == "formal":
return f"Good day, {name}."
return f"Hi, {name}!"- Default values become optional fields for the model.
- Literals / enums reduce bad arguments.
- Keep tool names stable; renames break saved agent flows.
Intermediate Examples
6. List Tools With a Minimal Client Harness
Validate the server without a full desktop host.
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main() -> None:
params = StdioServerParameters(
command="python",
args=["/ABSOLUTE/PATH/TO/server.py"],
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools], file=__import__("sys").stderr)
asyncio.run(main())- The client owns the subprocess lifecycle.
initializemust succeed before list/call.- Print client-side output to stderr if this script could be confused with a server.
Related: Building Your First MCP Server in TypeScript or Python
7. Call a Tool and Read Content
Drive one happy-path invocation for CI.
result = await session.call_tool("echo", {"message": "pong"})
# result.content is a list of content blocks; text tools usually expose .text
for block in result.content:
if hasattr(block, "text"):
print(block.text, file=__import__("sys").stderr)- Assert exact tool names and sample outputs in tests.
- Treat
isErrorflags as failures when your SDK exposes them. - Keep CI timeouts tight so hung handlers fail the job.
8. Sketch the Path to Remote HTTP
Same tools, different transport, once you need a shared service.
# Development sketch - verify host/path/API at build for your SDK version
if __name__ == "__main__":
import os
mode = os.environ.get("MCP_TRANSPORT", "stdio")
if mode == "stdio":
mcp.run(transport="stdio")
else:
# e.g. mcp.run(transport="streamable-http") with host/port settings
mcp.run(transport="streamable-http")- Remote mode needs TLS, origin checks, and auth before production.
- Session headers and protocol version headers appear only on HTTP.
- Keep business tools identical across transports so local and remote stay aligned.
Related
Related: Anatomy of an MCP Server: Handlers, Schemas, and Transport
Related: Building Your First MCP Server in TypeScript or Python
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.