mcp-server-development
MCP (Model Context Protocol) Server development guide. Use when building Claude Code extensions. Covers Tools, Resources, Prompts capabilities.
When & Why to Use This Skill
This Claude skill serves as a comprehensive development guide for the Model Context Protocol (MCP), the standard for building Claude Code extensions. It provides developers with structured patterns, security best practices, and implementation details for creating MCP servers that expose Tools, Resources, and Prompts, enabling seamless integration between Claude and external data sources or APIs.
Use Cases
- Building custom MCP servers to connect Claude with internal databases, allowing the AI to query and analyze proprietary data safely.
- Developing secure file-system tools for Claude Code that include built-in path traversal protection and input validation.
- Creating specialized API helpers that allow Claude to interact with third-party services like GitHub, Jira, or Slack through standardized JSON-RPC interfaces.
- Setting up robust testing environments using the MCP Inspector to debug and validate tool responses before deployment.
- Implementing idempotent tool designs to ensure AI-driven automation remains reliable and predictable across multiple executions.
| name | mcp-server-development |
|---|---|
| description | MCP (Model Context Protocol) Server development guide. Use when building Claude Code extensions. Covers Tools, Resources, Prompts capabilities. |
MCP Server Development
Core Principles
- Single Responsibility — Each server focuses on one domain
- Minimal Permissions — Request only necessary access
- Input Validation — Strictly validate all user input
- Error Handling — Return meaningful error messages
- Idempotent Design — Tools should be idempotent when possible
- Clear Documentation — Every Tool/Resource has a clear description
MCP Architecture
┌─────────────────────────────────────────────────┐
│ MCP Host │
│ (Claude Desktop / IDE) │
└─────────────────────────────────────────────────┘
│
JSON-RPC 2.0
(stdio / HTTP)
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Server A│ │ Server B│ │ Server C│
│ (Files) │ │ (GitHub)│ │ (DB) │
└─────────┘ └─────────┘ └─────────┘
Three Capabilities
| Type | Description | Example |
|---|---|---|
| Tools | Functions LLM can call | Execute commands, API calls |
| Resources | Data sources to read | File contents, DB records |
| Prompts | Pre-defined templates | Code review, doc generation |
Quick Start
1. Initialize Project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
2. Project Structure
my-mcp-server/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Entry point
│ ├── tools/ # Tool definitions
│ │ └── search.ts
│ ├── resources/ # Resource definitions
│ │ └── files.ts
│ └── prompts/ # Prompt templates
│ └── review.ts
└── tests/
└── server.test.ts
3. Basic Server
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
const server = new Server(
{ name: 'my-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'hello',
description: 'Say hello to someone',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' },
},
required: ['name'],
},
},
],
}));
// Handle tool calls
const HelloInput = z.object({ name: z.string().min(1) });
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'hello') {
const { name } = HelloInput.parse(request.params.arguments);
return {
content: [{ type: 'text', text: `Hello, ${name}!` }],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// Start server
const transport = new StdioServerTransport();
server.connect(transport);
Tools
Definition
{
name: 'search_files',
description: 'Search for files matching a glob pattern',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Glob pattern (e.g., "**/*.ts")',
},
directory: {
type: 'string',
description: 'Directory to search in',
default: '.',
},
},
required: ['pattern'],
},
}
Input Validation
import { z } from 'zod';
const SearchFilesInput = z.object({
pattern: z.string().min(1).max(500),
directory: z.string().default('.')
.refine(d => !d.includes('..'), 'Path traversal not allowed'),
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'search_files': {
const input = SearchFilesInput.parse(args);
const results = await glob(input.pattern, { cwd: input.directory });
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
Response Types
// Text response
return {
content: [{ type: 'text', text: 'Result here' }],
};
// Multiple content blocks
return {
content: [
{ type: 'text', text: 'File contents:' },
{ type: 'text', text: fileContent },
],
};
// Image (base64)
return {
content: [{
type: 'image',
data: base64EncodedImage,
mimeType: 'image/png',
}],
};
// Error (use isError flag)
return {
content: [{ type: 'text', text: 'Error: File not found' }],
isError: true,
};
Resources
Definition
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'file:///config.json',
name: 'Configuration',
description: 'Application configuration file',
mimeType: 'application/json',
},
{
uri: 'db://users',
name: 'Users Table',
description: 'Database users table',
mimeType: 'application/json',
},
],
}));
Reading Resources
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'file:///config.json') {
const content = await fs.readFile('config.json', 'utf-8');
return {
contents: [{
uri,
mimeType: 'application/json',
text: content,
}],
};
}
if (uri.startsWith('db://')) {
const table = uri.replace('db://', '');
const data = await db.query(`SELECT * FROM ${table} LIMIT 100`);
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(data, null, 2),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
Resource Templates
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
resourceTemplates: [
{
uriTemplate: 'file:///{path}',
name: 'File',
description: 'Read any file by path',
mimeType: 'text/plain',
},
],
}));
Prompts
Definition
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: 'code_review',
description: 'Generate a code review for the given file',
arguments: [
{
name: 'file_path',
description: 'Path to the file to review',
required: true,
},
{
name: 'focus',
description: 'Areas to focus on (security, performance, etc.)',
required: false,
},
],
},
],
}));
Getting Prompts
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'code_review') {
const filePath = args?.file_path as string;
const focus = args?.focus as string || 'general';
const code = await fs.readFile(filePath, 'utf-8');
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please review the following code with a focus on ${focus}:
\`\`\`
${code}
\`\`\`
Provide feedback on:
1. Code quality
2. Potential bugs
3. Suggestions for improvement`,
},
},
],
};
}
throw new Error(`Unknown prompt: ${name}`);
});
Security
Path Traversal Prevention
import path from 'path';
const ALLOWED_DIRS = ['/workspace', '/tmp'];
function validatePath(userPath: string): string {
// Resolve to absolute path
const resolved = path.resolve(userPath);
// Check against whitelist
const allowed = ALLOWED_DIRS.some(dir => resolved.startsWith(dir));
if (!allowed) {
throw new Error('Access denied: path outside allowed directories');
}
// Check for traversal attempts
if (userPath.includes('..')) {
throw new Error('Path traversal not allowed');
}
return resolved;
}
Input Sanitization
const SafeInput = z.object({
query: z.string()
.max(1000)
.refine(s => !s.includes('DROP'), 'Invalid query'),
filename: z.string()
.regex(/^[\w\-\.]+$/, 'Invalid filename'),
command: z.string()
.refine(s => !['rm', 'sudo', 'chmod'].some(c => s.includes(c)),
'Dangerous command not allowed'),
});
Secrets Handling
// Never expose secrets in responses
function sanitizeOutput(data: Record<string, unknown>): Record<string, unknown> {
const sensitiveKeys = ['password', 'apiKey', 'secret', 'token', 'credentials'];
return Object.fromEntries(
Object.entries(data).filter(([key]) =>
!sensitiveKeys.some(s => key.toLowerCase().includes(s))
)
);
}
Testing
MCP Inspector
# Install and run
npx @modelcontextprotocol/inspector node dist/index.js
Unit Tests
import { describe, it, expect, beforeAll } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
describe('MCP Server', () => {
let client: Client;
beforeAll(async () => {
// Setup test client connected to server
client = await createTestClient();
});
it('lists tools', async () => {
const { tools } = await client.listTools();
expect(tools).toContainEqual(
expect.objectContaining({ name: 'search_files' })
);
});
it('executes search_files tool', async () => {
const result = await client.callTool('search_files', {
pattern: '*.ts',
directory: 'src',
});
expect(result.content[0].text).toContain('index.ts');
});
it('rejects path traversal', async () => {
await expect(
client.callTool('search_files', {
pattern: '*.ts',
directory: '../../../etc',
})
).rejects.toThrow('Path traversal not allowed');
});
});
Deployment
Claude Desktop Config
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": {
"API_KEY": "your-key"
}
}
}
}
NPM Publishing
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": {
"my-mcp-server": "dist/index.js"
},
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
npm publish
Checklist
## Implementation
- [ ] Tools have clear descriptions
- [ ] Input validated with Zod
- [ ] Path traversal protection
- [ ] Meaningful error messages
- [ ] No sensitive data in responses
## Testing
- [ ] MCP Inspector tested
- [ ] Unit tests for tools
- [ ] Security tests (path traversal, etc.)
## Documentation
- [ ] README with usage examples
- [ ] Tool parameters documented
- [ ] Installation instructions
## Deployment
- [ ] package.json bin configured
- [ ] TypeScript compiled
- [ ] NPM published (optional)
See Also
- reference/protocol.md — MCP protocol details
- reference/security.md — Security best practices
- reference/testing.md — Testing strategies