Skip to content
mcp-server-writing logo

MCP Server Writing

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.

SKILL.md

Full skill instructions

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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View