MCP Builder
Overview
Build production-quality MCP (Model Context Protocol) servers that expose tools, resources, and prompts to AI clients. This skill covers the full development lifecycle: tool definition, resource management, prompt templates, transport configuration (stdio, SSE), error handling, security hardening, testing, and client integration.
Phase 1: Design
- Identify capabilities to expose (tools, resources, prompts)
- Define tool schemas with Zod/JSON Schema
- Plan resource URI patterns
- Design error handling strategy
- Choose transport layer (stdio for CLI, SSE for web)
STOP — Present the capability inventory and transport choice to user for approval.
Capability Selection Decision Table
| What You Have |
MCP Primitive |
Example |
| Actions that modify state |
Tool |
create-issue, send-email, deploy-app |
| Actions that read/query |
Tool |
search-documents, get-status |
| Data the AI should read |
Resource |
config://settings, docs://api/endpoints |
| Reusable prompt patterns |
Prompt |
code-review, summarize-document |
| Real-time data feeds |
Resource (subscribable) |
metrics://cpu/current |
Transport Selection Decision Table
| Context |
Transport |
Why |
| CLI tool, local client (Claude Desktop) |
Stdio |
Simple, no network overhead |
| Web application, remote clients |
SSE |
Network-accessible, real-time |
| Both local and remote |
Stdio + SSE |
Support both use cases |
| High-throughput, bidirectional |
WebSocket (custom) |
Lower latency than SSE |
Phase 2: Implementation
- Set up MCP server project structure
- Implement tool handlers with input validation
- Implement resource providers
- Add prompt templates
- Configure transport and authentication
STOP — Run basic smoke tests before moving to hardening.
Project Structure
src/
index.ts # Server entry point
tools/
search.ts # Tool implementations
create.ts
resources/
documents.ts # Resource providers
config.ts
prompts/
review.ts # Prompt templates
lib/
database.ts # Shared utilities
validation.ts
tests/
tools.test.ts
resources.test.ts
package.json
tsconfig.json
Tool Definition Pattern
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
server.tool(
'search-documents',
'Search documents by query. Returns matching documents with relevance scores.',
{
query: z.string().describe('Search query string'),
limit: z.number().min(1).max(100).default(10).describe('Maximum results to return'),
filter: z.object({
type: z.enum(['article', 'page', 'note']).optional(),
dateAfter: z.string().datetime().optional(),
}).optional().describe('Optional filters'),
},
async ({ query, limit, filter }) => {
const results = await searchEngine.search(query, { limit, ...filter });
return {
content: [{
type: 'text',
text: JSON.stringify(results, null, 2),
}],
};
}
);
Tool Design Principles
| Principle |
Rule |
| Clear naming |
verb-noun format: search-documents, create-issue |
| Descriptive descriptions |
Explain what, when, and return value |
| Validated inputs |
Zod schemas with .describe() on every field |
| Structured outputs |
Well-formatted text or JSON |
| Idempotent when possible |
Same input produces same result |
| Actionable errors |
Specific error messages with isError: true |
Tool Response Patterns
return { content: [{ type: 'text', text: 'Operation completed successfully' }] };
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
return {
content: [
{ type: 'text', text: `Found ${results.length} results:` },
{ type: 'text', text: results.map(r => `- ${r.title}: ${r.summary}`).join('\n') },
],
};
return { content: [{ type: 'image', data: base64Data, mimeType: 'image/png' }] };
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
Resource Management
Resource Definition
server.resource(
'config',
'config://app/settings',
{ mimeType: 'application/json' },
async () => ({
contents: [{
uri: 'config://app/settings',
mimeType: 'application/json',
text: JSON.stringify(appConfig),
}],
})
);
server.resource(
'document',
new ResourceTemplate('docs://{category}/{id}', { list: undefined }),
{ mimeType: 'text/markdown' },
async (uri, { category, id }) => ({
contents: [{
uri: uri.href,
mimeType: 'text/markdown',
text: await getDocument(category, id),
}],
})
);
Resource URI Conventions
file:///path/to/file — Local files
https://api.example.com/data — Remote HTTP resources
db://database/table/id — Database records
config://app/settings — Configuration
docs://category/slug — Documentation
Prompt Templates
server.prompt(
'code-review',
'Generate a code review for the given file',
{
filePath: z.string().describe('Path to the file to review'),
severity: z.enum(['strict', 'normal', 'lenient']).default('normal'),
},
async ({ filePath, severity }) => {
const code = await readFile(filePath, 'utf-8');
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `Review this code with ${severity} standards:\n\n\`\`\`\n${code}\n\`\`\``,
},
}],
};
}
);
Transport Layers
Stdio Transport (CLI tools, local development)
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const transport = new StdioServerTransport();
await server.connect(transport);
SSE Transport (Web applications, remote servers)
import express from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
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) => {
});
app.listen(3001);
Phase 3: Hardening
- Add comprehensive error handling
- Implement rate limiting and timeouts
- Security review (input sanitization, permission checks)
- Write integration tests
- Document tools and resources for clients
STOP — All tests must pass and security review must be complete before deployment.
Error Handling
server.tool('risky-operation', 'Performs an operation that might fail', {
input: z.string(),
}, async ({ input }) => {
try {
const result = await performOperation(input);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (error) {
if (error instanceof ValidationError) {
return {
content: [{ type: 'text', text: `Invalid input: ${error.message}` }],
isError: true,
};
}
if (error instanceof NotFoundError) {
return {
content: [{ type: 'text', text: `Resource not found: ${error.message}` }],
isError: true,
};
}
console.error('Unexpected error:', error);
return {
content: [{ type: 'text', text: 'An unexpected error occurred. Please try again.' }],
isError: true,
};
}
});
Error Handling Rules
| Rule |
Why |
| Never expose stack traces to clients |
Security risk |
Return isError: true for all errors |
Client can distinguish success/failure |
| Log unexpected errors server-side |
Debugging and monitoring |
| Provide actionable error messages |
Client can self-correct |
| Handle timeouts for external calls |
Prevent hanging requests |
| Validate all inputs before processing |
Reject bad data early |
Security Considerations
| Category |
Rules |
| Input validation |
Zod schemas, path traversal prevention, length limits |
| Permission model |
Least privilege, whitelist directories, separate read/write tools |
| Secrets |
Env vars only, never in responses, mask in logs, rotate regularly |
| Rate limiting |
Limit tool invocations per client |
| Auditing |
Log all tool calls with timestamps |
Testing MCP Servers
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
describe('MCP Server', () => {
let server: McpServer;
let client: Client;
beforeEach(async () => {
server = createServer();
client = new Client({ name: 'test-client', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
server.connect(serverTransport),
client.connect(clientTransport),
]);
});
test('search-documents returns results', async () => {
const result = await client.callTool({
name: 'search-documents',
arguments: { query: 'test', limit: 5 },
});
expect(result.content[0].type).toBe('text');
const data = JSON.parse(result.content[0].text);
expect(data.length).toBeLessThanOrEqual(5);
});
test('handles invalid input gracefully', async () => {
const result = await client.callTool({
name: 'search-documents',
arguments: { query: '', limit: -1 },
});
expect(result.isError).toBe(true);
});
});
Client Integration
Claude Desktop Configuration
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server/dist/index.js"],
"env": {
"API_KEY": "your-key-here"
}
}
}
}
Anti-Patterns / Common Mistakes
| Anti-Pattern |
Why It Is Wrong |
What to Do Instead |
| Tools that do too many things |
Hard to use, hard to test |
Split into focused single-purpose tools |
| Missing input validation |
Crashes, security holes |
Always use Zod schemas |
| Returning raw stack traces |
Security risk, confusing for AI |
Return isError: true with clean message |
| No timeout on external calls |
Hangs indefinitely |
Set timeouts on all I/O |
| Hardcoded secrets in source |
Credential exposure |
Use environment variables |
| Tools without descriptions |
Clients cannot discover purpose |
Write clear descriptions |
| Blocking event loop with sync ops |
Server becomes unresponsive |
Use async/await for all I/O |
| No tests |
Regressions go undetected |
Test with InMemoryTransport |
Documentation Lookup (Context7)
Use mcp__context7__resolve-library-id then mcp__context7__query-docs for up-to-date docs. Returned docs override memorized knowledge.
@anthropic-ai/sdk — for Claude API client, tool definitions, or streaming
Integration Points
| Skill |
Integration |
senior-devops |
Containerize and deploy MCP servers |
agent-development |
MCP servers provide tools for agents |
security-review |
Security hardening of tool inputs/outputs |
test-driven-development |
TDD for tool implementation |
deployment |
CI/CD pipeline for MCP server releases |
planning |
MCP server design is part of the implementation plan |
Skill Type
FLEXIBLE — Adapt project structure, transport choice, and tooling to the use case. Tool validation with Zod and error handling with isError are strongly recommended. Security review is recommended before production deployment.