Chapter 2 of 8
Building MCP Servers from Scratch
MCP Server architecture and protocol, developing a custom tool server with TypeScript, and integrating with Claude Code, Cline, and Cursor.
The Model Context Protocol (MCP) is an open standard (spec at modelcontextprotocol.io) that lets LLM applications connect to external tools, data sources, and services through a unified interface. Think of it as USB-C for AI: a single protocol that any client (Claude Code, Cline, Cursor) can use to talk to any server (a database query tool, a file system bridge, a custom API wrapper). This tutorial covers MCP architecture end to end — from the JSON-RPC protocol layer through building a production-ready tool server in TypeScript to wiring it up with the three most popular MCP clients.
Why Build an MCP Server?
Before MCP, every AI coding tool invented its own plugin API. Claude Code had hooks, Cursor had extensions, Cline had its own tool format. An integration you built for one tool was useless in another. MCP solves this by standardizing three primitives:
| Primitive | Purpose | Analogy |
|---|---|---|
| Tools | Functions the LLM can invoke | REST API endpoints |
| Resources | Data the LLM can read | GET endpoints returning context |
| Prompts | Reusable prompt templates | Stored procedures for LLM calls |
Build one MCP server and it works with every MCP-compatible client. That is the architectural payoff.
MCP Protocol Architecture
MCP uses a client-server model with three roles: the host (LLM application like Claude Code), the client (connector within the host that manages individual server connections), and the server (your tool service). The protocol itself is JSON-RPC 2.0, originally published by Anthropic in November 2024 and now maintained as an open specification at modelcontextprotocol.io (latest version: 2025-06-18).
Transport Layer
The transport layer determines how client and server exchange messages. The 2025-06-18 specification defines two standardized transports plus a deprecated legacy transport:
Transport Use Case Direction
───────────────── ────────────────────────── ─────────────────
stdio Local CLI tools, Claude Code Bidirectional over stdin/stdout
Streamable HTTP Production servers, cloud Single HTTP endpoint, optional
deployments, remote access SSE for server-push, sessions
SSE (deprecated) Legacy browser-based clients Separate GET+POST endpoints
Stdio is the simplest and most common for development. The client spawns the server as a child process and communicates over stdin/stdout. This is what Claude Code, Claude Desktop, and Cline use for local tool servers.
SSE (from the original 2024-11-05 spec) uses a GET endpoint for server-to-client streaming and separate HTTP POST requests for client-to-server messages. It is deprecated in the 2025-06-18 specification. Existing SSE servers continue to work, but new implementations should use Streamable HTTP instead.
Streamable HTTP is the production-grade transport. A single HTTP endpoint handles all message types. Clients send JSON-RPC via POST; the server optionally uses SSE for streaming responses. It supports session management via Mcp-Session-Id headers, resumable streams via Last-Event-ID, and works behind standard load balancers. This is the recommended transport for any server exposed over the network.
Message Format
All MCP communication uses JSON-RPC 2.0. A tool invocation looks like this:
// Client sends a tools/call request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_files",
"arguments": {
"pattern": "*.ts",
"directory": "/src"
}
}
}
// Server responds with the result
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Found 23 files matching *.ts in /src"
}
]
}
}
Lifecycle
Every MCP connection follows a three-phase lifecycle:
- Initialize — client sends
initializewith its capabilities and protocol version; server responds with its own capabilities - Operate — bidirectional exchange of tools/call, resources/read, prompts/get, and notification messages
- Shutdown — either side sends a termination notification and the transport closes
The server advertises its capabilities during initialization. This is how a client discovers what tools, resources, and prompts are available without prior configuration.
Client-Side Capabilities
The 2025-06-18 specification also defines three capabilities that clients expose to servers, enabling advanced bidirectional patterns:
| Capability | Purpose | When to Use |
|---|---|---|
| Sampling | Server requests LLM completions through the client | Tools that need language understanding (summarization, explanation) without their own API key |
| Roots | Server discovers the client's filesystem/workspace boundaries | File system tools that need to know which directories are accessible |
| Elicitation | Server prompts the user for additional structured input | Tools that need interactive clarification before proceeding |
These capabilities enable recursive patterns — the LLM calls a tool, the tool calls back to the LLM via sampling, the LLM processes the result. This tutorial focuses on the server-side primitives (tools, resources, prompts), which are the foundation for most MCP servers.
Setting Up the TypeScript Project
Let's build a practical MCP server: a code analysis tool that provides file search, dependency inspection, and code metrics. This server will work with Claude Code, Cline, and Cursor identically.
mkdir mcp-code-tools && cd mcp-code-tools
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
The @modelcontextprotocol/sdk package (from github.com/modelcontextprotocol/typescript-sdk) provides the server framework. zod handles input validation schemas — MCP tool parameters must be defined as JSON Schema, and zod maps directly to it.
Initialize TypeScript:
npx tsc --init --target ES2022 --module Node16 \
--moduleResolution Node16 --outDir dist --rootDir src
Create the source directory:
mkdir -p src
Building the Server: Core Structure
The foundation of any MCP server is a transport paired with an McpServer instance. Create src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create the server instance with metadata
const server = new McpServer({
name: "code-tools",
version: "1.0.0",
description: "Code analysis tools for file search, dependency inspection, and code metrics"
});
// Register tools, resources, and prompts here (next sections)
// Start the stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Code Tools MCP server running on stdio");
}
main().catch((error) => {
console.error("Server failed to start:", error);
process.exit(1);
});
Key details: StdioServerTransport reads JSON-RPC messages from stdin and writes responses to stdout. All logging must go to stderr (console.error) — stdout is reserved for the protocol. This is the single most common mistake new MCP server developers make.
Add the run script to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
}
}
Registering Tools
Tools are the core primitive — they are callable functions that the LLM invokes when it needs to perform an action. Register them using server.tool(), which takes a name, description, a zod schema for parameters, and an implementation function.
Add these tool registrations to src/index.ts before the main() function:
Tool 1: File Search
server.tool(
"search_files",
"Search for files matching a glob pattern in a directory tree. Returns file paths and sizes.",
{
pattern: z.string().describe("Glob pattern to match (e.g., '*.ts', 'src/**/*.test.ts')"),
directory: z.string().describe("Root directory to search in"),
maxResults: z.number().optional().default(50).describe("Maximum number of results to return")
},
async ({ pattern, directory, maxResults }) => {
const { execSync } = await import("child_process");
const { readdirSync, statSync } = await import("fs");
const { join } = await import("path");
try {
const results: Array<{ path: string; size: number }> = [];
function walk(dir: string): void {
if (results.length >= maxResults) return;
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.name.endsWith(pattern.replace("*", "")) ||
entry.name.includes(pattern.replace("*", ""))) {
results.push({
path: fullPath,
size: statSync(fullPath).size
});
if (results.length >= maxResults) return;
}
}
}
walk(directory);
return {
content: [{
type: "text",
text: JSON.stringify({
pattern,
directory,
count: results.length,
files: results
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error searching files: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
Tool 2: Dependency Inspector
server.tool(
"inspect_dependencies",
"Analyze package.json dependencies and return version information, update availability, and dependency counts.",
{
directory: z.string().describe("Path to the project directory containing package.json")
},
async ({ directory }) => {
const { readFileSync } = await import("fs");
const { join } = await import("path");
try {
const pkgPath = join(directory, "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const analysis = {
name: pkg.name,
version: pkg.version,
dependencyCount: Object.keys(deps).length,
devDependencyCount: Object.keys(devDeps).length,
totalDependencies: Object.keys(deps).length + Object.keys(devDeps).length,
dependencies: deps,
devDependencies: devDeps
};
return {
content: [{
type: "text",
text: JSON.stringify(analysis, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error reading package.json: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
Tool 3: Code Metrics
server.tool(
"code_metrics",
"Calculate code metrics for TypeScript/JavaScript files: line count, function count, and complexity estimate.",
{
filePath: z.string().describe("Path to the source file to analyze")
},
async ({ filePath }) => {
const { readFileSync } = await import("fs");
try {
const source = readFileSync(filePath, "utf-8");
const lines = source.split("\n");
const codeLines = lines.filter(line => {
const trimmed = line.trim();
return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("*");
});
const functionMatches = source.match(
/(?:function\s+\w+|(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>)/g
);
const functionCount = functionMatches ? functionMatches.length : 0;
const complexityKeywords = (source.match(/\b(if|else|for|while|case|catch|\?\?|&&|\|\|)\b/g) || []).length;
return {
content: [{
type: "text",
text: JSON.stringify({
file: filePath,
totalLines: lines.length,
codeLines: codeLines.length,
commentLines: lines.length - codeLines.length,
functionCount,
estimatedComplexity: complexityKeywords,
linesPerFunction: functionCount > 0 ? Math.round(codeLines.length / functionCount) : codeLines.length
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error analyzing file: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
The isError: true flag in the return value is critical — it tells the LLM client that the tool invocation failed, so the model can handle the error gracefully (retry, fall back, or report to the user) rather than treating garbage output as a successful result.
Registering Resources
Resources provide read-only data that the LLM can access on demand. They are similar to GET endpoints — the client requests a resource URI and the server returns its content. Resources are ideal for exposing configuration files, environment metadata, or computed context.
import { z } from "zod";
server.resource(
"project-info",
"code-tools://project-info",
"Returns metadata about the currently configured project workspace",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
server: "code-tools",
version: "1.0.0",
capabilities: ["file_search", "dependency_inspection", "code_metrics"],
supportedPatterns: ["*.ts", "*.js", "*.tsx", "*.jsx", "*.json"]
}, null, 2)
}]
})
);
server.resource(
"file-content",
"code-tools://file/{path}",
"Read the content of a file from the workspace",
async (uri) => {
const { readFileSync } = await import("fs");
const filePath = uri.pathname.replace(/^\//, "");
try {
const content = readFileSync(filePath, "utf-8");
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: content
}]
};
} catch (error) {
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: `Error reading file: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
The resource URI template code-tools://file/{path} uses URI template syntax. The MCP client can list available resources and their URI templates, then request specific ones by filling in the template variables.
Registering Prompts
Prompts are reusable templates that the LLM client can load on demand. They are useful for encoding common workflows — for example, a standardized code review prompt that uses the tools we just registered.
server.prompt(
"code-review",
"Generate a comprehensive code review for a file using code metrics and dependency data.",
{
filePath: z.string().describe("Path to the file to review"),
focusArea: z.enum(["performance", "readability", "security", "all"]).optional()
.default("all").describe("Specific area to focus the review on")
},
async ({ filePath, focusArea }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: [
`Review the file at ${filePath}.`,
focusArea !== "all" ? `Focus specifically on ${focusArea}.` : "Cover all aspects.",
"",
"Steps:",
"1. Use code_metrics to get the file's metrics",
"2. Read the file content",
"3. Analyze the code for issues related to the focus area",
"4. If the focus is 'security', check for common vulnerabilities",
"5. If the focus is 'performance', identify potential bottlenecks",
"6. Provide specific, actionable recommendations"
].join("\n")
}
}
]
})
);
When a user selects this prompt in their MCP client, the pre-formatted instructions are injected into the conversation, guiding the LLM to use the server's own tools in the right sequence.
Error Handling and Validation
Robust error handling separates production MCP servers from toy examples. The TypeScript SDK provides structured error types:
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
// In any tool handler:
server.tool(
"safe_operation",
"An operation with structured error handling.",
{ input: z.string() },
async ({ input }) => {
// Validation errors
if (input.length > 10000) {
throw new McpError(
ErrorCode.InvalidParams,
"Input exceeds maximum length of 10000 characters"
);
}
try {
// Business logic here
return { content: [{ type: "text", text: "Success" }] };
} catch (error) {
// Wrap unexpected errors with context
throw new McpError(
ErrorCode.InternalError,
`Operation failed: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
Use McpError with proper error codes for recoverable errors (invalid params, resource not found). Use isError: true in the return value for expected failure modes where the LLM should see the error message rather than an exception trace.
Adding Streamable HTTP Transport
For production deployments where the server runs remotely, add the Streamable HTTP transport alongside stdio:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// Add after the main() function
async function startHttpServer() {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: crypto.randomUUID });
await server.connect(transport);
// Express.js integration example
const express = await import("express");
const app = (0, express.default)();
app.post("/mcp", (req, res) => {
// Express passes the raw request to the transport
});
app.listen(3000, () => {
console.error("Code Tools MCP server running on HTTP at port 3000");
});
}
The Streamable HTTP transport lets you deploy the same server behind a load balancer with TLS termination, health checks, and all the infrastructure that production services require. Clients discover the server via its HTTP endpoint URL instead of a local command.
Testing the Server
Before connecting to a client, verify the server works in isolation using the MCP Inspector tool included in the SDK:
npx @modelcontextprotocol/inspector node dist/index.js
This launches a web UI at http://localhost:6274 that connects to your server over stdio and lets you manually invoke tools, read resources, and load prompts. It is the fastest way to debug tool definitions and parameter handling.
For automated tests:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function testServer() {
const transport = new StdioClientTransport({
command: "node",
args: ["dist/index.js"]
});
const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(transport);
// List available tools
const tools = await client.listTools();
console.log("Available tools:", tools.tools.map(t => t.name));
// Call a tool
const result = await client.callTool({
name: "code_metrics",
arguments: { filePath: "./src/index.ts" }
});
console.log("Metrics result:", result.content[0].text);
// Disconnect
await client.close();
}
testServer();
Integration: Claude Code
Claude Code configures MCP servers in .mcp.json at the project root (committed to git for team sharing), or via claude mcp add commands for user-scoped servers stored in ~/.claude.json:
{
"mcpServers": {
"code-tools": {
"command": "node",
"args": ["/absolute/path/to/mcp-code-tools/dist/index.js"],
"env": {
"NODE_ENV": "production"
}
}
}
}
For a remote server using Streamable HTTP:
{
"mcpServers": {
"code-tools-remote": {
"type": "http",
"url": "https://your-server.example.com/mcp",
"headers": {
"Authorization": "Bearer ${MCP_API_KEY}"
}
}
}
}
After adding the configuration, restart Claude Code or run /mcp to verify the server is connected. The tools appear in Claude Code's tool list and the model can invoke them automatically during loop engineering tasks — for example, using code_metrics inside a test-driven loop to verify that a refactoring actually reduced complexity.
You can also manage servers from the CLI:
# Add a stdio server
claude mcp add code-tools -- node /path/to/dist/index.js
# Add a remote HTTP server
claude mcp add --transport http code-tools-remote https://your-server.example.com/mcp
# List all configured servers
claude mcp list
# Remove a server
claude mcp remove code-tools
Integration: Cline
Cline (github.com/cline/cline, formerly Claude Dev) configures MCP servers through its MCP settings panel. Open the Cline panel in VS Code, click the MCP Servers icon, navigate to Configure, and add:
{
"mcpServers": {
"code-tools": {
"command": "node",
"args": ["/absolute/path/to/mcp-code-tools/dist/index.js"],
"env": {
"NODE_ENV": "production"
},
"disabled": false,
"autoApprove": []
}
}
}
This configuration is stored in ~/.cline/mcp.json for CLI usage or through the extension's settings for the IDE. Cline spawns the server process when a session starts and connects automatically. The tools are available to Cline's agent just like its built-in file editing and terminal tools. The autoApprove array lets you specify tool names that skip the user confirmation dialog.
For remote servers, Cline uses the Streamable HTTP transport:
{
"mcpServers": {
"code-tools-remote": {
"type": "streamableHttp",
"url": "https://your-server.example.com/mcp",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}
Integration: Cursor
Cursor configures MCP servers in .cursor/mcp.json at the project root (committed to git for team sharing), or in ~/.cursor/mcp.json for global access:
{
"mcpServers": {
"code-tools": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/mcp-code-tools/dist/index.js"],
"env": {
"NODE_ENV": "production",
"API_KEY": "${env:MCP_API_KEY}"
}
}
}
}
Place this file in your project's root directory and commit it to version control — team members get the MCP server configuration automatically. Cursor detects the file and connects to the server on workspace load. The tools become available in Cursor's Agent mode and Cmd+K inline editing.
Cursor supports variable interpolation in config values: ${env:NAME} for environment variables, ${workspaceFolder} for the project root, ${userHome} for the home directory, and ${pathSeparator} for the OS path separator.
For remote servers, Cursor infers the transport type from the URL:
{
"mcpServers": {
"code-tools-remote": {
"url": "https://your-server.example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:MCP_API_KEY}"
}
}
}
}
MCP Ecosystem Overview
The MCP ecosystem has grown rapidly since the protocol's public release. Here is the current landscape as of mid-2026:
Official Servers (from Anthropic)
| Server | Purpose | Transport |
|---|---|---|
| filesystem | File and directory operations | stdio |
| github | GitHub API: issues, PRs, repos | stdio |
| postgres | PostgreSQL database queries | stdio |
| sqlite | SQLite database queries | stdio |
| puppeteer | Browser automation and screenshots | stdio |
| brave-search | Web search via Brave API | stdio |
| google-maps | Location and directions via Google Maps | stdio |
| memory | Persistent knowledge graph storage | stdio |
Community Servers
The community has built hundreds of MCP servers. Notable categories:
- Database connectors — MySQL, MongoDB, Redis, Supabase, Neon, PlanetScale
- Cloud platforms — AWS, GCP, Azure, Vercel, Cloudflare, Railway
- Dev tools — Docker, Kubernetes, Terraform, Jenkins, GitHub Actions
- Communication — Slack, Discord, Notion, Linear, Jira
- AI/ML — Hugging Face, LangChain, LlamaIndex, Weaviate, Pinecone
The full registry is at github.com/modelcontextprotocol/servers.
MCP-Compatible Clients
| Client | Type | Transport Support | Best For |
|---|---|---|---|
| Claude Code | CLI | stdio, HTTP, SSE, WebSocket | Loop engineering, autonomous coding |
| Claude Desktop | Desktop app | stdio, SSE | General-purpose AI assistant |
| Cline | VS Code extension | stdio, Streamable HTTP | IDE-integrated AI coding |
| Cursor | IDE | stdio, SSE, Streamable HTTP | AI-powered editing with agent mode |
| Windsurf | IDE | stdio | Cascade multi-step execution |
| Continue | VS Code extension | stdio | Open-source AI coding assistant |
| Zed | Editor | stdio | High-performance AI editing |
| Goose | CLI/Desktop (Block) | stdio | Open-source AI coding agent |
Advanced Patterns
Progressive Tool Discovery
Rather than registering all tools at startup, implement lazy loading based on the client's declared capabilities:
// Only register advanced tools if the client supports them
server.tool(
"advanced_refactor",
"Perform complex AST-based refactoring (requires sampling support).",
{ /* schema */ },
async (params) => { /* implementation */ },
{ /* optional annotations */ }
);
Sampling for LLM-Augmented Tools
MCP's sampling capability lets the server itself call back to the LLM during tool execution. This enables tools that need language understanding — for example, a code explanation tool that asks the LLM to summarize a function:
// Inside a tool handler:
const summary = await server.request(
{ method: "sampling/createMessage" },
{
messages: [{ role: "user", content: `Explain this function in one sentence:\n${code}` }],
maxTokens: 100
}
);
This creates a recursive loop — the LLM calls a tool, the tool calls the LLM, the LLM processes the result. It is a powerful pattern for tools that bridge raw data and human-readable analysis.
Batching and Concurrency
For tools that process multiple files, implement batching to avoid overwhelming the client:
server.tool(
"batch_metrics",
"Calculate code metrics for multiple files at once.",
{
files: z.array(z.string()).max(20).describe("Array of file paths (max 20)")
},
async ({ files }) => {
const results = await Promise.allSettled(
files.map(f => analyzeFile(f))
);
// Return aggregated results
}
);
Limit batch sizes (hard cap at 20 files in this example) and use Promise.allSettled so one file failure does not abort the entire batch.
Security Considerations
MCP servers run with the same permissions as the process that spawns them. A stdio server has the full filesystem and network access of the user's shell. Apply these principles:
- Validate all inputs — use zod schemas with strict types; never pass raw user input to
execSync,eval, or file paths - Sandbox file access — restrict tools to a configurable root directory; reject paths containing
..or absolute paths outside the workspace - Limit resource consumption — set maximum file sizes, result lengths, and batch counts
- Log and audit — write structured logs (to stderr) with timestamps and request IDs
- Network hygiene — if your server makes outbound HTTP requests, validate URLs and set timeouts
- Secrets management — pass API keys through environment variables, never hardcode them
// Example: path validation utility
function safePath(baseDir: string, targetPath: string): string {
const { resolve, relative } = require("path");
const resolved = resolve(baseDir, targetPath);
const rel = relative(baseDir, resolved);
if (rel.startsWith("..") || resolve(rel) !== resolved) {
throw new McpError(ErrorCode.InvalidParams, "Path escapes workspace root");
}
return resolved;
}
Complete Project Structure
After completing this tutorial, your server project should look like this:
mcp-code-tools/
package.json
tsconfig.json
src/
index.ts # Server setup, transport, main entry point
tools/
search.ts # search_files tool
dependencies.ts # inspect_dependencies tool
metrics.ts # code_metrics tool
resources/
project.ts # project-info resource
files.ts # file-content resource
prompts/
review.ts # code-review prompt
utils/
validation.ts # Path validation, input sanitization
dist/ # Compiled output
Splitting tools, resources, and prompts into separate modules keeps the codebase maintainable as the server grows. Each module exports a registration function that takes the McpServer instance:
// src/tools/metrics.ts
export function registerCodeMetricsTool(server: McpServer): void {
server.tool("code_metrics", /* ... */);
}
// src/index.ts
import { registerCodeMetricsTool } from "./tools/metrics.js";
import { registerSearchTool } from "./tools/search.js";
registerCodeMetricsTool(server);
registerSearchTool(server);
Key Takeaways
- MCP standardizes AI-tool communication — one protocol, two standardized transports (stdio, Streamable HTTP), works with every compatible client
- Three server-side primitives — tools (callable functions), resources (read-only data), prompts (reusable templates) cover the full range of LLM interactions
- Three client-side capabilities — sampling (LLM callbacks), roots (workspace discovery), elicitation (interactive user input) enable advanced bidirectional patterns
- The TypeScript SDK (
@modelcontextprotocol/sdkv1.x) providesMcpServer, transport classes, and zod integration for type-safe tool definitions; a v2 SDK with split packages (@modelcontextprotocol/server,@modelcontextprotocol/client) is in development for Q3 2026 - Stdio is for local development, Streamable HTTP is for production — build once, deploy either way
- One server, every client — the same
code-toolsserver works identically in Claude Code, Cline, and Cursor with minimal configuration changes - Test with MCP Inspector —
npx @modelcontextprotocol/inspectorprovides an interactive web UI for debugging tool calls - Security is your responsibility — validate inputs, sandbox paths, limit resources, and never trust client-provided data blindly
- The ecosystem is large and growing — official servers cover common needs, community servers extend into every domain, and the client list spans CLI tools, IDEs, and desktop apps