Claude-skill-registry-data mcp-server-writing
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.
git clone https://github.com/majiayu000/claude-skill-registry-data
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/mcp-server-writing" ~/.claude/skills/majiayu000-claude-skill-registry-data-mcp-server-writing && rm -rf "$T"
data/mcp-server-writing/SKILL.mdMCP 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:
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:
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
explaining what the tool doesdescription
with property descriptionsinputSchema- Validation before processing
TypeScript (Low-Level API):
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:
@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.
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:
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:
@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:
@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.
// 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:
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:
if __name__ == "__main__": mcp.run(transport="stdio")
Examples
Example 1: Tool with Validation Error Response
// 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
// 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
@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
fieldsdescription - All input schema properties have
description - Input validation runs before any processing
- Error responses include
flagisError: true - 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
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
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
- MCP Python SDK
- MCP Best Practices
- See REFERENCE.md for complete templates and advanced patterns