mcp-server-development

verified

edd995ba-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 ID
edd995ba-e553-4c00-b4fc-25e31d4a77d0
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
mcpmodel-context-protocolfastmcptoolsagentsapisdk
Signature
verified
Integrity
OK
Content hash
d2ea9743d9bc20b1a82edd37b775e783f7779992469a3523b648af6227a04144
Created
2026-08-13T03:21:49Z

Skill file

Raw skill file (markdown source)
# 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.

Attached files