skip the blank page
Templates
Fourteen copy-paste starters, each the distilled version of a pattern the tutorials build and test. Copy one, replace the angle-bracket placeholders, ship.
Minimal MCP server (Python / FastMCP)
One tool, one resource, stdio — the smallest server that is still shaped correctly: validated input, an error a model can act on.
# server.py — minimal MCP server (Python, stdio)
# Install: uv add "mcp[cli]" (or: pip install "mcp[cli]")
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def get_order_status(order_id: str) -> str:
"""Look up the shipping status of an order by its numeric id."""
if not order_id.isdigit():
return "invalid order_id: must be numeric" # error a model can act on
return f"order {order_id}: shipped" # replace with your real lookup
@mcp.resource("docs://readme")
def readme() -> str:
"""Project documentation the client can pull in as context."""
return "What this server exposes, and the ids it expects."
if __name__ == "__main__":
mcp.run() # stdio transport by default
# Try it in a browser UI: uv run mcp dev server.py Minimal MCP server (TypeScript)
McpServer + zod + stdio, with the tsconfig settings that make the .js import extensions work instead of mystifying you.
// src/index.ts — minimal MCP server (TypeScript, stdio)
// npm install @modelcontextprotocol/sdk zod@3
//
// tsconfig note: set "module": "Node16" and "moduleResolution": "Node16".
// The .js extension on the imports below is correct ESM style — not a typo.
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: "my-server", version: "1.0.0" });
server.registerTool(
"get_order_status",
{
description: "Look up the shipping status of an order by its numeric id.",
inputSchema: { order_id: z.string().regex(/^\d+$/, "must be numeric") },
},
async ({ order_id }) => ({
content: [{ type: "text", text: `order ${order_id}: shipped` }],
})
);
await server.connect(new StdioServerTransport());
// Build, then verify:
// npx tsc && npx @modelcontextprotocol/inspector node build/index.js Streamable HTTP server skeleton (Python)
The remote transport, in stateless mode so it scales behind a load balancer from day one.
# http_server.py — Streamable HTTP MCP server (Python)
# Stateless mode: no per-session state, so any replica can answer any
# request — the load-balancer-friendly shape for real deployments.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-http-server", stateless_http=True)
@mcp.tool()
def ping() -> str:
"""Health check: returns pong."""
return "pong"
if __name__ == "__main__":
mcp.run(transport="streamable-http") # serves /mcp on port 8000
# Point clients at http://127.0.0.1:8000/mcp
# Production notes:
# - Put TLS in front; validate Origin headers (spec: HTTP 403 for invalid Origin).
# - Streamable HTTP replaced HTTP+SSE in spec 2025-03-26 — build new servers on this. OAuth resource-server middleware (sketch)
Bearer validation with the audience check that separates a compliant resource server from forbidden token passthrough. A sketch — adapt to your IdP.
// auth-middleware.ts — SKETCH to adapt, not production code.
// An MCP server is an OAuth *resource server*: it validates tokens issued by
// your (external) authorization server. It never mints or forwards tokens.
import type { Request, Response, NextFunction } from "express";
const RESOURCE = "https://mcp.example.com/mcp"; // this server's canonical URI
export async function requireBearer(req: Request, res: Response, next: NextFunction) {
const auth = req.headers.authorization ?? "";
const token = auth.startsWith("Bearer ") ? auth.slice(7) : null;
if (!token) {
// RFC 9728: point clients at your protected-resource metadata
res.setHeader(
"WWW-Authenticate",
`Bearer resource_metadata="${RESOURCE}/.well-known/oauth-protected-resource"`
);
return res.status(401).json({ error: "missing bearer token" });
}
// Verify signature + expiry against your IdP's JWKS — use a maintained
// library (e.g. jose); do NOT hand-roll JWT validation.
const claims = await verifyJwt(token);
// The audience check is the whole point (RFC 8707 resource indicators):
// accepting a token issued for some OTHER service is the forbidden
// "token passthrough" pattern the spec calls out explicitly.
if (claims.aud !== RESOURCE) {
return res.status(403).json({ error: "token not issued for this resource" });
}
next();
} claude_desktop_config.json starter
macOS and Windows variants with the three classics pre-solved: absolute paths, escaped backslashes, and the env block.
{
"//": "claude_desktop_config.json — Claude Desktop's MCP server registry.",
"//macos": "~/Library/Application Support/Claude/claude_desktop_config.json",
"//windows": "%AppData%\\Claude\\claude_desktop_config.json",
"mcpServers": {
"my-server": {
"//": "Absolute paths only — Claude Desktop does not inherit your shell PATH or cwd.",
"command": "/Users/you/project/.venv/bin/python",
"args": ["/Users/you/project/server.py"],
"env": { "API_KEY": "your-key-here" }
},
"my-server-on-windows": {
"//": "Windows: escape every backslash in JSON paths. Bare 'npx' often fails with spawn npx ENOENT — use the full path or cmd /c npx.",
"command": "C:\\Users\\you\\project\\.venv\\Scripts\\python.exe",
"args": ["C:\\Users\\you\\project\\server.py"]
}
}
}
// Fully quit and restart Claude Desktop after editing — closing the window is not enough. .mcp.json for Claude Code (project scope)
Check it into the repo and every teammate gets the same servers — with ${VAR} expansion keeping secrets out of git.
{
"//": ".mcp.json — project-scope MCP servers for Claude Code; check into the repo.",
"//2": "Equivalent CLI: claude mcp add --transport stdio --scope project orders -- npx -y @your-org/orders-mcp",
"mcpServers": {
"orders": {
"command": "npx",
"args": ["-y", "@your-org/orders-mcp"],
"env": {
"//": "${VAR} expands from each teammate's environment — the secret never lands in git.",
"ORDERS_API_KEY": "${ORDERS_API_KEY}"
}
},
"docs-remote": {
"type": "http",
"url": "https://docs.example.com/mcp",
"headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
}
}
}
// Teammates are prompted to approve project servers on first use. Check with: /mcp .cursor/mcp.json starter
Same idea for Cursor: a local stdio server and a remote URL server, project-level or global.
{
"//": ".cursor/mcp.json — project-level servers for Cursor.",
"//2": "Global alternative: ~/.cursor/mcp.json (same shape).",
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "@your-org/my-mcp-server"],
"env": { "API_KEY": "your-key-here" }
},
"my-remote-server": {
"url": "https://mcp.example.com/mcp"
}
}
}
// Enable under Cursor Settings → MCP; tools become available in Agent mode. Dockerfile for a stdio MCP server
Containerise the server for a clean, reproducible sandbox — and never lose an hour to the missing -i flag again.
# Dockerfile — containerised stdio MCP server (Python example)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # includes mcp[cli]
COPY server.py .
# stdio server: the client talks over stdin/stdout — nothing to EXPOSE.
ENTRYPOINT ["python", "server.py"]
# GOTCHA: always run with -i, or stdin closes and the server exits instantly:
# docker build -t my-mcp-server .
# docker run -i --rm my-mcp-server
# In client config:
# "command": "docker", "args": ["run", "-i", "--rm", "my-mcp-server"] Inspector smoke-test script
Four CLI-mode Inspector calls that catch "server loads but tools don't appear" before your users do. Drop it into CI.
#!/usr/bin/env bash
# smoke-test.sh — MCP smoke test via Inspector CLI mode (no UI, CI-friendly)
set -euo pipefail
SERVER_CMD="node build/index.js"
# Python equivalent: SERVER_CMD="uv --directory . run server.py"
# 1. Does it start, handshake, and list tools?
npx @modelcontextprotocol/inspector --cli $SERVER_CMD --method tools/list
# 2. Does a real call return what you expect?
npx @modelcontextprotocol/inspector --cli $SERVER_CMD \
--method tools/call --tool-name get_order_status --tool-arg order_id=1001
# 3. Bad input: the error should be actionable, not a stack trace.
npx @modelcontextprotocol/inspector --cli $SERVER_CMD \
--method tools/call --tool-name get_order_status --tool-arg order_id=abc
# 4. Resources, if you declare any
npx @modelcontextprotocol/inspector --cli $SERVER_CMD --method resources/list
# Interactive debugging instead? Drop --cli and everything after it:
# npx @modelcontextprotocol/inspector node build/index.js server.json registry-publish starter
The manifest for publishing to the official MCP registry, with the reverse-DNS namespace rule explained inline.
{
"//": "server.json — publish to registry.modelcontextprotocol.io via the mcp-publisher CLI.",
"//2": "Name is a reverse-DNS namespace you can prove you own: io.github.<user> is verified through GitHub login.",
"//3": "The registry is in preview and feeds aggregators/marketplaces — clients don't consume it directly.",
"$schema": "https://static.modelcontextprotocol.io/schemas/<latest>/server.schema.json",
"name": "io.github.your-user/my-server",
"description": "One sentence shown in registry search results.",
"version": "1.0.0",
"packages": [
{
"registryType": "npm",
"identifier": "@your-user/my-mcp-server",
"version": "1.0.0",
"transport": { "type": "stdio" }
}
]
}
// Publish: mcp-publisher login github && mcp-publisher publish
// Check <latest> schema URL + fields against the registry docs before publishing. MCP server README template
A well-documented server declares its tools, scopes, env vars, and security posture up front — this is the shape reviewers hope to find.
# <server-name> — MCP server
One sentence: what this server lets a model do, and for whom.
## Tools
| Tool | What it does | Side effects |
|------|--------------|--------------|
| `get_<thing>` | Reads <what>, returns <shape> | none (read-only) |
| `create_<thing>` | Writes <what> | creates a record in <system> |
## Resources & prompts
- `docs://<name>` — <what context it provides>
## Environment variables
| Var | Required | Purpose | Scope of the credential |
|-----|----------|---------|-------------------------|
| `API_KEY` | yes | auth to <system> | read-only, <which data> |
## Install
Per-client config blocks: Claude Desktop, Claude Code (.mcp.json), Cursor,
VS Code. Include absolute-path and Windows variants.
## Transport & auth
- stdio for local use; Streamable HTTP at `/mcp` for remote.
- Remote auth: this server is an OAuth resource server — it accepts only
tokens issued *for it* (audience-validated), never passed-through tokens.
## Security notes
- Data this server can reach: <systems, scopes>.
- Data it never returns to the model: <secrets, PII fields redacted>.
- Destructive tools and their guardrails: <list or "none — read-only">. Tool design worksheet
Task-shaped tools, honest descriptions, a response budget, actionable errors — fill it in before code, not after.
# Tool design worksheet — fill one per tool BEFORE writing code
## Task shape
- [ ] Tool = one task a user would name ("check an order"), not one API
endpoint. Would a competent intern know when to use it from the
description alone?
- [ ] Could two of my draft tools be one? Could this one hide 3 API calls?
## Naming & description
- [ ] Name is verb_noun, snake_case, unambiguous next to my other tools.
- [ ] Description says WHEN to use it (and when not), not just what it is.
- [ ] Every parameter documented, with format + one example value.
## Response budget
- [ ] Typical response fits a budget I chose (aim well under ~1K tokens).
Tool definitions and results all consume the model's context —
a 106-tool server can burn ~54K tokens before the first question.
- [ ] Large results: return a summary + id to fetch detail, never the dump.
- [ ] No internal fields, no nulls-for-everything, no base64 blobs.
## Errors a model can act on
- [ ] Error strings say what to DO: "order_id must be numeric" beats
"ValidationError in OrderService.lookup (code 4002)".
- [ ] Not-found is a normal answer ("no order 99999"), not an exception dump.
- [ ] Rate limits / auth failures tell the model whether retrying helps.
## Safety
- [ ] Inputs validated against a schema before any real work.
- [ ] Outputs redacted before they enter the model's context.
- [ ] If destructive: annotated as such, and the README's guardrails row is filled. Third-party server security review checklist
Provenance, tool poisoning, token handling, egress — the questions to answer before any server reaches an allowlist.
# Third-party MCP server review — <server name>, <date>, <reviewer>
# Run BEFORE the server reaches anyone's allowlist. Spec's own threat list:
# confused deputy, token passthrough, session hijacking, tool poisoning.
## Provenance
- [ ] Package name verified character-by-character against the official repo
(npm/PyPI typosquatting campaigns target MCP servers specifically)
- [ ] Publisher identity checked; repo has real history, not a fresh dump
- [ ] Version PINNED (no 'latest', no floating npx -y of unpinned packages)
## Tool surface
- [ ] Read every tool description in full — descriptions are prompt input to
the model; hostile instructions hidden there = tool poisoning
- [ ] Re-review descriptions on every version bump (rug-pull updates)
- [ ] Each tool's blast radius listed: read-only / writes / destructive
- [ ] No tool accepts free-form commands to pass through to a shell or API
## Credentials & tokens
- [ ] Runs with a dedicated least-privilege credential, not a personal one
- [ ] Secrets via env vars, never hardcoded in config or code
- [ ] Remote server: validates token audience — a server that accepts tokens
issued for other services is doing forbidden token passthrough
## Network & runtime
- [ ] Egress known: which hosts does it call? Any surprise telemetry?
- [ ] Outbound URL fetching blocks private ranges incl. 169.254.169.254 (SSRF)
- [ ] Can run sandboxed (container, -i for stdio) with a resource ceiling
## Verdict
approve (scope: ____________) / reject / needs-changes: ____________ MCP rollout one-pager
Intake, allowlist, approval tiers, audit — the whole governance loop for MCP at work, on one deliberate page.
# MCP rollout one-pager — <team / org> (keep it one page)
## Intake
- Anyone can propose a server: name, source, business need, data it touches.
- Requests land in <channel / form>; triaged weekly by <owner>.
## Review → allowlist
- Security review (see checklist) required before any allowlist entry.
- Allowlist is config, not policy prose: the approved server list + pinned
versions lives at <repo path>, deployed to managed clients.
- Local/stdio servers running as the user count too — "it's on my laptop"
is still an agent with your credentials.
## Approval tiers
- Read-only, public data → team-lead approval
- Read-only, internal data → team lead + data owner
- Writes / destructive tools → security review + named business owner
- Remote servers (HTTP) → all of the above + auth review (audience-
validated tokens; SSO via IdP where supported)
## Audit
- Every server has: an owner, a version, a review date, a re-review trigger
(any version bump or tool-list change).
- Client logs sampled <monthly>: which tools fired, against which data.
- Quarterly: prune servers nobody used — every idle tool definition still
costs context and attack surface.
## Help
- Questions: <channel> · Suspected incident: <channel> + kill the allowlist entry first newsletter
One practical MCP guide in your inbox. No news, no hype.
Tutorials and decision frameworks as they ship. Unsubscribe anytime.