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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
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"
manifest: data/mcp-server-writing/SKILL.md
source content

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

FactorTypeScriptPython FastMCP
Type SafetyExcellent (Zod/Ajv)Good (type hints)
Schema ValidationBuilt-in with ZodDecorator-based
Prototyping SpeedMediumFast
Production ReadinessHighHigh
EcosystemNode.js, npmpip, 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
    description
    explaining what the tool does
  • inputSchema
    with property descriptions
  • 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
    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

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