Use when designing, building, or debugging a Model Context Protocol (MCP) server in Node/TypeScript. Triggers: stdio JSON-RPC handshake, tool descriptions as discovery surface, lazy startup vs eager catalog loading, telemetry placement, schema design for tool inputs (zod), tool naming conventions for discoverability, error handling that does not leak stack traces, MCP client compatibility (Claude Desktop, Claude Code, Cursor), local resource fetching, secrets and env var handling, distributing as npm + claude mcp add. NOT for MCP client implementation, MCP HTTP transport (different surface), Anthropic Agent SDK building, or non-MCP plugin systems.
Instrucciones de origen · Vista previa de solo lectura
name
mcp-server-design
description
Use when designing, building, or debugging a Model Context Protocol (MCP) server in Node/TypeScript. Triggers: stdio JSON-RPC handshake, tool descriptions as discovery surface, lazy startup vs eager catalog loading, telemetry placement, schema design for tool inputs (zod), tool naming conventions for discoverability, error handling that does not leak stack traces, MCP client compatibility (Claude Desktop, Claude Code, Cursor), local resource fetching, secrets and env var handling, distributing as npm + claude mcp add. NOT for MCP client implementation, MCP HTTP transport (different surface), Anthropic Agent SDK building, or non-MCP plugin systems.
license
Apache-2.0
allowed-tools
Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch
metadata
{"category":"AI & Machine Learning","tags":["mcp","model-context-protocol","claude","server","jsonrpc","stdio"],"pairs-with":[{"skill":"skill-architect","reason":"Tool descriptions are a discovery surface exactly like skill descriptions; that skill's activation-engineering discipline applies to every tool an MCP server exposes"},{"skill":"error-handling-patterns","reason":"The isError/sanitization boundary this skill requires at each tool handler is an instance of that skill's error-taxonomy and boundary patterns"}],"provenance":{"kind":"first-party","owners":["port-daddy"]},"io-contract":{"kind":"deliverable","consumes":["[Truncated]","[Truncated]"],"produces":["[Truncated]","[Truncated]"]}}
MCP Server Design
The Model Context Protocol is a JSON-RPC 2.0 dialect over stdio (most commonly). The protocol is small; the design space is in what tools you expose, how you describe them, and how the server starts up. The descriptions are the discovery surface — a tool the client doesn't understand from its description never gets called.
When to use
Building an MCP server for a domain (skill catalog, code search, internal data).
Server starts but tools don't appear in the client.
Tool descriptions are technically accurate but the client never invokes them.
Latency budget tight; first-call cost too high.
Need to ship as a single npm command (claude mcp add foo -- npx -y foo-mcp).
Core capabilities
The handshake
client → server initialize { protocolVersion, capabilities, clientInfo }
server → client result { protocolVersion, capabilities, serverInfo }
client → server notifications/initialized (no response)
client → server tools/list
server → client result { tools: [...] }
client → server tools/call { name, arguments }
server → client result { content: [...], isError? }
The high-level SDK (@modelcontextprotocol/sdk) handles framing. You declare tools; the SDK serializes the schema and dispatches calls.
Minimal server with the SDK
import { McpServer } from'@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from'@modelcontextprotocol/sdk/server/stdio.js';
import { z } from'zod';
const server = newMcpServer({ name: 'mything', version: '1.0.0' });
server.tool(
'mything_search',
'Search the catalog. Returns ranked candidates with descriptions only ' +
'(no full bodies). Pair with mything_load to fetch the chosen item. ' +
,
{
: z.().(),
: z.().().().().().().(),
},
({ query, limit }) => {
results = (query, limit ?? );
{ : [{ : , : .({ query, results }, , ) }] };
},
);
server.( ());
'Local-only, no API keys.'
query
string
describe
'Natural-language search query'
limit
number
int
min
1
max
50
optional
default
10
describe
'Max results (default 10)'
async
const
await
search
10
return
content
type
'text'
text
JSON
stringify
null
2
await
connect
new
StdioServerTransport
Tool descriptions are the API
The model picks tools based on the description. Two rules:
Lead with capability, not implementation. "Search the catalog with a 5-stage cascade" is fine, but lead with "find the right skill for a task" because that's what the model is searching for.
Tell the model when NOT to use the tool. "Returns descriptions only — pair with mything_load for full bodies."
A discoverable description includes:
What it does (one sentence, capability-first)
What it returns (shape hints help the model parse)
JSON inside a text block is a workable convention; the model can parse it. Avoid throwing — return isError: true instead so the client gets a typed error.
Handling secrets
Servers run in the user's environment. process.env works, but:
Document required env vars in the README.
Fail fast at startup if a required secret is missing — don't fail the first tool call.
Never log the secret value, even truncated.
const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
console.error('MY_API_KEY required. Set in your shell or in claude mcp config.');
process.exit(2);
}
Never block on telemetry. Wrap in a 1.5s AbortController. Default to anonymous. Document WINDAGS_TELEMETRY=off (or your equivalent) prominently.
Distribution
Three install patterns:
# 1. npm global
npm install -g mything-mcp
claude mcp add mything -- mything-mcp
# 2. npx (no install)
claude mcp add mything -- npx -y mything-mcp@latest
# 3. From a plugin marketplace
claude plugin marketplace add yourorg/your-skills
claude plugin install your-skills
For (2), include "bin": { "mything-mcp": "./index.js" } in package.json with a shebang on index.js. For (3), the plugin manifest declares the MCP server alongside skills/commands.
Run this in CI on every commit. It catches startup regressions before they reach users.
Anti-patterns
Logging to stdout
Symptom: Client says "invalid JSON-RPC frame" or "unexpected character".
Diagnosis: Anything written to stdout after connect corrupts the frame stream.
Fix: All non-protocol output → stderr. Audit console.log and any logger that defaults to stdout.
Eager loading at startup
Symptom: Client times out the handshake; tool list never appears.
Diagnosis: Loading a 100MB index synchronously at boot.
Fix: Lazy-load behind first tool call. Print a one-liner status to stderr; do real work later.
Tool description that reads like a docstring
Symptom: Model doesn't invoke the tool even when the query matches its purpose.
Diagnosis: Description starts with "This tool will..." instead of the capability.
Fix: Lead with the verb. "Find skills relevant to a task." Then implementation details.
Path traversal vulnerability
Symptom: Reading a file by skill_id + file_path lets the user escape the skill dir.
Diagnosis: No startsWith check after path resolution.
Fix:path.resolve both, then startsWith(root + path.sep). Test with ../../../etc/passwd.
Secret leak in error message
Symptom: A failed call returns "invalid auth: Bearer sk_live_..." to the client.
Diagnosis: Error formatting includes the auth header verbatim.
Fix: Sanitize errors at the tool boundary. Return generic messages; log the detailed error to stderr.
Tool that does too much
Symptom: Model wastes turns trying to figure out which arguments to pass.
Diagnosis: One tool with 12 optional args covering 3 unrelated capabilities.
Fix: Split into 3 tools with focused schemas. Cross-reference each in their descriptions.
Quality gates
Handshake test runs in CI on every commit.
Server starts in <100ms (heavy work behind first tool call).
No stdout writes after connect.
Every tool description includes capability, return shape, and when-not-to-use.
Path-traversal test (../../../etc/passwd) returns an error.
Required env vars validated at startup with actionable error.
Telemetry fire-and-forget; never blocks tool response.
Tool inputs validated by zod with min/max/enum constraints.
Errors return isError: true, not thrown exceptions.
Distribution path tested: install + register + handshake on a clean machine.
Deterministic Audit
Before shipping an MCP server (or reviewing one), write its design as a JSON plan
matching schemas/mcp-server-plan.schema.json and run the deterministic auditor:
auditMcpServerDesign(plan) (in scripts/mcp_server_design_audit.mjs) encodes this
skill's anti-patterns as machine-checkable rules over structured fields only: logging to
stdout on the stdio transport, eager loading at startup (or a boot over the 100ms budget),
a file tool without a path-traversal guard, thrown or unsanitized errors, secrets checked
on the first tool call instead of at boot, blocking telemetry, a tool with too many
optional args, descriptions without a when-not-to-use clause, unconstrained inputs, and a
missing CI handshake test. It returns { pass, score, findings, recommendations } and
exits 1 on failure. examples/sample-input.json is a clean stdio/lazy design that audits
pass: true. Version history lives in CHANGELOG.md.
NOT for
MCP client implementation — different surface; pair with the MCP-client skill.
MCP HTTP transport — same protocol but different framing concerns.
Anthropic Agent SDK — building agents, not protocol servers.
Non-MCP plugin systems (LSP, VS Code extensions) — different protocols.
MCP resource subscriptions — separate area, see resources/list and notifications/resources/updated.