Mcp server dev
Build MCP (Model Context Protocol) servers in TypeScript with @modelcontextprotocol/sdk. Use when writing, reviewing, or refactoring MCP server code: (1) Creating MCP servers with McpServer, (2) Registering tools with registerTool, inputSchema, outputSchema, Zod validation, (3) Defining resources and resource templates, (4) Defining prompts with arguments, (5) Transports: StdioServerTransport, NodeStreamableHTTPServerTransport, SSE, (6) Tool annotations (readOnlyHint, destructiveHint, idempotentHint), (7) Error handling and isError responses, (8) Dynamic tool loading and tool list change notifications, (9) Middleware patterns for MCP tools, (10) Testing MCP servers with vitest, (11) Publishing and configuring for Claude Code, Cursor, Windsurf, (12) Any @modelcontextprotocol/sdk imports.From its SKILL.md
npx -y skills add arthjean/skills --skill mcp-server-devAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.9 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
MCP Server Development — TypeScript SDK
What is MCP
The Model Context Protocol is an open standard for connecting AI assistants (Claude, Cursor, etc.) to external tools and data. An MCP server exposes tools, resources, and prompts that clients can discover and invoke.
Quick Setup
bun init
bun add @modelcontextprotocol/sdk zod
// src/index.ts
import { McpServer } from '@modelcontextprotocol/server'
import { StdioServerTransport } from '@modelcontextprotocol/node'
import * as z from 'zod/v4'
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
})
// Register a tool
server.registerTool(
'greet',
{
description: 'Greet a user by name',
inputSchema: z.object({
name: z.string().describe('Name of the person to greet'),
}),
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }],
})
)
// Start server with stdio transport
const transport = new StdioServerTransport()
await server.connect(transport)
// package.json essentials
{
"type": "module",
"bin": { "my-mcp-server": "./dist/index.js" },
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Three Primitives
| Primitive | Purpose | Client Action | Example |
|---|---|---|---|
| Tools | Execute actions, return results | LLM decides when to call | API calls, calculations, file ops |
| Resources | Expose read-only data | User selects, attached to context | Files, DB records, API data |
| Prompts | Reusable prompt templates | User selects from menu | Code review template, debug prompt |
Registering Tools
import * as z from 'zod/v4'
// Basic tool
server.registerTool(
'calculate-bmi',
{
title: 'BMI Calculator',
description: 'Calculate Body Mass Index from weight and height',
inputSchema: z.object({
weightKg: z.number().describe('Weight in kilograms'),
heightM: z.number().positive().describe('Height in meters'),
}),
},
async ({ weightKg, heightM }) => {
const bmi = weightKg / (heightM * heightM)
return {
content: [{ type: 'text', text: `BMI: ${bmi.toFixed(1)}` }],
}
}
)
// Tool with output schema (MCP 2025-06-18)
server.registerTool(
'calculate-bmi',
{
description: 'Calculate BMI',
inputSchema: z.object({
weightKg: z.number(),
heightM: z.number(),
}),
outputSchema: z.object({
bmi: z.number(),
category: z.string(),
}),
annotations: {
title: 'BMI Calculator',
readOnlyHint: true,
idempotentHint: true,
},
},
async ({ weightKg, heightM }) => {
const bmi = weightKg / (heightM * heightM)
const output = { bmi, category: bmi < 25 ? 'normal' : 'overweight' }
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output,
}
}
)
// Tool with no parameters
server.registerTool(
'ping',
{ description: 'Health check', inputSchema: z.object({}) },
async () => ({ content: [{ type: 'text', text: 'pong' }] })
)
See references/tools-resources-prompts.md for resources, prompts, error responses, and content types.
Tool Annotations (MCP 2025-06-18)
annotations: {
title: 'Human-Readable Title', // Display name
readOnlyHint: true, // No side effects
destructiveHint: false, // Does not destroy data
idempotentHint: true, // Same input = same result
longRunningHint: false, // Completes quickly
}
Annotations help clients decide how to present and auto-approve tools. A readOnlyHint: true tool is safer for auto-approval than a destructiveHint: true one.
Transports
// 1. Stdio — Local process, Claude Code / CLI (most common)
import { StdioServerTransport } from '@modelcontextprotocol/node'
const transport = new StdioServerTransport()
await server.connect(transport)
// 2. Streamable HTTP — Remote server, session management
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'
import { randomUUID } from 'node:crypto'
import express from 'express'
const app = express()
app.use(express.json())
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
})
await server.connect(transport)
app.post('/mcp', async (req, res) => {
await transport.handleRequest(req, res, req.body)
})
app.get('/mcp', async (req, res) => {
await transport.handleRequest(req, res)
})
app.delete('/mcp', async (req, res) => {
await transport.handleRequest(req, res)
})
app.listen(3000)
// 3. Stateless HTTP — JSON responses, no SSE
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true, // Disables SSE, returns plain JSON
})
See references/transports.md for session management, SSE, reconnection, and multi-client patterns.
Error Handling
server.registerTool(
'fetch-data',
{
description: 'Fetch data from API',
inputSchema: z.object({ url: z.string().url() }),
},
async ({ url }) => {
try {
const res = await fetch(url)
if (!res.ok) {
return {
content: [{ type: 'text', text: `HTTP ${res.status}: ${res.statusText}` }],
isError: true,
}
}
const data = await res.text()
return { content: [{ type: 'text', text: data }] }
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
isError: true,
}
}
}
)
Rules:
- Return
isError: truefor application-level errors (the LLM sees the error and can retry) - Throw exceptions only for protocol-level errors (invalid request, server bug)
- Always include a human-readable error message in
content
Client Configuration
Claude Code (~/.claude/settings.json)
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/dist/index.js"],
"env": { "API_KEY": "..." }
}
}
}
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": { "API_KEY": "..." }
}
}
}
Cursor (.cursor/mcp.json)
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": { "API_KEY": "..." }
}
}
}
Project Structure
my-mcp-server/
src/
index.ts # Entry: server creation + transport
tools/
registry.ts # Tool registry (if many tools)
my-tool.ts # Individual tool definitions
another-tool.ts
resources/
my-resource.ts # Resource definitions
prompts/
my-prompt.ts # Prompt definitions
utils/
validation.ts # Shared Zod schemas
bin/
cli.js # #!/usr/bin/env node entry
tests/
tools.test.ts # Tool unit tests
package.json
tsconfig.json
See references/advanced-patterns.md for dynamic tool loading, middleware, tool registry, and the lazy MCP pattern.
See references/testing-distribution.md for testing with vitest, CLI setup, npm publishing, and auto-setup scripts.
Common Pitfalls
- v2 requires
z.object()wrappers. Raw shapes like{ name: z.string() }no longer work — wrap withz.object({}). - Use
zod/v4notzod. The SDK v2 requires Zod v4 (import * as z from 'zod/v4'). "type": "module"is required. The SDK uses ESM. All imports need.jsextensions in TypeScript.- Stdio transport = no
console.log. Stdout is the MCP protocol channel. Useconsole.errorfor debug output, or implement MCP logging. isErroris for the LLM, not the protocol. ReturnisError: truewith a helpful message so the model can understand and recover.- Tool names must be unique. Registering a duplicate name will overwrite the previous tool.
- Content array must not be empty. Always return at least one content item, even for "no result" cases.
- Schema descriptions matter. LLMs use
descriptionfields on both tools and individual parameters to decide what to call and how.
References
- references/tools-resources-prompts.md — Deep dive into tools, resources, resource templates, prompts, content types
- references/transports.md — Stdio, Streamable HTTP, SSE, session management, Express integration
- references/advanced-patterns.md — Tool registry, dynamic loading, middleware, lazy MCP pattern, annotations
- references/testing-distribution.md — Testing with vitest, CLI packaging, npm publishing, client auto-setup
Done When
- MCP server compiles without errors (
tsc) - All tools registered with inputSchema (Zod v4) and descriptions
- Transport configured (stdio for CLI, HTTP for remote)
- Error handling returns
isError: truewith human-readable messages - Client configuration generated for target platform (Claude Code/Desktop/Cursor)
- Common pitfalls avoided (ESM, zod/v4, no console.log with stdio)
Constraints (Three-Tier)
ALWAYS
- Use
zod/v4for schema definitions — notzod - Use
z.object({})wrappers — raw shapes don't work in SDK v2 - Set
"type": "module"in package.json - Return
isError: truefor application errors, throw only for protocol errors - Include descriptions on both tools and individual parameters
ASK FIRST
- Transport choice when not specified (stdio vs HTTP)
- Target client platform for configuration generation
NEVER
- Use
console.logwith stdio transport — stdout is the MCP channel - Register duplicate tool names — they silently overwrite
- Return empty content arrays — always include at least one content item
- Use
zodimport instead ofzod/v4
What ships with it: 4 files
35.4 KB alongside SKILL.md
references/
- advanced-patterns.md11.4 KB
- testing-distribution.md9.4 KB
- tools-resources-prompts.md8.5 KB
- transports.md6.2 KB