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

> Learn to create MCP servers in Python or TypeScript using Claude Skills. Explore tool registration, input validation, and configurable transports with FastMCP and McpServer.

- Repository: [Composio/awesome-claude-skills](https://github.com/composiohq/awesome-claude-skills)
- Tags: how-to-guide
- Published: 2026-08-30

---

**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`](https://github.com/ComposioHQ/awesome-claude-skills/blob/main/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.

```python
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.

```python
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`](https://github.com/ComposioHQ/awesome-claude-skills/blob/main/mcp-builder/reference/python_mcp_server.md) demonstrates a complete user search tool with API integration, pagination support, and dual response formats:

```python
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`](https://github.com/ComposioHQ/awesome-claude-skills/blob/main/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.

```typescript
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:

```typescript
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`](https://github.com/ComposioHQ/awesome-claude-skills/blob/main/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.