Why Build an MCP Server in TypeScript?
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"
}
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.