How to Create MCP Servers in Python or TypeScript with Claude Skills

Claude Skills are packaged as MCP (Model Context Protocol) servers using FastMCP in Python or McpServer in TypeScript, with both SDKs providing built-in support for tool registration, Pydantic or Zod input validation, pagination, and configurable transports including stdio, HTTP, and SSE.

The ComposioHQ/awesome-claude-skills repository provides reference implementations for exposing custom capabilities to Claude through the Model Context Protocol. Whether you choose Python or TypeScript, the high-level frameworks abstract protocol details like message framing and transport handshakes, allowing you to focus on defining tool logic with strict validation and descriptive annotations.

Building MCP Servers in Python with FastMCP

The Python implementation relies on the FastMCP class, which handles server lifecycle management and runs over stdio transport by default. According to mcp-builder/reference/python_mcp_server.md, you define tools using the @mcp.tool decorator and validate inputs using Pydantic v2 models with strict configuration.

Server Initialization and Naming

Initialize your server with a descriptive, service-oriented name following the {service}_mcp convention. This naming pattern helps agents infer the target service from task descriptions without requiring explicit configuration.

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field, ConfigDict

mcp = FastMCP("example_mcp")

Input Validation with Pydantic v2

Define strict input schemas using Pydantic v2 models with ConfigDict settings that strip whitespace and forbid extra fields. Include pagination parameters (limit, offset) and response format selection to support large datasets and client preferences.

class UserSearchInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
    query: str = Field(..., description="Search term", min_length=2, max_length=200)
    limit: int = Field(default=20, ge=1, le=100)
    offset: int = Field(default=0, ge=0)
    response_format: str = Field(default="markdown")

Tool Registration and Annotations

Register tools using the @mcp.tool decorator, providing annotations that declare whether the tool is read-only, destructive, idempotent, or open-world. These hints guide the agent's decision-making when planning tool invocations.

Complete Python Implementation

The following implementation from mcp-builder/reference/python_mcp_server.md demonstrates a complete user search tool with API integration, pagination support, and dual response formats:

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field, ConfigDict
import httpx

# Initialize the server

mcp = FastMCP("example_mcp")

# Input model (Pydantic v2)

class UserSearchInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
    query: str = Field(..., description="Search term", min_length=2, max_length=200)
    limit: int = Field(default=20, ge=1, le=100)
    offset: int = Field(default=0, ge=0)
    response_format: str = Field(default="markdown")

# Shared API helper

async def _make_api_request(endpoint: str, params: dict) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://api.example.com/v1/{endpoint}", params=params, timeout=30)
        resp.raise_for_status()
        return resp.json()

# Register the tool

@mcp.tool(name="example_search_users", annotations={"readOnlyHint": True, "destructiveHint": False,
                                                    "idempotentHint": True, "openWorldHint": True})
async def example_search_users(params: UserSearchInput) -> str:
    data = await _make_api_request("users/search", {"q": params.query,
                                                    "limit": params.limit,
                                                    "offset": params.offset})
    users = data.get("users", [])
    total = data.get("total", 0)

    if not users:
        return f"No users found matching '{params.query}'"

    if params.response_format == "markdown":
        lines = [f"# User Search Results: '{params.query}'", f"Found {total} users (showing {len(users)})", ""]

        for u in users:
            lines.append(f"## {u['name']} ({u['id']})")

            lines.append(f"- **Email**: {u['email']}")
            if u.get("team"):
                lines.append(f"- **Team**: {u['team']}")
            lines.append("")
        return "\n".join(lines)
    else:
        return httpx.Response(200, json={"total": total, "count": len(users),
                                        "offset": params.offset, "users": users}).text

if __name__ == "__main__":
    mcp.run()

Building MCP Servers in TypeScript with McpServer

The TypeScript implementation uses the McpServer class from @modelcontextprotocol/sdk, as detailed in mcp-builder/reference/node_mcp_server.md. This approach uses Zod for runtime type checking and provides explicit registerTool calls for method registration.

Server Setup and Schema Definition

Create a server instance with a hyphenated name following the {service}-mcp-server pattern. Define input schemas using Zod with .strict() validation to reject unexpected properties, mirroring the Pydantic behavior in Python.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "example-mcp", version: "1.0.0" });

enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json",
}

