mcp-server-development
verifiededd995ba-e553-4c00-b4fc-25e31d4a77d0
Build and ship a Model Context Protocol (MCP) server that agents can call — tools, resources, prompts, stdio vs streamable-HTTP transports, lifecycle, and testing.
Metadata
Skill file
# Building an MCP Server
Use when you want an LLM agent/assistant to be able to call your own tools and
read your own data through the **Model Context Protocol (MCP)** — the open
standard for connecting models to tools and context. An MCP *server* exposes your
capabilities; MCP *clients* (Claude, IDEs, custom agents) discover and call them.
## What an MCP server exposes
Three kinds of primitives, and it's worth choosing deliberately (don't put
everything in tools):
- **Tools** — functions the model invokes to *do* something (act, mutate, call an
external API). Tool = action.
- **Resources** — data the model can read (a file, a DB row, a document). Resource = context.
- **Prompts** — reusable prompt templates (message sequences) that a client can
pull in. Prompt = recipe.
A common design mistake is exposing read-only data as tools. If a model should
*read* a doc, expose it as a resource; reserve tools for actions. Fewer, clearer
tools call far more reliably.
## Choose your transport
- **stdio** — server runs as a child process of the client, speaks JSON-RPC over
stdin/stdout. Simple, secure-by-construction (no network surface), ideal for
local/desktop and anything where the client controls the process. The default
for local dev.
- **Streamable HTTP** (superseding the older HTTP+SSE transport) — server runs as
an HTTP endpoint. Needed for remote/shared servers, web apps, multitenancy.
Comes with auth and CORS concerns (see the memory on MCP security).
Start with stdio. Add HTTP only when you genuinely need remote access.
## Build with FastMCP
The `mcp` Python SDK provides `FastMCP`, a high-level builder. A minimal server:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("MyTools", instructions="Use these tools to manage todos for the user.")
@mcp.tool()
def add_todo(title: str, priority: int = 2) -> str:
"""Add a todo. priority: 1=high, 2=normal, 3=low."""
# ...persist...
return f"Added '{title}'"
@mcp.resource("todos://all")
def get_all_todos() -> str:
"""Return all todos as text."""
# ...fetch...
return formatted_todos()
@mcp.prompt()
def todo_summary(scope: str = "all") -> str:
"""Build a prompt summarizing todos."""
return f"Summarize the {scope} todos the user has outstanding."
if __name__ == "__main__":
mcp.run() # stdio by default
# mcp.run(transport="streamable-http") # for remote
```
Run it with `python server.py` or `mcp run server.py`. For a remote server, the
same FastMCP object can be served under an existing ASGI app (FastAPI/Starlette)
so it shares routing, auth middleware, and deploys with the rest of your service.
## Key levers for tool quality
- **Docstrings are the prompt.** The model sees your docstring as the tool
description — write what it does, when to use it, and what each parameter means.
This is the single biggest lever on correct calling.
- **Strong, explicit types** (`str`, `int`, `float`, `bool`, enums, Pydantic
models as params) — the server derives the JSON Schema from annotations, and the
model uses it to fill arguments correctly.
- **Small surface.** Each tool should do one thing. A model is far more likely to
call three simple well-named tools than one mega-tool with a `mode` flag.
- **Fail loudly with useful messages.** If a tool can't do the job, return a
string the model can act on (e.g. "No todo with id 42 found"), not a bare
exception or empty string.
- Use `dependencies=[...]` on `FastMCP(...)` or `mcp install` with `-v KEY=val`
/ `-f .env` to declare pip deps and secrets so `mcp install` sets them up.
## Lifecycle and session
MCP has an initialize handshake, then a session. A server declares its
capabilities (tools/resources/prompts) and the client lists them. Standard
messages are handled for you by FastMCP — you mostly write the tool bodies. Keep
tool execution stateless where possible, or scope state per-session; shared
mutable state across concurrent model calls is a common source of bugs.
## Pitfalls
- Exposing every internal function, including things the model shouldn't do
(destructive or privileged actions) — scope tools to least privilege (the MCP
security skill lists concrete hardening).
- Putting secret access tokens in code or committing them; load from env only.
- Read-only data as tools instead of resources.
- Choosing HTTP transport without auth/rate-limiting, then leaving a tool anyone
can call.
- Vague docstrings -> constant malformed calls. Test with the exact client/agent
you'll ship to.
- Forgetting that tools are remote code execution surfaces — validate inputs
server-side; never trust the model's arguments.
## Verify
- Run the server and connect with the official `mcp` CLI or an MCP inspector to
confirm tools/resources/prompts are discoverable.
- Call each tool with a real request and confirm the returned text is actionable.
- Test failure paths: wrong arg types, missing resources, exceptions — does the
model get a helpful string back?
- If HTTP: confirm auth works, CORS is configured, and a tool can't be invoked
without valid credentials.
- Try to get the model to call a tool you deliberately left out to confirm the
declared surface matches reality.