How to Build an MCP Server: TypeScript and Python Tutorial (2026)
An MCP server is a small program that gives AI apps such as Claude Code, Cursor, VS Code Copilot and ChatGPT new tools: "look up this repo", "query our database", "create a ticket". You write the tool once, describe its inputs with a schema, and every client that speaks the Model Context Protocol can call it. This guide builds a real server that fetches GitHub repository stats, in TypeScript and in Python, using the official SDKs as of November 2026 (@modelcontextprotocol/server 2.4 and mcp 2.3 for Python). Every code block below was run before publishing.
What is an MCP server?
MCP is a protocol, like HTTP or the Language Server Protocol: an AI app (the host, for example Claude Code) runs an MCP client for each server it connects to, and each server advertises what it can do. A server can offer three things (server concepts):
- Tools: functions the model decides to call, with a JSON Schema for the arguments. This is what most servers are for.
- Resources: read-only data the app can attach as context, addressed by URI (a file, a table schema, a doc page).
- Prompts: reusable prompt templates the user picks, often shown as slash commands.
Messages are JSON-RPC. You never write them by hand: the SDK turns registerTool into the tools/list and tools/call handlers, validates arguments against your schema, and frames everything for the transport.
Before you build, check the MCP server directory: there are already official servers for GitHub, Postgres, Supabase, Sentry, Playwright and hundreds more. Build your own when the tool is specific to your company, your API or your workflow.
Build an MCP server in TypeScript
You need Node.js 20 or later.
1. Set up the project
mkdir github-repos-mcp && cd github-repos-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
@modelcontextprotocol/server is the v2 TypeScript SDK. Older tutorials import from @modelcontextprotocol/sdk/server/mcp.js; that is the v1 package (still published, at 1.32), and the code below won't run against it unchanged. Zod describes tool inputs; the SDK converts the schema to JSON Schema for the model.
2. Write the server and its tool
Put the server in a factory function, so the same code serves stdio and HTTP. src/server.ts:
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
export function createServer(): McpServer {
const server = new McpServer({ name: 'github-repos', version: '1.0.0' });
server.registerTool(
'repo-stats',
{
description: 'Get stars, forks, open issues, license and last push date of a public GitHub repository',
inputSchema: z.object({
repo: z.string().regex(/^[\w.-]+\/[\w.-]+$/).describe('owner/name, e.g. vercel/next.js'),
}),
},
async ({ repo }) => {
const res = await fetch(`https://api.github.com/repos/${repo}`, {
headers: {
'User-Agent': 'mcp-repo-stats',
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
},
});
if (!res.ok) {
return { content: [{ type: 'text', text: `GitHub API error: HTTP ${res.status} for ${repo}` }], isError: true };
}
const r = (await res.json()) as {
stargazers_count: number;
forks_count: number;
open_issues_count: number;
pushed_at: string;
license: { spdx_id: string } | null;
};
return {
content: [
{
type: 'text',
text: `${repo}: ${r.stargazers_count} stars, ${r.forks_count} forks, ${r.open_issues_count} open issues, license ${r.license?.spdx_id ?? 'none'}, last push ${r.pushed_at}`,
},
],
};
},
);
return server;
}
Three details that matter:
- The description is the prompt. The model picks tools by name and description, so say what the tool returns and when to use it.
.describe()on each field becomes the argument's description. - Validation is free. Call it with
"bad repo"and the SDK answers withInput validation error: ... must match patternbefore your handler runs. - Return
isError: truefor failures the model can fix. A thrown exception and anisErrorresult both reach the model as a tool error, but returning one lets you write a helpful message ("repo not found, check owner/name") so it can retry.
3. Serve it over stdio
src/stdio.ts:
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { createServer } from './server.ts';
void serveStdio(createServer);
console.error('github-repos MCP server running on stdio'); // stderr: stdout carries the protocol
Run npx tsx src/stdio.ts. It prints the banner and then waits silently on stdin for a client. That is correct: stop it with Ctrl+C.
Build an MCP server in Python
The Python SDK v2 calls the high-level server MCPServer (v1 called it FastMCP, imported from mcp.server.fastmcp). Type hints and the docstring become the tool's schema and description. server.py:
import os
import httpx
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer("github-repos")
@mcp.tool()
async def repo_stats(repo: str) -> str:
"""Get stars, forks, open issues, license and last push date of a public GitHub repository (owner/name)."""
headers = {"User-Agent": "mcp-repo-stats"}
if token := os.environ.get("GITHUB_TOKEN"):
headers["Authorization"] = f"Bearer {token}"
async with httpx.AsyncClient() as client:
res = await client.get(f"https://api.github.com/repos/{repo}", headers=headers)
if res.status_code != 200:
raise ToolError(f"GitHub API error: HTTP {res.status_code} for {repo}")
r = res.json()
license_id = (r.get("license") or {}).get("spdx_id", "none")
return f"{repo}: {r['stargazers_count']} stars, {r['forks_count']} forks, {r['open_issues_count']} open issues, license {license_id}, last push {r['pushed_at']}"
if __name__ == "__main__":
mcp.run()
Install and run it:
pip install "mcp>=2.3" httpx # or: uv add mcp httpx
python server.py # waits on stdin, like the TypeScript version
ToolError sends its message to the model as a tool error. Keep mcp.run() under if __name__ == "__main__":, because some hosts and the mcp CLI import the file instead of running it.
Test your MCP server with MCP Inspector
MCP Inspector is the official debugging UI. It starts your server, lists its tools, and lets you call them with your own arguments:
npx @modelcontextprotocol/inspector npx tsx src/stdio.ts
# Python: npx @modelcontextprotocol/inspector python server.py (or: uv run mcp dev server.py)
Open the URL it prints, click Connect, then Tools → List Tools → repo-stats, enter vercel/next.js and run it. Check three things before you give the server to an agent: the description reads well, a bad input gives a useful error, and the output is short. Every token a tool returns lands in the model's context window.
Add your MCP server to Claude Code, Cursor and VS Code
Use absolute paths: clients start the server from their own working directory.
Claude Code (MCP docs):
claude mcp add github-repos -e GITHUB_TOKEN=ghp_yourtoken -- npx tsx /absolute/path/github-repos-mcp/src/stdio.ts
Add --scope project to write it to .mcp.json and share it with your team. Run /mcp inside Claude Code to see whether it connected, then ask "how many stars does vercel/next.js have?".
Cursor, in .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"github-repos": {
"command": "npx",
"args": ["tsx", "/absolute/path/github-repos-mcp/src/stdio.ts"],
"env": { "GITHUB_TOKEN": "ghp_yourtoken" }
}
}
}
VS Code (Copilot agent mode), in .vscode/mcp.json. Note the top-level key is servers, not mcpServers:
{
"servers": {
"github-repos": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "/absolute/path/github-repos-mcp/src/stdio.ts"]
}
}
}
Claude Desktop, Windsurf, Codex and others take the same command and args in their own config files; each server page in our directory shows the snippet per client.
Deploy a remote MCP server over Streamable HTTP
To share a server with your team or users, serve the same createServer factory over HTTP. In TypeScript, createMcpHandler builds the endpoint and @modelcontextprotocol/node adapts it to Node's http module (npm install @modelcontextprotocol/node). src/http.ts:
import { createServer as createHttpServer } from 'node:http';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createServer } from './server.ts';
const handler = toNodeHandler(createMcpHandler(createServer));
createHttpServer((req, res) => {
if (req.url === '/mcp') return void handler(req, res);
res.writeHead(404).end();
}).listen(3000, () => console.error('MCP server on http://localhost:3000/mcp'));
The factory runs per request, so the server is stateless and scales like any API. The SDK also ships adapters for Express (@modelcontextprotocol/express), Fastify and Hono, and a web-standard handler for Cloudflare Workers, Deno, Bun and serverless functions such as Vercel.
In Python it is one argument: mcp.run(transport="streamable-http", port=3001), which serves http://127.0.0.1:3001/mcp, or mcp.streamable_http_app() to mount it in an existing Starlette or FastAPI app.
Clients connect by URL. In Claude Code: claude mcp add --transport http github-repos https://your-domain.com/mcp. In Cursor: { "mcpServers": { "github-repos": { "url": "https://your-domain.com/mcp" } } }.
Design tips for MCP tools that agents use well
- Few, task-shaped tools.
search_issues(query, state)beats exposing all 40 endpoints of an API. Each tool's description costs context on every request. - Name for the model. Verb plus noun, and a description that says when to use the tool and what comes back.
- Small outputs. Return the five fields the model needs, not the raw 8 KB JSON. Paginate long lists.
- Errors that teach. "No repo vercel/nextjs. Did you mean vercel/next.js?" lets the agent fix the call itself.
- Read-only by default. Make destructive tools separate and explicit, so clients can ask the user to confirm them.
- Secrets via env. Pass tokens as environment variables in the client config, never as tool arguments the model can see.
FAQ
What language should I use to build an MCP server?
TypeScript or Python, the two languages with the most-used official SDKs and examples. Official SDKs also exist for Go, Java, Kotlin, C#, Ruby, Rust, Swift and PHP (SDK list). Pick the language of the API or codebase your tool wraps.
What is the difference between MCP tools, resources and prompts?
Tools are functions the model chooses to call (search, create, query). Resources are read-only data the app or user attaches as context, identified by a URI. Prompts are templates the user picks, often as slash commands. Most servers only need tools.
Do I need to deploy my MCP server?
No. A stdio server runs on your machine: the client starts it as a child process when it needs it. Deploy over Streamable HTTP only when other people or a hosted agent must reach it by URL.
What is the difference between FastMCP and MCPServer?
FastMCP was the high-level server class in version 1 of the official Python SDK (from mcp.server.fastmcp import FastMCP). Version 2 renamed it to MCPServer (from mcp.server import MCPServer) with the same decorator style. There is also a separate, community-run fastmcp package (version 4 as of November 2026) with extra features on top.
Why does my MCP server not show up in Claude Code or Cursor?
Usually one of four things: a relative path in the config (use absolute paths), something printed to stdout in a stdio server, a missing environment variable, or a crash on start. Run the exact command from the config in your terminal; then check /mcp in Claude Code or the MCP output panel in Cursor and VS Code.
Is MCP only for Claude?
No. Anthropic introduced it, but Cursor, VS Code (GitHub Copilot), OpenAI's Codex and ChatGPT, Windsurf, Zed, JetBrains IDEs and many agent frameworks support it. One server works in all of them.
Next steps
- Browse ready-made servers in the MCP directory and the best MCP servers for coding agents.
- Automate around your tools with Claude Code hooks and subagents.
- Compare the agents that will call your server: AI coding agents.