Building Your First MCP Server in TypeScript: A Step-by-Step Developer Guide
2026-09-16 · by bixzell
While Python often dominates discussions around machine learning research, TypeScript and Node.js form the backbone of modern developer tooling, CLI applications, and desktop IDE ecosystems. If you are building tools designed to run locally alongside editors like Cursor, Windsurf, or Claude Desktop, authoring your MCP servers in TypeScript offers native event-driven concurrency, rich typing, and instant portability.
In this guide, we will build a complete, production-ready MCP server using the official @modelcontextprotocol/sdk. We will implement parameter validation using Zod, connect external APIs, configure local stdio process communication, and wire the server into an AI-powered coding editor.
Official reference servers maintained by Anthropic, including TypeScript implementations for filesystems, Git, and SQLite.
1. Project Scaffolding & Dependencies
To begin, initialize a clean Node.js workspace. Ensure you have Node.js 18+ (Node 20+ or 22 LTS recommended) installed on your system.
mkdir my-mcp-server
cd my-mcp-server
npm init -y
Because modern Node.js development relies on standard ES Modules (import/export), open package.json and ensure "type": "module" is configured:
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js"
}
}
Now install the required production dependencies:
npm install @modelcontextprotocol/sdk zod dotenv
@modelcontextprotocol/sdk: The official Anthropic-maintained TypeScript SDK containing core protocol abstractions, JSON-RPC handlers, and transport wrappers.zod: The TypeScript-first schema declaration and validation library. The MCP SDK uses Zod directly to generate machine-readable JSON Schemas for tool arguments.dotenv: For loading local credentials and configuration without committing secrets to version control.
2. Server Initialization
Create an entrypoint file named server.js (or server.ts if using a TypeScript execution runner like tsx).
The first step is importing the McpServer abstraction and defining metadata:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import dotenv from "dotenv";
dotenv.config();
// Initialize the MCP server instance
const server = new McpServer({
name: "operational-calendar-server",
version: "1.0.0",
});
The metadata object (name and version) is advertised to clients during the initial protocol handshake (initialize). Keep names unique and descriptive so agent hosts can distinguish between multiple active servers.
3. Registering Tools with Zod Schemas
An MCP server can expose three primary primitives: Tools, Resources, and Prompts. Tools are executable functions that an AI agent can invoke to take action or fetch computed data.
To register a tool, invoke server.tool():
server.tool(
"get_schedule_by_date",
{
date: z.string().describe("Target date in YYYY-MM-DD format (e.g. 2026-09-27)"),
},
async ({ date }) => {
try {
const events = await fetchEventsForDate(date);
return {
content: [
{
type: "text",
text: JSON.stringify(events, null, 2),
},
],
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to retrieve schedule: ${error.message}`,
},
],
};
}
}
);
The Three Rules of Effective Tool Definition
- Semantic Naming: Tool names should be explicit and verb-oriented (e.g.,
get_schedule_by_date,query_order_status). Avoid ambiguous identifiers liketool1ordo_action. Language models inspect tool names to determine semantic relevance during planning. - Descriptive Parameter Hints: Always call
.describe()on Zod properties. The model reads these descriptions to construct valid arguments. If an ISO format or specific date structure is expected, specify it in the description. - Structured Protocol Returns: The MCP specification requires tools to return an object containing a
contentarray. Each item in the array must declare atype(such as"text"or"image"). For structured data, serialize it as clean JSON text.
4. Context Hygiene: Cleaning Raw API Outputs
A common failure mode when building custom MCP servers is dumping raw third-party API responses straight back to the model:
// ❌ ANTI-PATTERN: Dumping raw API responses into prompt context
const rawApiResponse = await externalClient.getCalendarData(date);
return {
content: [{ type: "text", text: JSON.stringify(rawApiResponse) }]
};
Third-party APIs frequently return bloated payloads with deep internal IDs, link relations, pagination tokens, and debug metadata. Dumping 50 KB of raw JSON for a simple question drains the model’s context window and triggers hallucination.
Instead, project and filter only the fields the agent needs:
// ✅ RECOMMENDED: Project and sanitize fields for LLM consumption
async function fetchEventsForDate(targetDate) {
// Query internal service or upstream API
const rawEvents = await calendarService.listEvents({ date: targetDate });
if (!rawEvents || rawEvents.length === 0) {
return { date: targetDate, summary: "No events scheduled", count: 0, items: [] };
}
// Filter out redundant metadata
const sanitizedItems = rawEvents.map(event => ({
title: event.summary,
start: event.start.dateTime || event.start.date,
end: event.end.dateTime || event.end.date,
attendeesCount: event.attendees ? event.attendees.length : 0,
}));
return {
date: targetDate,
count: sanitizedItems.length,
items: sanitizedItems,
};
}
This simple transformation reduces context consumption by over 85% while delivering cleaner signal to the model.
5. Standard I/O Transport & The Stdio Rule
MCP supports multiple communication transports. For local servers running on developer machines, the standard mechanism is Standard Input/Output (stdio). The host process spawns the server as a child process and transmits JSON-RPC frames over stdin and stdout.
To connect the transport:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error("Fatal server error:", err);
process.exit(1);
});
[!CAUTION] The Stdio Logging Rule: In stdio mode,
stdoutis strictly reserved for JSON-RPC protocol frames. Never invokeconsole.log()inside an MCP server communicating over stdio!If you log text with
console.log(), that plain text will be injected into stdout, corrupting the JSON-RPC framing and causing the host client to immediately disconnect. Always useconsole.error()for internal debugging logs—stderris captured by host environments without affecting protocol communication.
6. Wiring into Cursor and Claude Desktop
Once your server script is ready, connect it to your local agent environment.
Configuring Cursor
In Cursor, open Settings > Features > MCP, or directly edit the project-level .cursor/mcp.json file:
{
"mcpServers": {
"calendar-tools": {
"command": "node",
"args": ["/Users/username/projects/my-mcp-server/server.js"],
"env": {
"CALENDAR_API_KEY": "your_secure_api_key_here",
"CALENDAR_ID": "primary"
}
}
}
}
Ensure the path to server.js is an absolute path. After saving, check the MCP servers list in Cursor; the status indicator should turn green.
Configuring Claude Desktop
For Claude Desktop, add the same configuration block to ~/Library/Application Support/Claude/claude_desktop_config.json on macOS (or %APPDATA%\Claude\claude_desktop_config.json on Windows).
{
"mcpServers": {
"calendar-tools": {
"command": "node",
"args": ["/Users/username/projects/my-mcp-server/server.js"]
}
}
}
7. Interactive Testing with MCP Inspector
Restarting your IDE every time you modify tool logic quickly becomes tedious. Anthropic provides a dedicated graphical testing tool called MCP Inspector:
npx @modelcontextprotocol/inspector node server.js
This launches a local web interface (typically at http://localhost:5173) that connects directly to your server process:
- View all advertised tools, input schemas, and parameter types.
- Execute tool calls manually with mock parameters and inspect the returned JSON-RPC frames.
- Verify error-handling paths and edge cases (e.g., malformed dates or network timeouts) before deploying to your editor.
8. Summary Checklist for Production TypeScript Servers
Before deploying your TypeScript MCP server into active workflows, verify the following checklist:
| Verification Item | Requirement |
|---|---|
| Zero stdout pollution | All debug logs use console.error(); console.log() is strictly prohibited. |
| Strict Zod validation | Every tool parameter has explicit types and semantic .describe() documentation. |
| Context hygiene | Raw upstream responses are mapped and filtered before returning to the model. |
| Graceful error boundaries | Errors return isError: true with human-readable messages instead of unhandled promise rejections. |
| Absolute paths | Client configs reference fully qualified script paths to prevent working-directory resolution issues. |
By mastering the TypeScript MCP SDK, you can quickly convert private company databases, internal APIs, and specialized scripts into reliable, autonomous capabilities for modern AI development environments.