How to Build an MCP Server (TypeScript, Step by Step)
How to build an MCP server in TypeScript with the current SDK: set up the project, register a tool, serve it over stdio, test it and add it to Claude Code.
Josh Pocock5 min read
AI Tools
Build an MCP server
Stride AI Academy
To build an MCP server, install the MCP TypeScript SDK and zod, create a server, register each tool with a description and an input schema, serve it over stdio, then add it to Claude Code. A useful first server is one small tool that reads data you already have.
This tutorial builds a working server for agency work: a tool that answers "how many retainer hours does this client have left?" from a JSON file. The code follows the current MCP build-a-server guide for spec version 2026-07-28, checked October 2026. The SDK changed recently, so older tutorials may use package names and calls that no longer match.
What an MCP server does
MCP (Model Context Protocol) is an open standard for connecting AI applications to outside systems. A server can offer three kinds of things:
- Tools: functions the model can call, with the user's approval
- Resources: file-like data the client can read
- Prompts: ready-made templates for common tasks
Most useful servers for client work are tools: "look up this order", "create this task", "check this account".
What happens when Claude calls your tool
- 1You ask Claude a question
- 2Claude picks your toolFrom its description
- 3Claude Code calls the serverOver stdio, on your machine
- 4Your code runsReads your data, returns text
- 5Claude answers with the result
Step 1: Set up the project
You need Node.js 20 or newer.
mkdir agency-clients
cd agency-clients
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
mkdir srcAdd these fields to package.json:
{
"type": "module",
"bin": { "agency-clients": "./build/index.js" },
"scripts": { "build": "tsc && chmod 755 build/index.js" },
"files": ["build"]
}On Windows, where chmod doesn't exist, set the build script to just tsc.
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"types": ["node"],
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}Step 2: Some data to read
Create clients.json somewhere on your machine:
[
{ "name": "Acme Plumbing", "hoursUsed": 6, "hoursIncluded": 10 },
{ "name": "Northside Dental", "hoursUsed": 9, "hoursIncluded": 8 }
]In a real build this would be your CRM, database or time tracker's API. A file keeps the first version simple.
Step 3: Write the server
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
import { readFile } from "node:fs/promises";
const DATA_FILE = process.env.CLIENTS_FILE ?? "clients.json";
type Client = { name: string; hoursUsed: number; hoursIncluded: number };
async function loadClients(): Promise<Client[]> {
return JSON.parse(await readFile(DATA_FILE, "utf8")) as Client[];
}
// serveStdio calls this whenever it needs an instance, so return a new one each time.
function createServer() {
const server = new McpServer({ name: "agency-clients", version: "1.0.0" });
server.registerTool(
"get_client_hours",
{
description: "Get the retainer hours used and remaining this month for one client",
inputSchema: z.object({
client: z.string().describe("The client's name, for example Acme Plumbing"),
}),
},
async ({ client }) => {
const clients = await loadClients();
const match = clients.find((c) => c.name.toLowerCase() === client.toLowerCase());
if (!match) {
return { content: [{ type: "text", text: `No client named ${client}.` }] };
}
const left = match.hoursIncluded - match.hoursUsed;
return {
content: [
{
type: "text",
text: `${match.name}: ${match.hoursUsed} of ${match.hoursIncluded} hours used, ${left} left.`,
},
],
};
},
);
return server;
}
serveStdio(createServer, {
onerror: (error) => console.error("agency-clients error:", error),
});
console.error("agency-clients MCP server running on stdio");Three details from the official guide that trip people up:
- Never use
console.login a stdio server. Standard output carries the protocol messages, so printing to it breaks the server. Useconsole.error, which writes to standard error. createServermust return a fresh server each time.serveStdiocalls it whenever it needs an instance.- Use
serveStdio. It negotiates the protocol version with each client. The olderserver.connect(new StdioServerTransport())pattern still compiles but only serves 2025-era clients.
The tool's description matters most. It's how the model decides when to call the tool, so say what it does in the words a person would use.
Step 4: Build and test
npm run build
npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/listThe Inspector should list get_client_hours. Run the same command with --protocol-era legacy too, so you know older clients can use it. If nothing lists, check that the build finished and that nothing prints to standard output.
Step 5: Connect it to Claude Code
Add it to your project's .mcp.json with absolute paths:
{
"mcpServers": {
"agency-clients": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/agency-clients/build/index.js"],
"env": { "CLIENTS_FILE": "/absolute/path/to/clients.json" }
}
}
}Open Claude Code in that project, approve the server when asked, and type /mcp to confirm it's connected. Then ask: "How many hours does Northside Dental have left?" Claude should call your tool and reply that they're one hour over.
Claude Code asks before using servers from a project's .mcp.json, which is a good habit to keep: a stdio server runs on your machine with your permissions. See the Claude Code MCP docs for other ways to add servers.
Where to go next
- Add tools one at a time. "List clients over their hours", "log hours for a client". Keep each tool small with a clear description.
- Connect real data. Swap the JSON file for your CRM or database API. Keep keys in environment variables, never in code.
- Read-only first. Start with tools that only read. Add tools that change data once you trust the setup, and keep Claude's permission prompts on for them.
- Remote servers. To share a server with a team or a client, the official docs cover building servers that run over HTTP instead of stdio.
Before building, check whether the vendor already publishes one. Many do: see the MCP servers we'd set up first.
Learn to build client tools
The Stride AI Coding course teaches Claude Code and Codex for building tools for clients, including connecting them to the systems clients already use, with weekly live calls. Many of the free guides cover MCP setups.
FAQ
Is it hard to create an MCP server?
A basic one is a few dozen lines of code: create a server, register a tool with a description and an input schema, and serve it over stdio. The harder parts are the same as any integration: authentication, error handling and deciding what the model should be allowed to do.
Can I create a local MCP server?
Yes. A stdio server, like the one in this tutorial, runs as a local process on your machine and talks to the client through standard input and output. That's the simplest kind to build and test.
What language should I use for an MCP server?
Use one you or your AI coding tool are comfortable with. The official guide has tutorials for Python, TypeScript and several other languages. TypeScript and Python are the most common choices.
Can you give an example of an MCP server?
Official ones include GitHub's server for repositories and issues, Playwright's for browser automation and Supabase's for databases. The example in this post is a small custom server with one tool that reports a client's retainer hours from a JSON file.

