How to build an MCP server: Python and TypeScript examplesMCP servers: what they are and how to connect one

How to build an MCP server: an example in Python and TypeScript

The short version: an MCP server is a small program on an official SDK that declares tools — a name, a description and parameters. In Python a working server is about ten lines with the FastMCP library; in TypeScript, slightly more. You start with a local server (stdio), check it in MCP Inspector, connect it to Claude Code, and when the server has to be handed to other people you move it to HTTP. Below is the whole path, plus the rakes we stepped on running 26 remote servers in production.

First decide whether you need your own server

For the popular services — GitHub, Figma, Notion, a browser, search — servers already exist; see the best MCP servers. You write your own when the model needs access to yours: an internal API, a database, a CRM, the team's scripts. If you are still working out what MCP is, start with the overview of MCP servers.

An example in Python (FastMCP)

Install the SDK — uv is the most convenient way:

uv init my-mcp && cd my-mcp
uv add "mcp[cli]"

The file server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders")

@mcp.tool()
def order_status(order_id: str) -> str:
    """Order status by number. Use when the user asks where their order is."""
    # here goes the call to your API or database
    return f"Order {order_id}: handed over to the courier"

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

That is the entire server. FastMCP turns the function signature into a parameter schema and the docstring into the tool's description. The description is exactly what the model reads when deciding whether to call the tool, so write not "what the function does" but when to use it.

An example in TypeScript

npm init -y
npm install @modelcontextprotocol/sdk zod

The file server.ts (package.json needs "type": "module"):

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(
  'order_status',
  {
    description:
      'Order status by number. Use when the user asks where their order is.',
    inputSchema: { orderId: z.string().describe('Order number') },
  },
  async ({ orderId }) => ({
    content: [{ type: 'text', text: `Order ${orderId}: handed over to the courier` }],
  }),
);

await server.connect(new StdioServerTransport());

A stdio server must never write to stdout

A local server talks to the client over standard output, and any print() or console.log() breaks the protocol — the client gets garbage instead of JSON. Write logs to stderr (console.error; Python's logging goes there by default).

Checking it in MCP Inspector

Before connecting the server to a client, check it in Inspector — the official debugger with a web interface: it shows the list of tools, their schemas, and lets you call each one by hand.

# Python
uv run mcp dev server.py

# TypeScript, and any other server
npx @modelcontextprotocol/inspector npx tsx server.ts

Connecting to Claude Code and other clients

A local server is connected by its launch command. In Claude Code everything after -- is that command:

claude mcp add orders -- uv run --directory /path/to/my-mcp server.py
claude mcp add orders-ts -- npx tsx /path/to/server.ts

In clients with an mcp.json — Cursor, LM Studio and others — the same command is written as the command and args fields. Where each one keeps its config: Claude, Cursor, LM Studio, Codex, OpenCode.

From local to remote: HTTP

A local server only works for whoever launched it. To connect by URL — from claude.ai, from another machine, across a whole team — the server moves to the Streamable HTTP transport. In FastMCP that is one line:

mcp.run(transport="streamable-http")

In TypeScript, StreamableHTTPServerTransport replaces StdioServerTransport inside your HTTP server (Express, Fastify). With HTTP come questions a local server never had:

The questionWhat to settle
Who is callingSign-in: OAuth 2.1 as described in the MCP specification, or a key in the Authorization header. Claude.ai and Cursor can do OAuth; LM Studio and CI are easier with a key — supporting both is sensible
StateA stateless server is simpler to scale: every request stands on its own
Long callsOperations that run for minutes hold the connection open — they need protection from timeouts (see below)
CostIf a tool costs money, reserve the cost before calling the external API, not after

Rakes from production

We keep 26 remote MCP servers running — search, image, video, music and speech generation. Here is what the short guides leave out, and what cost us incidents.

1. A tool name is longer than it looks

Many clients and models glue the server name and the tool name together — for example happyhorse-video-generation_happyhorse-reference-to-video. OpenAI-compatible APIs validate that name against ^[a-zA-Z0-9_-]{1,64}$ and, on a breach, reject the whole request rather than the one tool. Keep names short, ASCII, with no dots or spaces, and check the length together with the server prefix.

2. More than five minutes of silence kills the call

Video generation sends nothing until the result is ready. Popular Node.js HTTP clients default to 300-second header and body timeouts, and at exactly five minutes the client drops the connection — while the server carries on working, stores the file and charges for it. The cure is keepalive comments in the SSE stream (a line like : keepalive every 20 seconds): the event parser discards them, but the connection timers are reset.

3. Without the right 401, the client will not find OAuth

For Claude or Cursor to offer a sign-in by themselves, a request without a token must be answered with 401 plus the header WWW-Authenticate: Bearer resource_metadata="…" pointing at /.well-known/oauth-protected-resource, which in turn points at an authorization server with dynamic client registration and PKCE. Omit the header and the client simply shows "connection error".

4. The description is a prompt

The model picks a tool by its description alone. A vague "works with orders" leads either to needless calls or to the tool never being called at all. Write: when to call it, when not to call it, what comes back and what to do on an error. And return errors as text with a hint — the model will read it and fix the parameters itself.

If you want your own server just for search or generation

For those jobs you do not have to write a server from scratch: ready-made remote servers connect by URL — see MCP for search and image and video generation. And how to assemble an agent out of servers is in AI agents and MCP.

Frequently asked questions

How do I create my own MCP server?
Take an official SDK — Python (FastMCP) or TypeScript — and declare your tools: name, description and parameters. In Python a function with the @mcp.tool() decorator plus a call to mcp.run() is enough. Check the server in MCP Inspector (mcp dev server.py) and connect it to a client, for example claude mcp add name -- uv run server.py.
Which language should I write an MCP server in?
Official SDKs exist for Python, TypeScript, Java, Kotlin, C#, Go and other languages. For a quick start Python with FastMCP is easier; if the server will be part of a Node.js backend, TypeScript. The protocol is the same, and the client does not care what the server is written in.
How does a local MCP server differ from a remote one?
A local one is launched by the client as a process on your computer and talks over stdio — it can reach your files, but it only works for you. A remote one runs over HTTP (Streamable HTTP), connects by URL from any device and from Claude on the web, but needs hosting and authorization.
How do I debug an MCP server?
With MCP Inspector: npx @modelcontextprotocol/inspector plus the server's launch command, or mcp dev server.py for Python. Inspector shows the tools and their schemas and lets you call each one by hand. In a local server, write logs to stderr: stdout belongs to the protocol.
How do I add authorization to a remote MCP server?
The MCP specification describes OAuth 2.1: on a request without a token the server answers 401 with a WWW-Authenticate header pointing at the protected-resource metadata, and the authorization server supports PKCE and dynamic client registration. For clients without OAuth it is convenient to also accept a key in the Authorization: Bearer header.

Try it yourself

Everything in this guide runs inside Twelver

One chat for text, images, video, music and voice — no separate services or subscriptions.

Open Twelver chat
Rate your experience