MCP Server Development
Overview
Comprehensive skill for developing Model Context Protocol (MCP) servers. MCP is a standardized protocol that enables AI agents (like Claude) to access external tools, data sources, and services through a unified interface.
Covers: MCP protocol fundamentals, building servers in Python (FastMCP) and TypeScript, agent-centric tool design, Claude Code integration (plugins, skills, hooks), production deployment, evaluation-driven development, and plugin packaging.
When to Use
- Building new MCP servers from scratch
- Integrating external APIs or services as MCP tools
- Creating Claude Code plugins
- Designing tools optimized for AI agent consumption
- Configuring MCP servers for Claude Desktop or other clients
- Writing evaluations to test MCP server quality
- Implementing security, caching, or production patterns
When NOT to Use
- Designing tool interfaces or consolidation patterns for agents (use tool-design)
- Prompt engineering or prompt optimization (use prompt-engineering)
- Building the actual business logic your tools wrap (that's application development)
- Debugging Claude's behavior when calling tools (that's a Claude usage issue)
Decision Tree
What are you building?
│
├─ New MCP server
│ ├─ Python codebase or data/ML integration? → FastMCP (Python)
│ ├─ TypeScript/Node.js codebase? → @modelcontextprotocol/sdk (TypeScript)
│ └─ Need both? → Build in Python first, add TypeScript wrapper if needed
│
├─ Existing server needs improvement
│ ├─ Tools return inconsistent formats? → Add output schemas + CHARACTER_LIMIT
│ ├─ Tools fail on edge cases? → Add validation (Pydantic/Zod) + error handling
│ ├─ Context window overflow? → Optimize responses (paginate, truncate, summarize)
│ └─ Security concerns? → Add path validation, destructiveHint, confirmation flags
│
├─ Deploying to production
│ ├─ Local only? → STDIO transport
│ ├─ Remote service? → Streamable HTTP transport
│ ├─ Server-to-client notifications or streaming? → Streamable HTTP with optional SSE responses
│ └─ Older client compatibility required? → Also expose legacy HTTP+SSE endpoints
│
└─ Not sure if MCP is the right approach
├─ Wrapping an API for Claude? → Yes, MCP server
├─ Building a full web application? → No, use Next.js/Python web framework
└─ Just need Claude to read files? → No, Claude can do that natively
Quick Start
Language Selection
Choose Python (FastMCP) when: Primary codebase is Python, integrating with data science/ML, or need rapid prototyping.
Choose TypeScript when: Primary codebase is Node.js/TypeScript, building full-stack apps, or need advanced type safety.
Python Quick Start (FastMCP)
from fastmcp import FastMCP
from pydantic import Field
from typing import Annotated
mcp = FastMCP("my-server")
@mcp.tool()
def search_items(
query: str,
limit: Annotated[int, Field(ge=1, le=100)] = 10
) -> dict:
"""Search for items matching the query."""
results = perform_search(query, limit)
return {"results": results, "count": len(results)}
@mcp.resource("data://config")
def get_config() -> dict:
return {"setting": "value"}
if __name__ == "__main__":
mcp.run() # STDIO transport (default)
TypeScript Quick Start
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("search_items", {
title: "Search Items",
description: "Search for items matching the query",
inputSchema: z.object({
query: z.string(),
limit: z.number().int().min(1).max(100).default(10),
}).strict(),
annotations: { readOnlyHint: true },
}, async (params) => {
const results = await performSearch(params.query, params.limit);
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }, null, 2) }] };
});
const transport = new StdioServerTransport();
await server.connect(transport);
Core Concepts
MCP Components
| Component | Purpose | Example |
|---|---|---|
| Tools | Executable functions for AI to call | search_repos, create_issue |
| Resources | Read-only data sources | data://config, file://readme.md |
| Prompts | Reusable prompt templates | explain_code, summarize_document |
Transport Types
| Transport | Use Case |
|---|---|
| STDIO | Local processes, Claude Desktop (default, most common) |
| Streamable HTTP | Remote services; one MCP endpoint supporting POST and optional GET/SSE |
| Legacy HTTP+SSE | Backwards compatibility for clients pinned to the 2024-11-05 transport |
Current MCP transport guidance defines stdio and Streamable HTTP as the standard transports. Do not design a new remote server as separate "HTTP" and "SSE" transports. Streamable HTTP uses HTTP POST for JSON-RPC messages and may use SSE to stream multiple server messages from the MCP endpoint. For compatibility, older clients may still need the legacy HTTP+SSE pair alongside the new endpoint.
MCP Configuration
{
"mcpServers": {
"server-name": {
"command": "python",
"args": ["-m", "my_server"],
"env": {"API_KEY": "***"}
}
}
}
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Wrapping REST endpoints 1:1 as tools | Tools mirror API structure, not agent workflows | Design workflow-oriented tools: create_issue_with_labels instead of create_issue + add_labels |
| No output format constraints | Responses bloat context window with irrelevant data | Enforce CHARACTER_LIMIT (25,000 chars) with truncation; return only what the agent needs |
| Missing input validation | Malformed inputs crash the server or produce garbage | Python: Pydantic Field() with constraints; TypeScript: Zod schemas with .strict() |
| Generic error messages | Agent cannot self-correct when tools fail | Return actionable errors: "Rate limit exceeded, retry after 60s" not "Error: 429" |
| Tools without annotations | Agent doesn't know if a tool is safe or destructive | Set readOnlyHint, destructiveHint, idempotentHint annotations appropriately |
| Unpaginated list operations | Large datasets overwhelm the context window | Implement pagination with limit/offset parameters |
| Storing secrets in tool parameters | API keys leak into conversation logs | Use environment variables; reference via env in MCP config |
| No response format standardization | Each tool returns data in a different structure | Support both JSON and Markdown; define consistent response schema per tool |
Best Practices Summary
Tool Design
- Design workflow-oriented tools, not API endpoint wrappers
- Use descriptive names with service prefix:
{service}_{action}_{resource} - Optimize for AI context window efficiency
- Provide actionable error messages
Input/Output
- Support both JSON and Markdown response formats
- Implement pagination for list operations
- Enforce CHARACTER_LIMIT (typically 25,000) with truncation
Validation
- Python: Use Pydantic
Field()with constraints - TypeScript: Use Zod schemas with
.strict() - Sanitize file paths and external identifiers
Security
- Validate file paths against allowed directories
- Use confirmation flags for destructive operations
- Set
destructiveHintannotation for state-changing tools - Store secrets in environment variables
- For Streamable HTTP, validate
Origin, bind local servers to localhost, require authentication, and include the negotiatedMCP-Protocol-Versionheader on HTTP requests
See Extended Patterns for the full development workflow, Claude Code plugin integration, evaluation creation, complete reference file listings, and script documentation.
Additional Resources
- MCP Specification:
https://modelcontextprotocol.io/specification/2025-11-25 - MCP Transports:
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports - FastMCP:
https://github.com/jlowin/fastmcp - Claude Code Docs:
https://docs.anthropic.com/claude-code