MCP: Model Context Protocol Server Development
Build, review, and debug MCP servers that expose tools, resources, and prompts to AI coding assistants. The goal is secure, well-structured servers that follow the protocol spec and don't become yet another server with preventable injection vulnerabilities.
Target versions (May 2026):
- MCP specification: 2025-11-25 (current stable; 2026-03-15 in draft)
- TypeScript SDK: @modelcontextprotocol/sdk 1.29.0 (1.x stable; 2.0.0-alpha in dev)
- Python SDK: mcp 1.27.0 (v1.26.0+)
- Protocol transports: stdio, Streamable HTTP (the standalone HTTP+SSE transport was deprecated in spec 2025-03-26; SSE still streams inside Streamable HTTP)
When to use
- Building a new MCP server (tools, resources, prompts)
- Adding tool handlers to an existing MCP server
- Configuring MCP transport (stdio for local, streamable HTTP for remote)
- Implementing MCP authentication (OAuth 2.1)
- Implementing MCP elicitation (interactive dialogs)
- Reviewing MCP server code for injection or tool poisoning vulnerabilities
- Debugging MCP connection issues between client and server
- Migrating from a custom tool integration to MCP
When NOT to use
- General REST API development that doesn't use MCP - just write the API
- Claude API / Anthropic SDK usage in an application - use ai-ml
- Security auditing existing servers across a codebase - use security-audit (it has an MCP section)
- Using MCP browsing tools to browse or scrape web pages - use browse
- Writing prompts for LLMs (not MCP prompt resources) - use prompt-generator
AI Self-Check
When generating or reviewing MCP server code, verify each item before presenting the result:
- All tool handler inputs validated server-side (no raw string interpolation into shell commands, SQL, file paths, or URLs)
- Tool descriptions accurate and concise (some clients truncate long descriptions)
- Resource URIs use a defined scheme and are validated before use
- Error responses use proper MCP error codes, not raw stack traces
- Authentication implemented for remote transports that handle user data (OAuth 2.1 with PKCE)
- No secrets hardcoded in tool handlers or server configuration
-
inputSchemauses specific JSON Schema types withrequired,maxLength, constraints - Server handles graceful shutdown (cleanup on SIGINT/SIGTERM)
- Streamable HTTP: binds to
127.0.0.1(not0.0.0.0) when local - Streamable HTTP: validates
Originheader (DNS rebinding prevention) - Rate limiting on tool invocations
- Tool annotations treated as untrusted by client-side code
- Elicitation does not request passwords, tokens, or secrets
- Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
- Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
- Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
- Routing overlap checked: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
- Spec claims verified: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
- Spec version checked: transports, auth, resources, tools, and prompts match current MCP docs and SDK behavior
- Tool poisoning considered: tool descriptions, dynamic metadata, and server updates cannot silently expand authority
- SDK methods verified: see Common Mistakes #8 - verify every API call against actual SDK docs rather than inventing method names
Performance
- Keep tool schemas tight and responses small; large unstructured tool outputs waste model context.
- Use resources for reusable context instead of returning the same large payload from every tool call.
- Batch read-only lookups where latency matters, but keep side-effecting tools separate and auditable.
Best Practices
- Treat MCP servers as security boundaries: authenticate, authorize, and log side effects explicitly.
- Make tool names and schemas stable; version breaking changes instead of changing semantics in place.
- Require user confirmation for tools that spend money, mutate infrastructure, delete data, or expose secrets.
Workflow
Build vs. Review: Steps 1-6 are for building new servers. When reviewing existing MCP server code: (1) scope using Step 1 questions - what tools, transport, and auth does the server use; (2) audit each tool handler against Step 3 injection vectors and the AI Self-Check; (3) cross-reference the Common Mistakes section for patterns AI models frequently introduce.
Step 1: Determine the server's purpose
Before writing code, clarify:
- What tools will it expose? Each tool = one operation the AI can invoke.
- What resources will it serve? Resources = read-only data the AI can access.
- What transport? stdio for local CLI integration, streamable HTTP for remote/production.
- What authentication? None for stdio. OAuth 2.1 recommended for remote servers handling user data.
- What language? TypeScript (most mature SDK) or Python (simpler, FastMCP).
Step 2: Scaffold the server
TypeScript (recommended for production):
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" });
// Current SDK API: `server.registerTool(name, { title, description, inputSchema }, handler)`.
// The older `server.tool(name, desc, schema, handler)` shorthand still works in v1.x.
server.registerTool(
"search_docs",
{
title: "Search docs",
description: "Search documentation by keyword",
inputSchema: { query: z.string().max(200).describe("Search query"), limit: z.number().int().min(1).max(100).default(10) },
},
async ({ query, limit }) => {
// If this tool reads files, apply path validation from Step 3 before any fs access.
const sanitized = query.replace(/[^\w\s-]/g, "");
const results = await searchIndex(sanitized, limit);
return { content: [{ type: "text", text: JSON.stringify(results) }] };
}
);
server.resource("config", "config://app/settings", async (uri) => ({
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(config) }],
}));
const transport = new StdioServerTransport();
await server.connect(transport);
Python (FastMCP for quick prototyping):
import json, re
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def search_docs(query: str, limit: int = 10) -> str:
"""Search documentation by keyword."""
sanitized = re.sub(r"[^\w\s-]", "", query[:200])
return str(search_index(sanitized, min(limit, 100)))
@mcp.resource("config://app/settings")
def get_config() -> str:
"""Application configuration."""
return json.dumps(config)
if __name__ == "__main__":
mcp.run()
Step 3: Implement tools securely
Injection is the top MCP vulnerability class. Every tool handler is an attack surface.
The #1 rule: never interpolate user input into commands, queries, or paths.
Common injection vectors in MCP tools:
| Vector | Bad pattern | Safe pattern |
|---|---|---|
| Shell | Interpolated command strings | execFile with argument arrays + path validation |
| SQL | String concatenation in queries | Parameterized queries with $1 placeholders |
| File paths | Direct readFile(userPath) | Resolve path, validate prefix against allowlist |
| URLs | Direct fetch(userUrl) | Parse URL, validate scheme + host against allowlist |
| Tem |