Blog
4 min read

How to Build an MCP Server (TypeScript and Python)

Build a working Model Context Protocol server that gives Claude Code and other AI tools new abilities. Tools, resources and prompts explained, a TypeScript server with the official SDK and a Python one with FastMCP, connecting it to Claude Code, stdio vs HTTP transports, and designing tools agents use well.

An MCP server exposes abilities — tools, data, prompt templates — that any MCP-compatible AI client can use: Claude Code, Claude Desktop, Cursor, VS Code and others. Write it once, and every client can call it. (What is MCP?, MCP vs API)

You'd build one to let an agent query your internal API, look things up in your database safely, trigger a deploy, or read your company's documentation.

The three things a server can offer

  • Tools — functions the model can call: search_orders, create_ticket. The most common by far.
  • Resources — data the client can read, identified by URI: a file, a record, a report.
  • Prompts — reusable prompt templates the user can invoke.

Start with tools.

A TypeScript server

npm init -y
npm install @modelcontextprotocol/sdk zod
// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({ name: 'orders', version: '1.0.0' })

server.registerTool(
  'get_order',
  {
    title: 'Get order',
    description: 'Look up an order by its ID. Returns status, total and line items.',
    inputSchema: { orderId: z.string().describe('Order ID, e.g. ord_123') },
  },
  async ({ orderId }) => {
    const res = await fetch(`${process.env.ORDERS_API}/orders/${orderId}`, {
      headers: { Authorization: `Bearer ${process.env.ORDERS_TOKEN}` },
    })
    if (!res.ok) {
      return { content: [{ type: 'text', text: `Order ${orderId} not found (HTTP ${res.status})` }], isError: true }
    }
    return { content: [{ type: 'text', text: JSON.stringify(await res.json(), null, 2) }] }
  },
)

await server.connect(new StdioServerTransport())

Note: with the stdio transport, never console.log — stdout is the protocol channel. Log to stderr (console.error).

The same in Python (FastMCP)

uv add "mcp[cli]" httpx
# server.py
import os, httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders")

@mcp.tool()
async def get_order(order_id: str) -> str:
    """Look up an order by its ID. Returns status, total and line items."""
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"{os.environ['ORDERS_API']}/orders/{order_id}",
            headers={"Authorization": f"Bearer {os.environ['ORDERS_TOKEN']}"},
        )
        r.raise_for_status()
        return r.text

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

The docstring and type hints become the tool's description and schema.

Connect it to Claude Code

claude mcp add orders -e ORDERS_API=https://api.internal -e ORDERS_TOKEN=$ORDERS_TOKEN -- npx tsx server.ts

Check with /mcp, then ask: "What's the status of order ord_123?" (Connecting MCP servers to Claude Code)

The MCP Inspector (npx @modelcontextprotocol/inspector) lets you call your tools by hand while developing.

stdio vs HTTP

stdio Streamable HTTP
Runs As a local process the client starts As a web service
Good for Personal tools, local files, dev Shared servers, teams, remote clients
Auth Environment variables OAuth or tokens

Start local with stdio. Move to HTTP when several people or machines need the same server — then add authentication, and treat it like any public API. (API authentication methods)

Designing tools agents use well

The model chooses tools from their names and descriptions. Good design matters more than code:

  • Describe when to use it, not just what it does. Include formats and examples in parameter descriptions.
  • Fewer, higher-level tools beat many tiny ones. get_customer_summary is better than six calls the model must chain.
  • Return what the model needs, concisely. Huge JSON dumps waste context; paginate and trim. (Context engineering)
  • Helpful errors (isError: true with a reason and a hint) let the model recover.
  • Separate read and write tools, so clients can allow reads freely and gate writes.

Security

Your server runs with whatever credentials you give it, on behalf of a model that may be reading untrusted text:

  • Least privilege — a read-only token unless writes are essential. (Principle of least privilege)
  • Validate inputs server-side; never pass model input straight into SQL or shell commands. (SQL injection)
  • Confirm destructive actions — make them separate tools that clients will prompt for.
  • Don't leak secrets in tool output.

More in Securing MCP servers.

Distributing it

Package it as a Claude Code plugin to share with your team alongside skills and hooks. (Claude Code plugins)


EasySpawn gives you a persistent server to run HTTP MCP servers next to the APIs and databases they wrap — always on, behind HTTPS, reachable by your whole team's agents. See how it works or join the waitlist.

Related: What Is MCP? · Securing MCP Servers · Connecting MCP Servers to Claude Code · What Is Function Calling?

Keep reading