const UserSearchInputSchema = z.object({
  query: z.string().min(2).max(200).describe("Search term"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
  response_format: z.nativeEnum(ResponseFormat).default(ResponseFormat.MARKDOWN),
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

Tool Registration with Metadata

Use the server.registerTool method to bind handlers to tool names. Provide metadata including title, description, input schema, and behavior annotations that inform the agent about tool characteristics.

Complete TypeScript Implementation

This implementation demonstrates the same user search functionality with Zod validation and stdio transport configuration:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import axios from "axios";

// Server instance
const server = new McpServer({ name: "example-mcp", version: "1.0.0" });

// Enum for response formats
enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json",
}

// Zod schema for input validation
const UserSearchInputSchema = z.object({
  query: z.string().min(2).max(200).describe("Search term"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
  response_format: z.nativeEnum(ResponseFormat).default(ResponseFormat.MARKDOWN),
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

// Shared API helper
async function makeApiRequest<T>(endpoint: string, params: any): Promise<T> {
  const resp = await axios.get(`https://api.example.com/v1/${endpoint}`, { params, timeout: 30000 });
  return resp.data as T;
}

// Register the tool
server.registerTool(
  "example_search_users",
  {
    title: "Search Example Users",
    description: "Search users by name, email, or team. Returns markdown or JSON.",
    inputSchema: UserSearchInputSchema,
    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
  },
  async (params: UserSearchInput) => {
    const data = await makeApiRequest<any>("users/search", {
      q: params.query,
      limit: params.limit,
      offset: params.offset,
    });
    const users = data.users ?? [];
    const total = data.total ?? 0;

    if (users.length === 0) {
      return { content: [{ type: "text", text: `No users found matching '${params.query}'` }] };
    }

    if (params.response_format === ResponseFormat.MARKDOWN) {
      const lines = [`# User Search Results: '${params.query}'`, `Found ${total} users (showing ${users.length})`, ""];

      for (const u of users) {
        lines.push(`## ${u.name} (${u.id})`);

        lines.push(`- **Email**: ${u.email}`);
        if (u.team) lines.push(`- **Team**: ${u.team}`);
        lines.push("");
      }
      return { content: [{ type: "text", text: lines.join("\n") }] };
    } else {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              { total, count: users.length, offset: params.offset, users },
              null,
              2,
            ),
          },
        ],
      };
    }
  },
);

// Run the server (stdio transport)
async function main() {
  const transport = new (await import("@modelcontextprotocol/sdk/server/stdio.js")).StdioServerTransport();
  await server.connect(transport);
  console.error("Example MCP server running via stdio");
}
main().catch((e) => console.error("Server error:", e));

Cross-Cutting Concerns and Best Practices

Both Python and TypeScript implementations in the ComposioHQ/awesome-claude-skills repository follow consistent patterns for reliability and agent interoperability, as outlined in mcp-builder/SKILL.md.

Centralized Error Handling

Implement shared helper functions (such as _make_api_request in Python or makeApiRequest in TypeScript) to translate HTTP status errors, timeouts, and API failures into clear, actionable error messages. This ensures agents receive intelligible feedback rather than raw stack traces.

Pagination and Truncation

Tools that list resources must accept limit and offset parameters to prevent context window overflow. Respect a CHARACTER_LIMIT constant when formatting responses to avoid overwhelming the agent with data that exceeds processing capacity.

Transport Layer Configuration

By default, both FastMCP and McpServer run over stdio for local CLI integration. For web-service deployments, you can switch to HTTP or SSE (Server-Sent Events) by initializing the appropriate transport when connecting the server. This flexibility allows the same tool definitions to serve local agents and remote services.

Summary

  • Claude Skills are MCP servers – Both Python (FastMCP) and TypeScript (McpServer) SDKs provide high-level frameworks that handle protocol details.
  • Strict validation is required – Use Pydantic v2 in Python and Zod in TypeScript with strict mode to reject invalid or extra fields.
  • Use descriptive naming – Follow {service}_mcp (Python) or {service}-mcp-server (TypeScript) conventions for agent discoverability.
  • Annotate tool behavior – Declare hints for read-only, destructive, idempotent, and open-world characteristics to guide agent decision-making.
  • Handle pagination and limits – Implement limit/offset parameters and respect character limits to manage response sizes.
  • Support multiple transports – Default to stdio for local use, with HTTP or SSE available for networked deployments.

Frequently Asked Questions

What is the difference between FastMCP and McpServer?

FastMCP is the high-level Python framework that uses decorators (@mcp.tool) for tool registration and Pydantic for validation, while McpServer is the TypeScript equivalent that uses explicit registerTool calls and Zod schemas. Both handle transport configuration, message framing, and protocol compliance automatically, allowing you to focus on business logic.

How do I validate inputs in an MCP server?

In Python, define Pydantic v2 models with ConfigDict(extra='forbid') to strictly validate and sanitize inputs. In TypeScript, use Zod schemas with .strict() to achieve the same effect. Both approaches enforce constraints like string lengths and numeric ranges before your tool logic executes, preventing invalid data from reaching your API calls.

Which transport should I use for my MCP server?

Use stdio (the default) for local CLI tools and integrations where the client spawns the server process directly. Use HTTP or SSE (Server-Sent Events) for web-service deployments where the server must accept connections over a network or persist independently of the client process.

How do I prevent my MCP tools from returning too much data?

Implement pagination parameters (limit and offset) in tool definitions that list resources, and enforce a CHARACTER_LIMIT constant to truncate responses. This prevents context window overflow and ensures the agent receives manageable, actionable data chunks that can be processed efficiently.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →