The Quick Answer
Create a TypeScript MCP server that exposes tools to AI assistants in under 5 minutes:
$npm init -y$npm install @modelcontextprotocol/sdk zod$npm install -D @types/node tsx typescript
// index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { z } from "zod";
const server = new McpServer({
name: "my-mcp-server",
version: "1.0.0"
});
server.tool("add_numbers",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: `${a + b}` }]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
This creates a working MCP server that AI assistants can call to perform calculations. The SDK handles protocol compliance, type safety through Zod schemas, and transport management automatically.
Prerequisites
- Node.js 18 or higher installed
- TypeScript knowledge (basic to intermediate)
- Understanding of async/await patterns
- Familiarity with JSON-RPC concepts (helpful but not required)
Installation
# Create new project$mkdir my-mcp-server && cd my-mcp-server$npm init -y# Install core dependencies$npm install @modelcontextprotocol/sdk zod# Install dev dependencies$npm install -D @types/node tsx typescript# Initialize TypeScript$npx tsc --init
// Update package.json scripts
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
}
}
Configuration
MCP servers require proper TypeScript configuration for optimal type safety and compatibility. The SDK uses modern ES modules and requires specific compiler settings.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Configure your server metadata and capabilities. This information is exposed to clients during the initial handshake:
// src/config.ts
export const SERVER_CONFIG = {
name: "my-mcp-server",
version: "1.0.0",
capabilities: {
tools: true,
resources: true,
prompts: true
}
};
For Claude Desktop integration, add your server to the configuration file:
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/your/dist/index.js"]
}
}
}
Usage
Creating Tools
Tools are the primary way MCP servers expose functionality. Each tool requires a name, Zod schema for validation, and an async handler function.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { z } from "zod";
const server = new McpServer({
name: "utility-server",
version: "1.0.0"
});
// Simple calculation tool
server.tool(
"calculate",
{
operation: z.enum(["add", "subtract", "multiply", "divide"]),
a: z.number(),
b: z.number()
},
async ({ operation, a, b }) => {
let result: number;
switch (operation) {
case "add": result = a + b; break;
case "subtract": result = a - b; break;
case "multiply": result = a * b; break;
case "divide": result = a / b; break;
}
return {
content: [{
type: "text",
text: `Result: ${result}`
}]
};
}
);
Tools can return multiple content types including text, images, and embedded resources. The SDK automatically handles serialization and protocol compliance.
Managing Resources
Resources provide structured data access with URI-based addressing. Use ResourceTemplates for dynamic content generation:
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
// Static resource
server.resource(
"config",
async () => ({
contents: [{
uri: "config://settings",
mimeType: "application/json",
text: JSON.stringify({ debug: true })
}]
})
);
// Dynamic resource with URI parameters
server.resource(
"user-profile",
new ResourceTemplate("user://{username}", {
list: async () => [
{ uri: "user://alice", name: "Alice's Profile" },
{ uri: "user://bob", name: "Bob's Profile" }
]
}),
async (uri, { username }) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
username,
created: new Date().toISOString()
})
}]
})
);
Resources support pagination, caching headers, and streaming for large datasets. The template system enables REST-like patterns while maintaining protocol compliance.
Implementing Prompts
Prompts provide reusable templates for common AI interactions. They help standardize how your server communicates with language models:
server.prompt(
"code-review",
{
language: z.enum(["typescript", "javascript", "python"]),
code: z.string(),
focus: z.enum(["security", "performance", "style"]).optional()
},
({ language, code, focus }) => ({
messages: [{
role: "user",
content: `Review this ${language} code${focus ? ` focusing on ${focus}` : ''}:\n\n\`\`\`${language}\n${code}\n\`\`\`\n\nProvide specific suggestions for improvement.`
}]
})
);
Common Issues
Error: Cannot find module '@modelcontextprotocol/sdk'
Module resolution failures often occur with incorrect TypeScript configuration. The SDK requires Node16 or NodeNext module resolution.
// Fix: Update tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
# Verify installation$npm list @modelcontextprotocol/sdk
This error can also occur when running TypeScript files directly without compilation. Use tsx
for development or compile before running in production.
Error: MCP error -32000: Connection closed
Transport initialization failures cause immediate connection drops. This typically happens when the server crashes during startup or when stdio streams are incorrectly configured.
// Add error handling to prevent crashes
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk";
server.onerror = (error) => {
console.error("Server error:", error);
// Log but don't exit - let MCP handle reconnection
};
process.on('SIGINT', async () => {
await server.close();
process.exit(0);
});
Error: Tool not found (code -32602)
Missing tool registration or naming mismatches cause tool execution failures. Tools must be registered before connecting the transport.
// Register all tools before connecting
server.tool("my_tool", schema, handler);
server.tool("another_tool", schema2, handler2);
// Only connect after all tools are registered
const transport = new StdioServerTransport();
await server.connect(transport);
Tool names must match exactly when called by clients. Use consistent naming conventions (snake_case is recommended) across your server.
Examples
File System Explorer
A practical MCP server that provides safe file system access with proper error handling and security constraints:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";
const server = new McpServer({
name: "fs-explorer",
version: "1.0.0"
});
// Sandbox to user's home directory for security
const SANDBOX = process.env.HOME || "/home/user";
server.tool(
"list_files",
{
directory: z.string().default("."),
pattern: z.string().optional()
},
async ({ directory, pattern }) => {
const fullPath = path.resolve(SANDBOX, directory);
// Security: Ensure path stays within sandbox
if (!fullPath.startsWith(SANDBOX)) {
throw new Error("Access denied: Path outside sandbox");
}
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const files = entries
.filter(e => !pattern || e.name.includes(pattern))
.map(e => ({
name: e.name,
type: e.isDirectory() ? "dir" : "file"
}));
return {
content: [{
type: "text",
text: JSON.stringify(files, null, 2)
}]
};
}
);
server.tool(
"read_file",
{
filePath: z.string(),
encoding: z.enum(["utf8", "base64"]).default("utf8")
},
async ({ filePath, encoding }) => {
const fullPath = path.resolve(SANDBOX, filePath);
if (!fullPath.startsWith(SANDBOX)) {
throw new Error("Access denied");
}
const content = await fs.readFile(fullPath, encoding);
return {
content: [{
type: "text",
text: content
}]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
This server demonstrates path sandboxing for security, flexible parameter handling with defaults, and proper error messages for debugging. Production deployments should add rate limiting, audit logging, and more granular permissions.
Database Query Interface
An MCP server that safely exposes database access with parameterized queries and connection pooling:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { z } from "zod";
import { createPool, Pool } from "mysql2/promise";
const server = new McpServer({
name: "database-query",
version: "1.0.0"
});
let pool: Pool;
// Initialize connection pool
async function initDatabase() {
pool = createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "readonly",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
}
server.tool(
"query_database",
{
query: z.string(),
parameters: z.array(z.any()).optional(),
limit: z.number().max(1000).default(100)
},
async ({ query, parameters, limit }) => {
// Security: Only allow SELECT queries
if (!query.trim().toUpperCase().startsWith("SELECT")) {
throw new Error("Only SELECT queries are allowed");
}
// Add limit to prevent large result sets
const limitedQuery = `${query} LIMIT ${limit}`;
try {
const [rows] = await pool.execute(limitedQuery, parameters);
return {
content: [{
type: "text",
text: JSON.stringify(rows, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Query error: ${error.message}`
}]
};
}
}
);
// Graceful shutdown
process.on("SIGTERM", async () => {
await pool.end();
await server.close();
process.exit(0);
});
await initDatabase();
const transport = new StdioServerTransport();
await server.connect(transport);
Key design decisions include read-only access enforcement, parameterized queries to prevent SQL injection, connection pooling for performance, and graceful shutdown handling. For production use, add query timeout controls, result caching, and comprehensive audit logging.
API Integration Gateway
A sophisticated MCP server that acts as a gateway to external APIs with authentication and caching:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
import { z } from "zod";
const server = new McpServer({
name: "api-gateway",
version: "1.0.0"
});
// In-memory cache with TTL
const cache = new Map<string, { data: any; expires: number }>();
server.tool(
"call_api",
{
endpoint: z.string().url(),
method: z.enum(["GET", "POST", "PUT", "DELETE"]).default("GET"),
headers: z.record(z.string()).optional(),
body: z.any().optional(),
cacheTTL: z.number().default(300) // 5 minutes
},
async ({ endpoint, method, headers, body, cacheTTL }) => {
const cacheKey = `${method}:${endpoint}:${JSON.stringify(body)}`;
// Check cache
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return {
content: [{
type: "text",
text: JSON.stringify({
cached: true,
data: cached.data
}, null, 2)
}]
};
}
// Make API request
const response = await fetch(endpoint, {
method,
headers: {
"Content-Type": "application/json",
...headers
},
body: body ? JSON.stringify(body) : undefined
});
const data = await response.json();
// Cache successful responses
if (response.ok) {
cache.set(cacheKey, {
data,
expires: Date.now() + (cacheTTL * 1000)
});
}
return {
content: [{
type: "text",
text: JSON.stringify({
status: response.status,
data
}, null, 2)
}]
};
}
);
// Resource for API documentation
server.resource(
"api-docs",
new ResourceTemplate("api://docs/{service}", {
list: async () => [
{ uri: "api://docs/weather", name: "Weather API" },
{ uri: "api://docs/geocoding", name: "Geocoding API" }
]
}),
async (uri, { service }) => ({
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: `# ${service} API Documentation\n\nEndpoints and usage examples...`
}]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
This gateway pattern enables AI assistants to interact with external services while providing caching for efficiency, flexible authentication options, and structured documentation access. Production deployments should implement OAuth token management, request retry logic, and comprehensive error handling for different API failure modes.
Related Guides
Building an MCP server in Python using FastMCP
Build production-ready MCP servers in Python using the FastMCP framework with automatic tool handling.
Building an MCP server in Go
Learn how to build high-performance MCP servers using Go with concurrency and production-ready patterns.
Installing MCP servers globally vs locally: which approach to choose
Choose between global and local MCP server installations based on your project needs and workflow.