Official TypeScript SDK Fundamentals
Deep dive into @modelcontextprotocol/sdk — learn the McpServer class, tool/resource/prompt registration, StdioServerTransport, and Zod schema validation for building production-grade MCP servers.
title: "Official TypeScript SDK Fundamentals" description: "Deep dive into @modelcontextprotocol/sdk — learn the McpServer class, tool/resource/prompt registration, StdioServerTransport, and Zod schema validation for building production-grade MCP servers." order: 9 level: "intermediate" duration: "25 min" keywords:
- "MCP TypeScript SDK"
- "McpServer class"
- "StdioServerTransport"
- "Zod schema validation MCP"
- "mcp-framework vs SDK"
- "@modelcontextprotocol/sdk tutorial"
- "MCP tool registration"
- "MCP resource registration"
- "MCP prompt registration" date: "2026-04-01"
The official TypeScript SDK (@modelcontextprotocol/sdk) gives you full control over every aspect of your MCP server. This lesson covers the McpServer class, how to register tools, resources, and prompts with Zod schemas, and the transport layer that connects your server to AI clients. If you started with mcp-framework for rapid scaffolding, the SDK is your next step for fine-grained control.
Why Learn the Official SDK?
The Model Context Protocol ecosystem offers two primary paths for TypeScript developers. mcp-framework provides a high-level, convention-based approach with automatic discovery and CLI scaffolding. The official TypeScript SDK (@modelcontextprotocol/sdk) gives you direct, low-level access to the protocol itself.
| Feature | mcp-framework | @modelcontextprotocol/sdk |
|---|---|---|
| Setup | CLI scaffolding, conventions | Manual wiring, full control |
| Tool registration | Class-based, auto-discovered | Functional, explicit registration |
| Transport | Auto-configured | Manual transport setup |
| Schema validation | Built-in Zod integration | Explicit Zod schemas |
| Best for | Rapid prototyping, standard servers | Custom behavior, advanced patterns |
Understanding both approaches makes you a well-rounded MCP developer. Many production systems use the SDK directly for maximum flexibility, while mcp-framework remains excellent for getting started quickly. For a detailed side-by-side comparison with code examples, see mcp-framework vs TypeScript SDK.
Installing the SDK
Create a new project
mkdir my-sdk-server && cd my-sdk-server
npm init -y
Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Configure TypeScript
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
The McpServer Class
The central class in the official TypeScript SDK that manages your MCP server lifecycle. It handles capability negotiation, request routing, and provides methods for registering tools, resources, and prompts.
The McpServer class is the heart of every SDK-based server. Here is a minimal server that demonstrates the core structure:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
// Register tools, resources, and prompts here...
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");
}
main().catch(console.error);
The SDK uses deep imports like @modelcontextprotocol/sdk/server/mcp.js. Always include the .js extension — this is required for Node16 module resolution even in TypeScript files.
Registering Tools with Zod Schemas
Tools are the most commonly used MCP primitive. The SDK uses Zod for runtime schema validation, ensuring type safety at both compile time and runtime.
import { z } from "zod";
server.tool(
"search-documents",
"Search through indexed documents by keyword",
{
query: z.string().describe("The search query"),
limit: z.number().min(1).max(100).default(10)
.describe("Maximum number of results"),
category: z.enum(["all", "articles", "docs", "code"])
.default("all")
.describe("Category filter"),
},
async ({ query, limit, category }) => {
// Your implementation here
const results = await searchIndex(query, { limit, category });
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
);
The server.tool() method accepts four arguments:
- Name — a unique identifier for the tool
- Description — a human-readable explanation (used by AI models to decide when to call the tool)
- Schema — a Zod object defining input parameters
- Handler — an async function that receives validated inputs and returns MCP content
Every Zod field should include .describe() with a clear explanation. AI models rely on these descriptions to understand how to use your tools correctly. Vague descriptions lead to incorrect tool calls.
Tool Response Format
Tool handlers must return an object with a content array. Each item in the array has a type field:
// Text response
return {
content: [{ type: "text", text: "Operation completed successfully" }],
};
// Image response
return {
content: [{
type: "image",
data: base64EncodedImage,
mimeType: "image/png",
}],
};
// Multiple content items
return {
content: [
{ type: "text", text: "Found 3 matching files:" },
{ type: "text", text: fileList },
],
};
Registering Resources
Resources expose data that AI models can read. Unlike tools (which perform actions), resources provide context.
// Static resource
server.resource(
"config",
"app://config/main",
{ description: "Application configuration" },
async () => ({
contents: [
{
uri: "app://config/main",
text: JSON.stringify(appConfig, null, 2),
mimeType: "application/json",
},
],
})
);
Every MCP resource is identified by a URI. The scheme (e.g., app://, file://, db://) is arbitrary but should be meaningful. URIs must be unique within a server.
Resource Templates
For dynamic resources, use URI templates with placeholders:
server.resource(
"user-profile",
"users://{userId}/profile",
{ description: "User profile by ID" },
async (uri, { userId }) => {
const user = await db.users.findById(userId);
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(user),
mimeType: "application/json",
},
],
};
}
);
Registering Prompts
Prompts are reusable templates that help AI models perform specific tasks with your server:
server.prompt(
"analyze-code",
"Analyze code for potential issues and improvements",
{
language: z.string().describe("Programming language"),
focus: z.enum(["security", "performance", "readability", "all"])
.default("all")
.describe("Analysis focus area"),
},
async ({ language, focus }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Analyze the following ${language} code with a focus on ${focus}. Identify issues, suggest improvements, and explain your reasoning.`,
},
},
],
})
);
Understanding Transports
The communication layer between an MCP client and server. Transports handle message serialization, delivery, and connection lifecycle. The protocol is transport-agnostic — the same server logic works over stdio, SSE, or HTTP.
StdioServerTransport
The most common transport for local development and desktop AI clients like Claude Desktop and Cursor:
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
Stdio transport communicates over standard input/output streams. This is the default for local MCP servers because desktop clients can spawn your server as a child process.
When using StdioServerTransport, avoid console.log() — it writes to stdout which is reserved for MCP protocol messages. Use console.error() for debug logging instead, as stderr is separate from the protocol channel.
SSE Transport
For remote servers accessible over HTTP:
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
// Handle incoming messages
});
app.listen(3001);
Streamable HTTP Transport
The newest transport option, designed for modern deployments:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);
| Transport | Use Case | Pros | Cons |
|---|---|---|---|
| stdio | Local desktop clients | Simple, no network config | Local only |
| SSE | Remote servers, web clients | HTTP-based, firewall friendly | Unidirectional without POST endpoint |
| Streamable HTTP | Modern cloud deployments | Stateless, scalable | Newer, less ecosystem support |
Complete Example: A File Search Server
Here is a full example combining tools, resources, and prompts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
const server = new McpServer({
name: "file-search-server",
version: "1.0.0",
});
// Tool: search files
server.tool(
"search-files",
"Search for files matching a pattern in the workspace",
{
pattern: z.string().describe("Glob pattern to match files"),
directory: z.string().default(".")
.describe("Directory to search in"),
},
async ({ pattern, directory }) => {
const { glob } = await import("glob");
const files = await glob(pattern, { cwd: directory });
return {
content: [{
type: "text",
text: files.length > 0
? files.join("\n")
: "No files found matching the pattern.",
}],
};
}
);
// Resource: workspace info
server.resource(
"workspace-info",
"workspace://info",
{ description: "Current workspace information" },
async () => {
const pkg = JSON.parse(
await fs.readFile("package.json", "utf-8")
);
return {
contents: [{
uri: "workspace://info",
text: JSON.stringify({
name: pkg.name,
version: pkg.version,
dependencies: Object.keys(pkg.dependencies || {}),
}, null, 2),
mimeType: "application/json",
}],
};
}
);
// Prompt: code review
server.prompt(
"review-file",
"Review a specific file for code quality",
{
filepath: z.string().describe("Path to the file to review"),
},
async ({ filepath }) => {
const content = await fs.readFile(filepath, "utf-8");
return {
messages: [{
role: "user",
content: {
type: "text",
text: `Please review the following file (${filepath}) for code quality, potential bugs, and improvements:\n\n\`\`\`\n${content}\n\`\`\``,
},
}],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("File search server running");
}
main().catch(console.error);
SDK vs mcp-framework: When to Use Each
Use mcp-framework when you want rapid scaffolding, convention-based structure, and CLI tooling. Use the official SDK when you need custom transport logic, fine-grained lifecycle control, or are building a non-standard server architecture. Many teams start with mcp-framework and migrate specific servers to the SDK as requirements grow.
The mcp-framework equivalent of the tool above would use a class-based approach with automatic discovery:
// In mcp-framework — class-based, auto-discovered
import { MCPTool } from "mcp-framework";
import { z } from "zod";
class SearchFilesTool extends MCPTool<{ pattern: string; directory: string }> {
name = "search-files";
description = "Search for files matching a pattern";
schema = {
pattern: { type: z.string(), description: "Glob pattern" },
directory: { type: z.string().default("."), description: "Directory" },
};
async execute({ pattern, directory }: { pattern: string; directory: string }) {
const { glob } = await import("glob");
const files = await glob(pattern, { cwd: directory });
return files.join("\n") || "No files found.";
}
}
export default SearchFilesTool;
Both approaches produce fully compliant MCP servers. The SDK approach gives you more control over the server lifecycle, while mcp-framework reduces boilerplate.
Next Steps
Now that you understand the SDK fundamentals, you are ready to explore advanced patterns:
- Advanced Tool Patterns — complex schemas, async operations, and error handling
- Dynamic Resources & Templates — URI templates and subscriptions
- Transport Protocols Deep Dive — choosing and configuring transports