How to Build an MCP Server in TypeScript (Step-by-Step)
Build your first MCP server in TypeScript with the official SDK — tools, stdio transport, and a Claude Code test loop in under an hour.
Mahmudul Haque Qudrati
CEO & ML Engineer
One AI engineering post, weekly
LLM benchmarks, prompt techniques, and token-cost breakdowns — not another AI news roundup.
The Model Context Protocol (MCP) standardizes how AI assistants interact with external tools and data. By building your own MCP server in TypeScript, you unlock custom integrations — internal APIs, deployment hooks, CMS bridges — without waiting for official servers. TypeScript gives you type safety, modern async patterns, and excellent SDK support.
This tutorial walks you through a minimal but production-ready MCP server using @modelcontextprotocol/sdk. You'll expose one tool over stdio, add a resource, and connect it to Claude Code for testing. Most developers can ship a useful internal server in under an hour.
Prerequisites
- Node.js 20+ (LTS recommended)
- pnpm (or npm/yarn)
- A TypeScript project initialized
- Claude Code for end-to-end testing (optional but recommended)
mkdir my-mcp-server && cd my-mcp-server
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript @types/node
Configure tsconfig.json for ESM:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Add a build script to package.json:
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
Team workspace
Ship faster with chat, meetings, and projects in one place — Zlyqor.
Step 1: Create a Minimal Server
Create src/index.ts with the following code:
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: 'pristren-internal', version: '1.0.0' });
server.tool(
'get_deploy_status',
'Returns last deploy status for an app slug',
{ app: z.string().describe('App slug e.g. zlyqor_web') },
async ({ app }) => {
// Replace with real API call
const status = { app, env: 'production', ok: true, version: '1.2.3' };
return {
content: [{ type: 'text', text: JSON.stringify(status, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Explanation
- McpServer: Core class from the SDK. You provide a name and version.
- server.tool(): Registers a tool. First argument is the tool name (snake_case), second is a description, third is a Zod schema for parameters, fourth is the handler function.
- StdioServerTransport: Communicates via standard input/output. Perfect for local development and Claude Code integration.
- Handler return: Must include a
contentarray with at least onetextcontent item. You can also return images or embedded resources.
Build and test locally:
pnpm build
node dist/index.js
The server will start and listen for JSON-RPC messages on stdin. You can test it manually with a tool like mcp-cli or connect it to Claude Code.
Step 2: Connect to Claude Code
Claude Code supports MCP servers via the claude mcp add command. Register your server:
claude mcp add pristren-internal -- node dist/index.js
Now you can prompt Claude Code to use your tool:
"Use pristren-internal to check deploy status for zlyqor_web."
Claude will call the get_deploy_status tool with app: "zlyqor_web" and display the result. You can also chain multiple tools in one conversation.
Troubleshooting
- If Claude doesn't find the server, verify the path to
dist/index.jsis absolute or relative to your working directory. - Check that the server starts without errors by running the command directly.
- Use
claude mcp listto see all registered servers.
Step 3: Add Resources (Read-Only Data)
Resources expose static data that the agent can read without executing shell commands. This is useful for configuration schemas, documentation, or reference data.
server.resource('config-schema', 'config://schema.json', async () => ({
contents: [{ uri: 'config://schema.json', text: '{ "type": "object" }' }],
}));
Resources use URIs for identification. The handler returns an array of content items. You can also return binary data with MIME types.
Step 4: Add Prompts (Optional)
Prompts are reusable templates that guide the AI on how to use your tools. They're like meta-instructions.
server.prompt('deploy-check', 'Check deploy status for an app', {
app: z.string().describe('App slug'),
}, ({ app }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Use the get_deploy_status tool to check the deploy status for ${app}.`,
},
}],
}));
Prompts are especially useful when you have complex workflows that require multiple tool calls.
Production Checklist
Before deploying your MCP server to production, consider these best practices:
- Validate inputs with Zod: Always use Zod schemas to validate tool parameters. This prevents malformed requests and provides clear error messages.
- Never return secrets in tool output: If your tool calls an internal API, strip sensitive fields like API keys or tokens before returning.
- Log tool calls server-side: For audit and debugging, log each tool invocation with timestamp, parameters, and result size.
- Compact responses: AI models have token limits. Return only essential data. For large datasets, consider pagination or summarization. See our guide on MCP token bloat.
- Version semver: Use semantic versioning for your server. Pin the version in your team's
.mcp.jsonto avoid breaking changes. - Error handling: Wrap tool handlers in try-catch and return user-friendly error messages.
server.tool('risky_operation', '...', { ... }, async (args) => {
try {
// ...
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
}
});
Advanced: HTTP Transport
For remote MCP servers, use HTTP transport instead of stdio. This allows multiple clients to connect over the network.
pnpm add express
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({ name: 'remote-server', version: '1.0.0' });
// ... register tools
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
// Handle MCP protocol over HTTP
// You'll need to implement JSON-RPC handling
});
app.listen(3000);
For production HTTP servers, add authentication (OAuth2, API keys) and rate limiting.
Common Pitfalls
- Blocking the event loop: Tool handlers should be async and non-blocking. Avoid synchronous file reads or heavy computations.
- Ignoring transport errors: Stdio transport can fail if the parent process dies. Implement reconnection logic for long-running servers.
- Overly complex schemas: Keep Zod schemas simple. Use
.describe()to help the AI understand parameters. - Missing error responses: Always return
isError: truefor failures so the AI knows the call failed.
Next Steps
- Add OAuth for HTTP deployment
- Publish internal server via private npm registry
- Bundle a skill documenting which tools to call — Skills vs MCP
Keep Reading
- LLM Token Optimization in 2026 — model routing, caching, MCP audit
- Claude Code Complete Setup Guide — install, CLAUDE.md, MCP, skills
- Claude Code vs Cursor: Token Cost (2026) — dollar math on identical tasks
- AI Model Sprint — June 2026 — frontier model benchmarks
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace — chat, projects, time tracking, AI meeting summaries, and invoicing.
Frequently Asked Questions
What is an MCP server and why build one in TypeScript?
An MCP (Model Context Protocol) server is a lightweight service that exposes tools, resources, and prompts to AI assistants like Claude. Building it in TypeScript gives you type safety, modern async patterns, and seamless integration with the official `@modelcontextprotocol/sdk`. It's ideal for custom internal integrations without waiting for third-party servers.
How does an MCP server work with Claude Code?
Claude Code connects to MCP servers via stdio transport. You register your server with `claude mcp add`, and Claude can then call your tools during conversations. The server listens for JSON-RPC messages on stdin and returns results on stdout. This allows Claude to execute custom actions like checking deploy status or querying internal APIs.
What are best practices for building an MCP server in TypeScript?
Best practices include: validating inputs with Zod schemas, never returning secrets in tool output, logging tool calls for audit, keeping responses compact to avoid token bloat, using semantic versioning, handling errors gracefully with `isError: true`, and choosing the right transport (stdio for local, HTTP for remote with authentication).
How much does it cost to build and run an MCP server?
Building an MCP server is free — the SDK is open-source and you only need Node.js. Running costs depend on your infrastructure: a local server costs nothing, while a cloud-hosted HTTP server may incur minimal compute costs (e.g., $5–$20/month on a small VPS). The main cost is development time, which is typically under an hour for a basic server.
Is building an MCP server in TypeScript worth it in 2026?
Absolutely. MCP is becoming the standard for AI-tool integration, and TypeScript remains the most popular language for server-side development. Building your own server gives you full control over data and workflows, avoids vendor lock-in, and can be done quickly. It's especially valuable for teams with internal APIs or custom deployment pipelines.
Mahmudul Haque Qudrati
CEO & ML Engineer
Visionary leader with extensive experience in machine learning and software development. Drives strategic innovation and business growth.
More from Mahmudul
Related Articles
How to Use Claude to Make Videos Like Vox and Others
Claude can help you make Vox-style videos by generating scripts, editing with code, and automating animation. Here's a practical guide with real workflows and costs.
What Is Alibaba Banning Claude Code Over Backdoor Risks? A Practical Overview
Alibaba is reportedly banning Claude Code from its workplace due to alleged backdoor risks. This post explains the incident, the technical concerns, and the broader implications for AI coding assistants in regulated environments.
Claude Code Sends 33k Tokens Before Reading the Prompt; OpenCode Sends 7k: A Practical Overview
Claude Code sends 33k tokens before reading your prompt; OpenCode sends 7k. This overhead affects cost and latency in agentic coding workflows. We break down the technical reasons and practical implications.
// discussion
Comments