Building Your Own MCP Server with TypeScript β A Practical @modelcontextprotocol/sdk Tutorial
Current Situation Analysis
The rapid proliferation of AI-powered development environments has created a severe integration fragmentation problem. Teams building internal tooling, data pipelines, or workflow automations now face the prospect of writing and maintaining separate plugin architectures for Claude, Cursor, Windsurf, Zed, and emerging AI agents. Each platform historically demanded its own authentication flow, API contract, and lifecycle management, turning simple utility functions into multi-platform maintenance burdens.
This problem is frequently misunderstood as a simple API routing challenge. Developers often attempt to wrap existing REST endpoints or CLI scripts directly into AI clients, ignoring the semantic contract that modern AI agents require. AI models do not execute code; they select tools based on natural language intent, input constraints, and expected output formats. Treating AI integration as traditional backend routing leads to poor tool selection, hallucinated parameters, and fragile error handling.
Industry data confirms the shift toward standardization. Major AI coding platforms have converged on the Model Context Protocol (MCP) as the baseline integration layer. By adopting a unified schema-driven contract, organizations reduce integration overhead by approximately 60-70% per new AI platform release. The protocol enforces strict input validation, standardized response envelopes, and transport abstraction, eliminating the need for platform-specific glue code. The technical reality is clear: building isolated AI plugins is a depreciating strategy, while MCP server architecture is becoming the operational baseline for developer tooling.
WOW Moment: Key Findings
The architectural advantage of MCP becomes quantifiable when comparing traditional plugin development against schema-driven MCP server implementation. The following comparison isolates the operational metrics that determine long-term maintainability and AI reliability.
| Approach | Platform Coverage | Schema Enforcement | AI Context Alignment | Deployment Complexity |
|---|---|---|---|---|
| Traditional Plugin Architecture | 1:1 per AI client | Manual/Ad-hoc | Low (prompt-dependent) | High (N codebases) |
| MCP Server Architecture | 1:N (universal) | Strict (Zod/JSON Schema) | High (semantic descriptions) | Low (single transport layer) |
This finding matters because it shifts the engineering focus from platform-specific wiring to semantic tool design. When tool registration includes precise natural language descriptions and strict input schemas, AI models achieve significantly higher selection accuracy and parameter compliance. The standardized response envelope ({ content: [{ type: "text", text: "..." }] }) removes client-side parsing ambiguity, while transport decoupling allows identical business logic to run in-memory during development and over stdio in production. Teams that internalize this model stop chasing AI platform updates and start shipping interoperable tooling that outlives individual client releases.
Core Solution
Building a production-ready MCP server requires isolating three distinct concerns: server instantiation, schema-driven tool binding, and transport abstraction. The implementation below demonstrates a RepoInsightServer that queries GitHub's public API to retrieve repository metrics. The architecture prioritizes type safety, semantic clarity, and transport flexibility.
Step 1: Project Bootstrap & Module Configuration
The SDK distributes exclusively as an ECMAScript Module. Node.js must be explicitly configured to resolve ESM imports, or the runtime will fail during transport initialization.
{
"name": "repo-insight-mcp",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.0.0",
"typescript": "^5.4.0"
}
}
The tsconfig.json must align with bundler resolution to prevent path mapping errors during SDK import:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Why this matters: The "type": "module" flag and bundler resolution prevent the ERR_MODULE_NOT_FOUND errors that commonly occur when Node attempts to resolve the SDK's internal ESM entry points. Skipping this configuration forces developers into CommonJS workarounds that break transport initialization.
Step 2: Server Instantiation & Transport Decoupling
The server instance acts as a registry, not a network listener. Transport layers are injected separately, allowing identical business logic to run across different communication channels.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "repo-insight-server",
version: "2.1.0"
});
Why this matters: Decoupling the registry from the transport layer enables seamless environment switching. Development uses in-memory pipes for instant feedback, while production clients (Claude Desktop, Cursor) consume the same registry over standard input/output streams. No business logic changes are required when switching contexts.
Step 3: Schema-Driven Tool Binding
Tool registration requires four distinct arguments. The second and third arguments are critical for AI reliability: the description guides model selection, and the Zod schema enforces input contracts before execution.
server.tool(
"fetch_repo_metrics",
"Retrieve public repository statistics including star count, fork count, and primary language. Use when the user asks about GitHub project popularity, tech stack, or contribution metrics.",
{
repositoryPath: z
.string()
.regex(/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/, "Must be in owner/repo format")
.describe("GitHub repository path (e.g., 'microsoft/vscode')")
},
async ({ repositoryPath }) => {
const [owner, repo] = repositoryPath.split("/");
const endpoint = `https://api.github.com/repos/${owner}/${repo}`;
const response = await fetch(endpoint, {
headers: { Accept: "application/vnd.github.v3+json" }
});
if (!response.ok) {
return {
content: [
{
type: "text",
text: `Repository not found or rate limited. Status: ${response.status}`
}
]
};
}
const data = await response.json();
const metrics = [
`Repository: ${data.full_name}`,
`Stars: ${data.stargazers_count.toLocaleString()}`,
`Forks: ${data.forks_count.toLocaleString()}`,
`Language: ${data.language || "None detected"}`,
`Open Issues: ${data.open_issues_count}`
].join("\n");
return {
content: [{ type: "text", text: metrics }]
};
}
);
Why this matters: AI models select tools based on semantic overlap between user intent and tool descriptions. Vague descriptions cause cross-tool invocation errors. Zod validation prevents malformed requests from reaching the handler, reducing runtime exceptions. The standardized response envelope ensures consistent parsing across all MCP clients.
Step 4: Transport Initialization & Client Simulation
Development testing requires a paired transport that routes messages within the same process. Production deployment swaps this for a stdio bridge.
// Development: In-memory roundtrip
const [clientPipe, serverPipe] = InMemoryTransport.createLinkedPair();
await server.connect(serverPipe);
const testClient = {
name: "dev-simulator",
version: "1.0.0"
};
// Simulate client discovery
const toolRegistry = await server.listTools();
console.log("Registered endpoints:", toolRegistry.tools.map(t => t.name));
// Simulate invocation
const invocation = await server.callTool({
name: "fetch_repo_metrics",
arguments: { repositoryPath: "facebook/react" }
});
console.log(invocation.content[0].text);
Why this matters: In-memory testing eliminates network latency and external API dependencies during schema validation. It verifies that the registry, transport, and handler contract align before deployment. Production clients automatically discover tools via listTools and invoke them via callTool, mirroring this exact flow over stdio.
Pitfall Guide
1. Exception Throwing Instead of Structured Returns
Explanation: Throwing JavaScript errors inside tool handlers breaks the MCP response contract. Clients expect a structured envelope, and uncaught exceptions cause inconsistent behavior across AI platforms.
Fix: Always return errors within the content array. Map HTTP failures, validation errors, and runtime exceptions to descriptive text payloads.
2. Ambiguous Tool Descriptions
Explanation: AI models use tool descriptions as selection heuristics. Generic phrases like "fetch data" or "get info" cause the model to invoke the wrong tool or hallucinate parameters. Fix: Write descriptions that specify the exact use case, expected input format, and output domain. Include trigger phrases the AI should match.
3. ESM/Module Resolution Mismatch
Explanation: The SDK ships as ESM. Running in CommonJS mode or using incorrect moduleResolution causes import failures during transport initialization.
Fix: Enforce "type": "module" in package.json and set "moduleResolution": "bundler" in tsconfig.json. Verify Node version supports top-level await.
4. Transport Lifecycle Leaks
Explanation: Failing to close client connections or reusing transport instances across multiple server lifecycles causes message queue corruption and stale tool registries.
Fix: Instantiate fresh transport pairs per test cycle. Call close() on client transports after verification. Avoid global transport singletons.
5. Ignoring External API Rate Limits
Explanation: Public APIs (GitHub, Open Library, etc.) enforce strict rate limits. Unthrottled MCP tool invocations quickly exhaust quotas, returning 403/429 errors that degrade AI reliability. Fix: Implement request caching, exponential backoff, or queue-based throttling. Return clear rate-limit messages in the response envelope.
6. Hardcoded Secrets in Handlers
Explanation: Embedding API keys or tokens directly in tool handlers exposes credentials in version control and client logs. MCP servers often run in shared environments. Fix: Inject secrets via environment variables. Validate presence during server startup. Never log or return credential fragments in response payloads.
7. Overcomplicating Response Payloads
Explanation: Returning deeply nested JSON or binary data in the content array increases token consumption and parsing latency. AI clients optimize for flat, text-heavy responses.
Fix: Flatten metrics into newline-separated strings. Use type: "image" or type: "resource" only when explicitly required. Keep payloads under 2KB when possible.
Production Bundle
Action Checklist
- Verify ESM configuration: Ensure
package.jsoncontains"type": "module"andtsconfig.jsonuses"moduleResolution": "bundler". - Define semantic tool descriptions: Write descriptions that explicitly state when the AI should invoke the tool and what data it returns.
- Enforce Zod validation: Attach
.describe()to every schema field. Validate regex patterns for structured inputs (paths, IDs, emails). - Standardize error handling: Replace all
throwstatements with structured{ content: [{ type: "text", text: "..." }] }returns. - Implement transport switching: Use
InMemoryTransportfor CI/CD validation andStdioServerTransportfor client deployment. - Add rate limit safeguards: Wrap external API calls in retry logic with exponential backoff. Cache responses where idempotent.
- Secure credential injection: Load API keys from
process.env. Fail fast on startup if required variables are missing. - Validate response size: Flatten output strings. Avoid nested objects unless the client explicitly requests structured data.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Local development & unit testing | InMemoryTransport.createLinkedPair() |
Zero network overhead, instant feedback, isolates business logic from I/O | None (dev-only) |
| Desktop AI clients (Claude, Cursor) | StdioServerTransport |
Standardized pipe communication, native client support, process isolation | Minimal (stdio overhead) |
| Web-based AI agents | Streamable HTTP Transport | Enables CORS, session management, and load balancing across instances | Moderate (requires reverse proxy) |
| High-frequency internal tooling | Batched tool registration + caching | Reduces external API calls, prevents rate limit exhaustion | Low (memory overhead for cache) |
Configuration Template
// src/server.ts
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: "production-tool-server", version: "1.0.0" });
server.tool(
"execute_workflow",
"Trigger internal deployment pipeline or data sync job. Requires job ID and environment target.",
{
jobId: z.string().uuid(),
environment: z.enum(["staging", "production"])
},
async ({ jobId, environment }) => {
// Business logic here
return {
content: [{ type: "text", text: `Workflow ${jobId} queued for ${environment}` }]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Quick Start Guide
- Initialize the project:
mkdir mcp-tool-server && cd mcp-tool-server && npm init -y - Install dependencies:
npm install @modelcontextprotocol/sdk zod && npm i -D typescript @types/node tsx - Configure modules: Set
"type": "module"inpackage.jsonand apply thetsconfig.jsontemplate above. - Write the server: Create
src/server.tswith theMcpServerinstance, tool registration, andStdioServerTransportbinding. - Run locally: Execute
npx tsx src/server.tsto verify stdio initialization, or swap toInMemoryTransportfor instant client simulation.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
