home / skills / meriley / claude-code-skills / mcp-server-writing
npx playbooks add skill meriley/claude-code-skills --skill mcp-server-writingReview the files below or copy the command above to add this skill to your agents.
---
name: mcp-server-writing
description: Creates production-ready MCP servers with tools, resources, and prompts using TypeScript SDK or Python FastMCP. Use when building MCP integrations for Claude or LLM applications.
version: 1.0.0
---
# MCP Server Writing
## Purpose
Guide creation of production-ready Model Context Protocol (MCP) servers following official SDK patterns, security best practices, and real-world implementation patterns.
## When to Use This Skill
- Building a new MCP server from scratch
- Adding tools, resources, or prompts to an existing MCP server
- Choosing between TypeScript and Python implementations
- Implementing proper error handling and validation for MCP tools
- Setting up structured logging for MCP servers
## When NOT to Use This Skill
- Reviewing existing MCP code (use **mcp-server-reviewing**)
- Building simple CLI tools that don't need LLM integration
- Creating HTTP REST APIs (MCP uses JSON-RPC, not REST)
- General TypeScript/Python development unrelated to MCP
---
## Quick Decision: TypeScript vs Python
| Factor | TypeScript | Python FastMCP |
| ------------------------ | ------------------- | ----------------- |
| **Type Safety** | Excellent (Zod/Ajv) | Good (type hints) |
| **Schema Validation** | Built-in with Zod | Decorator-based |
| **Prototyping Speed** | Medium | Fast |
| **Production Readiness** | High | High |
| **Ecosystem** | Node.js, npm | pip, uv |
**Recommendation:** Use TypeScript for production servers with complex schemas. Use Python FastMCP for rapid prototyping or when integrating with Python libraries.
---
## Core Workflow
### Step 1: Initialize Server
**TypeScript:**
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "my-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
```
**Python FastMCP:**
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-mcp-server", json_response=True)
```
### Step 2: Define Tools with Input Schemas
Tools are functions the LLM can call. Always include:
- Clear `description` explaining what the tool does
- `inputSchema` with property descriptions
- Validation before processing
**TypeScript (Low-Level API):**
```typescript
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
const TOOLS: Tool[] = [
{
name: "process_data",
description:
"Validates and transforms input data. Returns structured result with any validation errors.",
inputSchema: {
type: "object",
properties: {
data: {
type: "string",
description: "Raw data string to process",
},
format: {
type: "string",
enum: ["json", "csv", "xml"],
description: "Expected input format",
},
},
required: ["data", "format"],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: TOOLS }));
```
**Python FastMCP:**
```python
@mcp.tool()
def process_data(data: str, format: str = "json") -> dict:
"""Validates and transforms input data.
Args:
data: Raw data string to process
format: Expected input format (json, csv, xml)
Returns:
Structured result with validation errors if any
"""
# Implementation here
return {"success": True, "processed": data}
```
### Step 3: Implement Tool Handler with Validation
**Critical:** Always validate inputs before processing. Use Ajv for TypeScript.
```typescript
import Ajv from "ajv";
const ajv = new Ajv();
const validateProcessDataArgs = ajv.compile({
type: "object",
properties: {
data: { type: "string" },
format: { type: "string", enum: ["json", "csv", "xml"] },
},
required: ["data", "format"],
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "process_data") {
// Validate inputs
if (!validateProcessDataArgs(args)) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Validation failed",
details: validateProcessDataArgs.errors,
suggestion: "Check input parameters match the schema",
}),
},
],
isError: true,
};
}
// Process valid inputs
const result = processData(args.data, args.format);
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
}
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
});
```
### Step 4: Add Resources (Optional)
Resources expose data the LLM can read. Use for configuration, status, or reference data.
**TypeScript:**
```typescript
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListResourcesRequestSchema, () => ({
resources: [
{
uri: "config://settings",
name: "Application Settings",
description: "Current server configuration",
mimeType: "application/json",
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "config://settings") {
return {
contents: [
{
uri: "config://settings",
mimeType: "application/json",
text: JSON.stringify({ theme: "dark", version: "1.0.0" }),
},
],
};
}
throw new Error(`Unknown resource: ${request.params.uri}`);
});
```
**Python FastMCP:**
```python
@mcp.resource("config://settings")
def get_settings() -> str:
"""Current server configuration."""
return json.dumps({"theme": "dark", "version": "1.0.0"})
```
### Step 5: Add Prompts (Optional)
Prompts are reusable templates the LLM can request.
**Python FastMCP:**
```python
@mcp.prompt()
def review_code(code: str, language: str = "python") -> str:
"""Generate a code review prompt."""
return f"Please review this {language} code for best practices:\n\n{code}"
```
### Step 6: Implement Structured Logging
**Critical for MCP:** Log to stderr, NEVER stdout. stdout is reserved for JSON-RPC.
```typescript
// utils/logger.ts
class Logger {
private log(level: string, message: string, context?: object): void {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
service: "my-mcp-server",
...context,
};
// CRITICAL: Use stderr, not stdout
console.error(JSON.stringify(entry));
}
info(message: string, context?: object): void {
this.log("INFO", message, context);
}
error(message: string, context?: object, error?: Error): void {
this.log("ERROR", message, {
...context,
error: error
? { name: error.name, message: error.message, stack: error.stack }
: undefined,
});
}
}
export const logger = new Logger();
```
### Step 7: Start the Server
**TypeScript:**
```typescript
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info("MCP Server started", { transport: "stdio" });
}
main().catch((error) => {
logger.error("Fatal error", {}, error);
process.exit(1);
});
```
**Python FastMCP:**
```python
if __name__ == "__main__":
mcp.run(transport="stdio")
```
---
## Examples
### Example 1: Tool with Validation Error Response
```typescript
// Always return actionable suggestions with errors
if (!validateArgs(args)) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
errors: [
{
field: "partner_id",
message: "Invalid UUID format",
suggestion: "Use format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
},
],
}),
},
],
isError: true,
};
}
```
### Example 2: ReDoS Prevention
```typescript
// Limit input length before regex processing
const MAX_INPUT_LENGTH = 50_000;
function safeInput(text: string): string {
return text.length > MAX_INPUT_LENGTH
? text.slice(0, MAX_INPUT_LENGTH)
: text;
}
// Use in tool handlers
const safeText = safeInput(args.text);
const matches = safeText.match(/pattern/);
```
### Example 3: Dynamic Resource Template
```python
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Get user profile by ID."""
user = fetch_user(user_id)
return json.dumps({"id": user_id, "name": user.name, "role": user.role})
```
---
## Validation Checklist
Before deploying your MCP server:
- [ ] All tools have clear `description` fields
- [ ] All input schema properties have `description`
- [ ] Input validation runs before any processing
- [ ] Error responses include `isError: true` flag
- [ ] Error messages include actionable `suggestion`
- [ ] Logging goes to stderr (not stdout)
- [ ] Input length limited before regex processing (ReDoS prevention)
- [ ] No hardcoded secrets in code
- [ ] Environment variables used for configuration
---
## Common Patterns
### Pattern: Structured Error Response
```typescript
interface ToolError {
field: string;
message: string;
suggestion: string;
}
function createErrorResponse(errors: ToolError[]) {
return {
content: [
{
type: "text",
text: JSON.stringify({ success: false, errors }),
},
],
isError: true,
};
}
```
### Pattern: Tool Invocation Logging
```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const startTime = Date.now();
logger.info("Tool invocation started", {
tool: name,
args_keys: Object.keys(args || {}),
});
try {
const result = await handleTool(name, args);
logger.info("Tool invocation completed", {
tool: name,
duration_ms: Date.now() - startTime,
});
return result;
} catch (error) {
logger.error(
"Tool invocation failed",
{
tool: name,
duration_ms: Date.now() - startTime,
},
error,
);
throw error;
}
});
```
---
## Troubleshooting
### Issue: Server hangs or produces garbled output
**Cause:** Logging to stdout instead of stderr
**Solution:** Change all `console.log()` to `console.error()` for logging
### Issue: Validation errors not shown to LLM
**Cause:** Missing `isError: true` in response
**Solution:** Add `isError: true` to all error responses
### Issue: Complex regex causes timeouts
**Cause:** ReDoS vulnerability with unbounded input
**Solution:** Limit input length with `safeInput()` function
---
## Related Skills
- **mcp-server-reviewing** - Audit MCP servers for best practices
- **security-scan** - Check for secrets before commits
- **quality-check** - Run linting and formatting
## Resources
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
- [MCP Best Practices](https://modelcontextprotocol.info/docs/best-practices/)
- See **REFERENCE.md** for complete templates and advanced patterns