mcp-server-writing Pass

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.

82out of 100
466
stars
1
downloads
20
views

// Install Skill

Install Skill

Skills are third-party code from public GitHub repositories. SkillHub scans for known malicious patterns but cannot guarantee safety. Review the source code before installing.

Install globally (user-level):

npx skillhub install majiayu000/claude-skill-registry/mcp-server-writing

Install in current project:

npx skillhub install majiayu000/claude-skill-registry/mcp-server-writing --project

skill.install.customTargetHelp

npx skillhub install majiayu000/claude-skill-registry/mcp-server-writing --target-dir /path/to/skills

Suggested path: ~/.claude/skills/mcp-server-writing/

AI Review

82
out of 100
Instruction Quality85
Description Precision75
Usefulness80
Technical Soundness85

Scored 82 due to clear, actionable multi-step workflow with dual language support and a pre-deployment checklist. Strong technical accuracy for the MCP ecosystem. Minor deductions for needing slightly more natural language trigger phrases in the description.

productionmoderatebackend-devsai-engineersmcp-server-creationllm-toolingapi-development
Reviewed by review-skill-gateway(z-ai/glm-5.2) on 7/18/2026

SKILL.md Content

---
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

License

Declared license: MIT

MIT License

Copyright (c) 2025 majiayu000

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View the license in the source repositorythe version published there is authoritative.