
Mcp Creator
- 157 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Scaffold MCP servers exposing tools and resources to Claude Code or other agent hosts.
About
Walks developers through creating Model Context Protocol servers for agent, API, and CLI hosts: project structure, tool definitions, resource URIs, authentication, local testing, and registration so Claude agents can call external systems safely and repeatably.
- MCP server project scaffolding
- Tool and resource schema design
- Auth and secrets integration
- Local test and debug workflow
- Host configuration examples
Mcp Creator by the numbers
- 157 all-time installs (skills.sh)
- Ranked #3,299 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill mcp-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 157 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Scaffold MCP servers exposing tools and resources to Claude Code or other agent hosts.
Files
MCP Creator
Expert in building production-ready Model Context Protocol servers. Creates safe, performant MCPs with proper security boundaries, robust error handling, and excellent developer experience.
When to Use This Skill
Use MCP when you need:
- External API integration with authentication
- Stateful connections (databases, WebSockets, sessions)
- Multiple related tools sharing configuration
- Security boundaries between Claude and external services
- Connection pooling and resource management
Do NOT use MCP for:
- Pure domain expertise (use Skill)
- Multi-step orchestration (use Agent)
- Local stateless operations (use Script)
- Simple file processing (use Claude's built-in tools)
Quick Start
# Scaffold new MCP server
npx @modelcontextprotocol/create-server my-mcp-server
# Install SDK
npm install @modelcontextprotocol/sdk
# Test with inspector
npx @modelcontextprotocol/inspectorMCP Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Claude │
└─────────────────────────┬───────────────────────────────────┘
│ MCP Protocol (JSON-RPC)
┌─────────────────────────┴───────────────────────────────────┐
│ MCP Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Tools │ │ Resources │ │ Prompts │ │
│ │ (actions) │ │ (read-only) │ │ (templates) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │ │
│ ┌───────────────────────┴──────────────────────────────┐ │
│ │ Auth / Rate Limiting / Caching │ │
│ └───────────────────────┬──────────────────────────────┘ │
└──────────────────────────┼──────────────────────────────────┘
│
┌──────────────────────────┴──────────────────────────────────┐
│ External Services │
│ APIs │ Databases │ File Systems │ WebSockets │
└─────────────────────────────────────────────────────────────┘Core MCP Server Template
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Tool definitions
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "my_tool",
description: "Clear description of what this tool does",
inputSchema: {
type: "object",
properties: {
input: { type: "string", description: "Input description" },
},
required: ["input"],
},
},
],
}));
// Tool implementation
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "my_tool") {
try {
const result = await processInput(args.input);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);Tool Design Principles
1. Clear Naming
// ✅ Good: Action-oriented, specific
"get_user_profile"
"create_issue"
"analyze_sentiment"
// ❌ Bad: Vague, generic
"process"
"do_thing"
"handle"2. Precise Input Schemas
// ✅ Good: Typed, constrained, documented
{
type: "object",
properties: {
userId: { type: "string", pattern: "^[a-f0-9]{24}$" },
action: { type: "string", enum: ["read", "write", "delete"] },
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }
},
required: ["userId", "action"],
additionalProperties: false
}
// ❌ Bad: Untyped, unconstrained
{ type: "object" }3. Structured Outputs
// ✅ Good: Consistent structure
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
data: result,
metadata: { requestId, timestamp }
}, null, 2)
}]
};
// ❌ Bad: Inconsistent, unstructured
return { content: [{ type: "text", text: "done" }] };Security Hardening (CRITICAL)
Input Validation
import { z } from "zod";
const UserInputSchema = z.object({
userId: z.string().regex(/^[a-f0-9]{24}$/),
email: z.string().email(),
query: z.string().max(1000).refine(
(q) => !q.includes("--") && !q.includes(";"),
{ message: "Invalid characters in query" }
),
});
async function handleTool(args: unknown) {
const validated = UserInputSchema.parse(args); // Throws on invalid
// Safe to use validated data
}Secret Management
// ✅ Good: Environment variables
const API_KEY = process.env.SERVICE_API_KEY;
if (!API_KEY) throw new Error("SERVICE_API_KEY required");
// ✅ Good: Secret manager integration
const secret = await secretManager.getSecret("service-api-key");
// ❌ NEVER: Hardcoded secrets
const API_KEY = "sk-abc123..."; // SECURITY VULNERABILITYRate Limiting
class RateLimiter {
private requests: Map<string, number[]> = new Map();
canProceed(key: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const timestamps = this.requests.get(key) || [];
const recent = timestamps.filter(t => now - t < windowMs);
if (recent.length >= limit) return false;
recent.push(now);
this.requests.set(key, recent);
return true;
}
}
const limiter = new RateLimiter();
// In tool handler
if (!limiter.canProceed(userId, 100, 60000)) {
throw new McpError(ErrorCode.InvalidRequest, "Rate limit exceeded");
}Authentication Boundaries
// Validate credentials before any operation
async function withAuth<T>(
credentials: Credentials,
operation: () => Promise<T>
): Promise<T> {
if (!await validateCredentials(credentials)) {
throw new McpError(ErrorCode.InvalidRequest, "Invalid credentials");
}
return operation();
}Error Handling Patterns
Structured Error Responses
// Define error types
enum ServiceError {
NOT_FOUND = "NOT_FOUND",
UNAUTHORIZED = "UNAUTHORIZED",
RATE_LIMITED = "RATE_LIMITED",
VALIDATION_ERROR = "VALIDATION_ERROR",
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR",
}
// Map to MCP errors
function toMcpError(error: unknown): McpError {
if (error instanceof z.ZodError) {
return new McpError(
ErrorCode.InvalidParams,
`Validation error: ${error.errors.map(e => e.message).join(", ")}`
);
}
if (error instanceof ServiceError) {
return new McpError(ErrorCode.InternalError, error.message);
}
return new McpError(ErrorCode.InternalError, "Unknown error occurred");
}Graceful Degradation
async function fetchWithFallback<T>(
primary: () => Promise<T>,
fallback: () => Promise<T>,
options: { retries?: number; timeout?: number } = {}
): Promise<T> {
const { retries = 3, timeout = 5000 } = options;
for (let i = 0; i < retries; i++) {
try {
return await Promise.race([
primary(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout)
),
]);
} catch (error) {
if (i === retries - 1) {
console.error("Primary failed, trying fallback:", error);
return fallback();
}
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // Backoff
}
}
throw new Error("All retries exhausted");
}Performance Optimization
Connection Pooling
// PostgreSQL pool
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Reuse connections
async function query(sql: string, params: unknown[]) {
const client = await pool.connect();
try {
return await client.query(sql, params);
} finally {
client.release();
}
}Caching Layer
class Cache<T> {
private store: Map<string, { value: T; expires: number }> = new Map();
get(key: string): T | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expires) {
this.store.delete(key);
return undefined;
}
return entry.value;
}
set(key: string, value: T, ttlMs: number): void {
this.store.set(key, { value, expires: Date.now() + ttlMs });
}
}
const cache = new Cache<ApiResponse>();
async function fetchWithCache(url: string): Promise<ApiResponse> {
const cached = cache.get(url);
if (cached) return cached;
const response = await fetch(url);
const data = await response.json();
cache.set(url, data, 300000); // 5 min TTL
return data;
}Anti-Patterns
Anti-Pattern: No Input Validation
What it looks like: Passing user input directly to APIs/databases Why wrong: SQL injection, command injection, data corruption Instead: Validate with Zod, sanitize inputs, use parameterized queries
Anti-Pattern: Secrets in Code
What it looks like: Hardcoded API keys, tokens in source Why wrong: Secrets leak via git, logs, error messages Instead: Environment variables, secret managers, encrypted config
Anti-Pattern: No Rate Limiting
What it looks like: Unlimited API calls to external services Why wrong: Cost explosion, API bans, resource exhaustion Instead: Token bucket, sliding window, or adaptive rate limiting
Anti-Pattern: Synchronous Blocking
What it looks like: sleep(), blocking I/O in async handlers Why wrong: Blocks all requests, causes timeouts Instead: Proper async/await, non-blocking patterns
Anti-Pattern: Silent Failures
What it looks like: Empty catch blocks, swallowed errors Why wrong: Debugging impossible, data corruption undetected Instead: Structured error handling, logging, proper propagation
Anti-Pattern: No Timeouts
What it looks like: Waiting indefinitely for external services Why wrong: Hung connections, resource leaks Instead: Explicit timeouts on all external calls, circuit breakers
Testing Your MCP
Using MCP Inspector
# Start inspector
npx @modelcontextprotocol/inspector
# In another terminal, start your server
node dist/index.js
# Connect inspector to your server
# Test tool invocations manuallyUnit Testing
import { describe, it, expect } from "vitest";
describe("my_tool", () => {
it("should validate input", async () => {
await expect(
handleTool({ userId: "invalid" })
).rejects.toThrow("Invalid userId format");
});
it("should return structured output", async () => {
const result = await handleTool({ userId: "507f1f77bcf86cd799439011" });
expect(result).toHaveProperty("success", true);
expect(result).toHaveProperty("data");
});
});Decision Tree: When to Add to MCP
Does this tool need...
├── External API with auth? → Add to MCP
├── Persistent state/connection? → Add to MCP
├── Rate limiting for external service? → Add to MCP
├── Shared credentials with other tools? → Add to MCP
├── Security boundary from Claude? → Add to MCP
└── None of the above? → Consider Script insteadSuccess Metrics
| Metric | Target |
|---|---|
| Tool latency P95 | < 500ms |
| Error rate | < 1% |
| Input validation coverage | 100% |
| Secret exposure | 0 |
| Rate limit violations | 0 |
Reference Files
| File | Contents |
|---|---|
references/architecture-patterns.md | Transport layers, server lifecycle, resource management |
references/tool-design.md | Schema patterns, naming conventions, output formats |
references/security-hardening.md | Complete OWASP-aligned security checklist |
references/error-handling.md | Error types, recovery strategies, logging |
references/testing-debugging.md | Inspector usage, unit/integration tests |
references/performance.md | Caching, pooling, async patterns |
templates/ | Production-ready server templates |
---
Creates: Safe, performant MCP servers | Robust tool interfaces | Security-hardened integrations
Use with: security-auditor (security review) | site-reliability-engineer (deployment) | agent-creator (when MCP supports agents)
Changelog
All notable changes to the MCP Creator skill will be documented in this file.
[1.0.0] - 2024-12-15
Added
- Initial release of MCP Creator skill
- Core SKILL.md with MCP architecture overview, tool design patterns, and security hardening
- Reference documentation:
architecture-patterns.md- Transport layers, lifecycle, resource managementsecurity-hardening.md- OWASP-aligned security checklist, input validation, rate limitingtool-design.md- Naming conventions, schema patterns, output formatstesting-debugging.md- Testing strategies, debugging techniques, common issues- Production-ready templates:
basic-server.ts- Minimal server with proper structureauthenticated-api.ts- Full API integration with auth, caching, rate limiting
Security
- Comprehensive input validation patterns using Zod
- Secret management best practices
- Rate limiting implementation examples
- OWASP Top 10 mitigations
Documentation
- Decision tree for when to use MCP vs Script vs Agent
- Anti-patterns section with examples
- Success metrics table
MCP Architecture Patterns
Transport Layers
Stdio Transport (Recommended for CLI)
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Best for: CLI tools, local development, Claude Code integration
const transport = new StdioServerTransport();
await server.connect(transport);Pros: Simple, no network config, secure (process-level isolation) Cons: Single client, no remote access
SSE Transport (HTTP)
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
app.use("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
app.listen(3000);Pros: Multiple clients, remote access, browser-compatible Cons: More complex, requires HTTP server
Custom Transport
class CustomTransport implements Transport {
async start(): Promise<void> { /* Initialize connection */ }
async close(): Promise<void> { /* Cleanup */ }
async send(message: JSONRPCMessage): Promise<void> { /* Send message */ }
onMessage?: (message: JSONRPCMessage) => void;
onError?: (error: Error) => void;
onClose?: () => void;
}Server Lifecycle
Initialization Pattern
class MCPServer {
private pool: Pool | null = null;
private cache: Cache | null = null;
async initialize(): Promise<void> {
// 1. Validate configuration
this.validateConfig();
// 2. Initialize connections (parallel)
const [pool, cache] = await Promise.all([
this.initDatabase(),
this.initCache(),
]);
this.pool = pool;
this.cache = cache;
// 3. Warm caches if needed
await this.warmCaches();
// 4. Register signal handlers
this.registerShutdownHandlers();
}
private registerShutdownHandlers(): void {
const shutdown = async () => {
console.error("Shutting down...");
await this.pool?.end();
await this.cache?.quit();
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
}
}Health Check Pattern
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "health_check",
description: "Check server health and dependencies",
inputSchema: { type: "object", properties: {} },
},
// ... other tools
],
}));
async function healthCheck(): Promise<HealthStatus> {
const checks = await Promise.allSettled([
checkDatabase(),
checkCache(),
checkExternalApi(),
]);
return {
status: checks.every(c => c.status === "fulfilled") ? "healthy" : "degraded",
checks: {
database: checks[0].status === "fulfilled" ? "ok" : "failed",
cache: checks[1].status === "fulfilled" ? "ok" : "failed",
externalApi: checks[2].status === "fulfilled" ? "ok" : "failed",
},
timestamp: new Date().toISOString(),
};
}Resource Management
Connection Pool Pattern
import { Pool, PoolConfig } from "pg";
const poolConfig: PoolConfig = {
connectionString: process.env.DATABASE_URL,
max: 20, // Max connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000, // Fail fast if can't connect
statement_timeout: 30000, // Kill queries after 30s
};
const pool = new Pool(poolConfig);
// Monitor pool health
pool.on("error", (err) => {
console.error("Unexpected pool error:", err);
});
pool.on("connect", () => {
console.error("New client connected to pool");
});Resource Cleanup Pattern
class ResourceManager {
private resources: Set<Disposable> = new Set();
register<T extends Disposable>(resource: T): T {
this.resources.add(resource);
return resource;
}
async disposeAll(): Promise<void> {
const errors: Error[] = [];
for (const resource of this.resources) {
try {
await resource.dispose();
} catch (error) {
errors.push(error as Error);
}
}
this.resources.clear();
if (errors.length > 0) {
throw new AggregateError(errors, "Resource disposal failed");
}
}
}State Management
Stateless Design (Preferred)
// Each request is self-contained
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Get state from request or external store
const session = await getSession(request.params.sessionId);
// Process
const result = await processWithSession(session, request.params);
// Persist state externally
await saveSession(session);
return result;
});Stateful Design (When Necessary)
// For WebSockets, long-running connections
class StatefulServer {
private sessions: Map<string, Session> = new Map();
async handleConnection(clientId: string): Promise<Session> {
const session = new Session(clientId);
this.sessions.set(clientId, session);
// Set TTL for cleanup
setTimeout(() => this.cleanup(clientId), 3600000); // 1 hour
return session;
}
private cleanup(clientId: string): void {
const session = this.sessions.get(clientId);
if (session?.isExpired()) {
session.dispose();
this.sessions.delete(clientId);
}
}
}Capabilities Declaration
Tools Capability
const server = new Server(
{ name: "my-server", version: "1.0.0" },
{
capabilities: {
tools: {}, // Enable tools
},
}
);Resources Capability
const server = new Server(
{ name: "my-server", version: "1.0.0" },
{
capabilities: {
resources: {
subscribe: true, // Enable resource subscriptions
},
},
}
);
// Define resources
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "config://settings",
name: "Server Settings",
description: "Current server configuration",
mimeType: "application/json",
},
],
}));
// Read resource
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "config://settings") {
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(getConfig()),
}],
};
}
});Prompts Capability
const server = new Server(
{ name: "my-server", version: "1.0.0" },
{
capabilities: {
prompts: {}, // Enable prompts
},
}
);
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: "analyze_code",
description: "Analyze code for issues",
arguments: [
{ name: "code", description: "Code to analyze", required: true },
{ name: "language", description: "Programming language" },
],
},
],
}));Multi-Server Coordination
Shared Configuration
// config.ts - Shared across servers
export const CONFIG = {
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME,
},
redis: {
url: process.env.REDIS_URL,
},
rateLimit: {
requestsPerMinute: 100,
burstSize: 20,
},
};Service Registry Pattern
// For discovering other MCP servers
class ServiceRegistry {
private services: Map<string, ServiceInfo> = new Map();
register(name: string, info: ServiceInfo): void {
this.services.set(name, info);
}
discover(name: string): ServiceInfo | undefined {
return this.services.get(name);
}
async healthCheckAll(): Promise<Record<string, boolean>> {
const results: Record<string, boolean> = {};
for (const [name, info] of this.services) {
try {
const response = await fetch(`${info.url}/health`);
results[name] = response.ok;
} catch {
results[name] = false;
}
}
return results;
}
}Logging Best Practices
Structured Logging
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});
// In handlers
logger.info({ tool: "my_tool", args: sanitizedArgs }, "Tool invoked");
logger.error({ err, tool: "my_tool" }, "Tool failed");Audit Logging
interface AuditLog {
timestamp: string;
tool: string;
userId?: string;
args: Record<string, unknown>;
result: "success" | "failure";
duration: number;
errorCode?: string;
}
async function auditLog(entry: AuditLog): Promise<void> {
// To file, database, or external service
await appendToAuditLog(entry);
}Versioning Strategy
Semantic Versioning
const server = new Server(
{
name: "my-server",
version: "2.1.0", // MAJOR.MINOR.PATCH
},
{ capabilities: { tools: {} } }
);Backwards Compatibility
// Support old and new parameter names
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = request.params.arguments;
// Support both old and new field names
const userId = args.userId || args.user_id; // New or legacy
// Deprecation warning
if (args.user_id) {
console.warn("user_id is deprecated, use userId");
}
});MCP Security Hardening Guide
Security Checklist
Before deploying any MCP server, verify:
INPUT VALIDATION
├── [ ] All inputs validated with schema (Zod/JSON Schema)
├── [ ] String lengths limited
├── [ ] Numeric ranges constrained
├── [ ] Regex patterns for format validation
├── [ ] No SQL/command injection vectors
└── [ ] Arrays have max length limits
AUTHENTICATION & AUTHORIZATION
├── [ ] Credentials in environment variables only
├── [ ] Secrets never logged or returned in errors
├── [ ] API keys rotated regularly
├── [ ] Minimum required permissions
└── [ ] Token validation on every request
RATE LIMITING
├── [ ] Per-user rate limits
├── [ ] Per-tool rate limits
├── [ ] Global rate limits
├── [ ] Graceful degradation on limit
└── [ ] Rate limit headers in responses
ERROR HANDLING
├── [ ] No stack traces in production
├── [ ] Sensitive data sanitized from errors
├── [ ] Generic errors for auth failures
├── [ ] Structured error responses
└── [ ] All errors logged securely
NETWORK SECURITY
├── [ ] HTTPS for all external calls
├── [ ] Certificate validation enabled
├── [ ] Timeouts on all requests
├── [ ] No SSRF vulnerabilities
└── [ ] IP allowlisting where appropriateInput Validation Patterns
Using Zod (Recommended)
import { z } from "zod";
// Define strict schemas
const UserQuerySchema = z.object({
userId: z.string()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9_-]+$/, "Invalid userId format"),
query: z.string()
.max(10000)
.refine(
(q) => !containsSqlInjection(q),
{ message: "Invalid query content" }
),
options: z.object({
limit: z.number().int().min(1).max(1000).default(100),
offset: z.number().int().min(0).default(0),
sortBy: z.enum(["created", "updated", "name"]).default("created"),
}).optional(),
});
// Validate in handler
async function handleUserQuery(args: unknown) {
const validated = UserQuerySchema.parse(args);
// Safe to use validated.userId, validated.query, etc.
}SQL Injection Prevention
// ✅ Good: Parameterized queries
const result = await pool.query(
"SELECT * FROM users WHERE id = $1 AND status = $2",
[userId, status]
);
// ✅ Good: Query builder with escaping
const users = await knex("users")
.where({ id: userId, status })
.select("*");
// ❌ BAD: String concatenation
const result = await pool.query(
`SELECT * FROM users WHERE id = '${userId}'` // SQL INJECTION!
);Command Injection Prevention
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
// ✅ Good: execFile with array arguments
async function runGitCommand(args: string[]) {
// Validate args don't contain shell metacharacters
for (const arg of args) {
if (/[;&|`$]/.test(arg)) {
throw new Error("Invalid characters in argument");
}
}
const { stdout } = await execFileAsync("git", args);
return stdout;
}
// ❌ BAD: exec with string
import { exec } from "child_process";
exec(`git ${userInput}`); // COMMAND INJECTION!Path Traversal Prevention
import path from "path";
function safePath(basePath: string, userPath: string): string {
const resolved = path.resolve(basePath, userPath);
// Ensure resolved path is within base
if (!resolved.startsWith(path.resolve(basePath))) {
throw new Error("Path traversal attempt detected");
}
return resolved;
}
// Usage
const filePath = safePath("/app/data", userSuppliedPath);
await fs.readFile(filePath);Secret Management
Environment Variables (Minimum)
// config.ts
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Required environment variable ${name} is not set`);
}
return value;
}
export const CONFIG = {
apiKey: requireEnv("SERVICE_API_KEY"),
dbUrl: requireEnv("DATABASE_URL"),
jwtSecret: requireEnv("JWT_SECRET"),
};Secret Manager Integration
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
const client = new SecretManagerServiceClient();
async function getSecret(name: string): Promise<string> {
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${name}/versions/latest`,
});
return version.payload?.data?.toString() || "";
}
// Cache secrets on startup
let cachedSecrets: Record<string, string> = {};
async function initializeSecrets(): Promise<void> {
const secretNames = ["API_KEY", "DB_PASSWORD", "JWT_SECRET"];
const results = await Promise.all(
secretNames.map(async (name) => [name, await getSecret(name)])
);
cachedSecrets = Object.fromEntries(results);
}Secret Rotation
class RotatingSecret {
private currentSecret: string;
private previousSecret: string | null = null;
private rotationInterval: NodeJS.Timeout;
constructor(
private fetchSecret: () => Promise<string>,
rotationMs: number = 3600000 // 1 hour
) {
this.rotationInterval = setInterval(() => this.rotate(), rotationMs);
}
async initialize(): Promise<void> {
this.currentSecret = await this.fetchSecret();
}
private async rotate(): Promise<void> {
this.previousSecret = this.currentSecret;
this.currentSecret = await this.fetchSecret();
// Keep previous valid for grace period
setTimeout(() => {
this.previousSecret = null;
}, 60000); // 1 minute grace
}
validate(token: string): boolean {
return token === this.currentSecret ||
(this.previousSecret && token === this.previousSecret);
}
}Rate Limiting Implementation
Token Bucket Algorithm
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private capacity: number,
private refillRate: number // tokens per second
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
consume(tokens: number = 1): boolean {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const newTokens = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + newTokens);
this.lastRefill = now;
}
}
// Per-user buckets
const userBuckets = new Map<string, TokenBucket>();
function getRateLimiter(userId: string): TokenBucket {
if (!userBuckets.has(userId)) {
userBuckets.set(userId, new TokenBucket(100, 10)); // 100 capacity, 10/sec
}
return userBuckets.get(userId)!;
}Sliding Window Counter
class SlidingWindowCounter {
private windows: Map<string, Map<number, number>> = new Map();
constructor(
private windowSizeMs: number,
private limit: number
) {}
isAllowed(key: string): boolean {
const now = Date.now();
const windowStart = Math.floor(now / this.windowSizeMs) * this.windowSizeMs;
const prevWindowStart = windowStart - this.windowSizeMs;
let keyWindows = this.windows.get(key);
if (!keyWindows) {
keyWindows = new Map();
this.windows.set(key, keyWindows);
}
// Get counts
const currentCount = keyWindows.get(windowStart) || 0;
const prevCount = keyWindows.get(prevWindowStart) || 0;
// Weight previous window by overlap
const prevWeight = (this.windowSizeMs - (now - windowStart)) / this.windowSizeMs;
const totalCount = currentCount + Math.floor(prevCount * prevWeight);
if (totalCount >= this.limit) {
return false;
}
// Increment current window
keyWindows.set(windowStart, currentCount + 1);
// Cleanup old windows
for (const [windowTime] of keyWindows) {
if (windowTime < prevWindowStart) {
keyWindows.delete(windowTime);
}
}
return true;
}
}OWASP Top 10 Mitigations
A01: Broken Access Control
// Verify permissions on every request
async function checkPermission(
userId: string,
resource: string,
action: string
): Promise<boolean> {
const permissions = await getPermissions(userId);
return permissions.includes(`${resource}:${action}`);
}
// In handler
if (!await checkPermission(userId, "documents", "read")) {
throw new McpError(ErrorCode.InvalidRequest, "Access denied");
}A02: Cryptographic Failures
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(scrypt);
// ✅ Good: Use strong hashing
async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16).toString("hex");
const hash = await scryptAsync(password, salt, 64) as Buffer;
return `${salt}:${hash.toString("hex")}`;
}
// ✅ Good: Timing-safe comparison
async function verifyPassword(password: string, stored: string): Promise<boolean> {
const [salt, hash] = stored.split(":");
const hashBuffer = Buffer.from(hash, "hex");
const suppliedHash = await scryptAsync(password, salt, 64) as Buffer;
return timingSafeEqual(hashBuffer, suppliedHash);
}
// ❌ BAD: Weak algorithms
import { createHash } from "crypto";
createHash("md5").update(password).digest("hex"); // INSECURE!A03: Injection
See SQL and Command Injection sections above.
A05: Security Misconfiguration
// Validate all configuration
function validateConfig(config: Config): void {
// No default credentials
if (config.adminPassword === "admin") {
throw new Error("Default admin password detected");
}
// HTTPS required in production
if (config.nodeEnv === "production" && !config.apiUrl.startsWith("https")) {
throw new Error("HTTPS required in production");
}
// Debug mode off in production
if (config.nodeEnv === "production" && config.debug) {
throw new Error("Debug mode must be off in production");
}
}A10: Server-Side Request Forgery (SSRF)
import { URL } from "url";
import dns from "dns/promises";
const PRIVATE_RANGES = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^127\./,
/^169\.254\./,
/^0\./,
];
async function safeFetch(urlString: string): Promise<Response> {
const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs allowed");
}
// Resolve hostname and check for private IPs
const addresses = await dns.resolve4(url.hostname);
for (const addr of addresses) {
if (PRIVATE_RANGES.some(r => r.test(addr))) {
throw new Error("Private IP addresses not allowed");
}
}
// Allow-list domains if possible
const allowedDomains = ["api.example.com", "service.example.org"];
if (!allowedDomains.includes(url.hostname)) {
throw new Error("Domain not in allow list");
}
return fetch(urlString);
}Error Handling Security
Sanitize Error Messages
class SecureError extends Error {
constructor(
public userMessage: string,
public internalMessage: string,
public code: string
) {
super(internalMessage);
}
}
function handleError(error: unknown): McpError {
// Log full error internally
console.error("Internal error:", error);
// Return safe message to user
if (error instanceof SecureError) {
return new McpError(ErrorCode.InternalError, error.userMessage);
}
if (error instanceof z.ZodError) {
return new McpError(
ErrorCode.InvalidParams,
"Invalid input parameters" // Don't expose field names
);
}
// Generic error for unexpected cases
return new McpError(
ErrorCode.InternalError,
"An unexpected error occurred"
);
}Secure Logging
// Redact sensitive fields
function redactSensitive(obj: Record<string, unknown>): Record<string, unknown> {
const sensitiveFields = ["password", "token", "apiKey", "secret", "authorization"];
const redacted = { ...obj };
for (const field of sensitiveFields) {
if (field in redacted) {
redacted[field] = "[REDACTED]";
}
}
return redacted;
}
// Usage
logger.info({ args: redactSensitive(args) }, "Tool called");MCP Testing and Debugging Guide
Testing Tools
MCP Inspector (Official)
# Install globally
npm install -g @modelcontextprotocol/inspector
# Run inspector
npx @modelcontextprotocol/inspector
# Connect to your server
# In inspector UI: Connect to stdio serverInspector capabilities:
- List available tools and resources
- Execute tool calls with custom arguments
- View request/response JSON
- Test error handling
- Profile performance
Manual Testing with Node
// test-client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
async function testServer() {
// Spawn server process
const serverProcess = spawn("node", ["dist/index.js"]);
const transport = new StdioClientTransport({
command: "node",
args: ["dist/index.js"],
});
const client = new Client({
name: "test-client",
version: "1.0.0",
}, {
capabilities: {},
});
await client.connect(transport);
// List tools
const tools = await client.listTools();
console.log("Available tools:", tools);
// Call a tool
const result = await client.callTool({
name: "my_tool",
arguments: { input: "test" },
});
console.log("Result:", result);
await client.close();
}
testServer().catch(console.error);Unit Testing
Testing Tool Handlers
// __tests__/handlers.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { handleMyTool } from "../src/handlers";
describe("handleMyTool", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should validate input correctly", async () => {
await expect(handleMyTool({ input: "" }))
.rejects.toThrow("Input is required");
});
it("should process valid input", async () => {
const result = await handleMyTool({ input: "test" });
expect(result).toHaveProperty("success", true);
expect(result).toHaveProperty("data");
});
it("should handle options correctly", async () => {
const result = await handleMyTool({
input: "test",
options: { format: "text" },
});
expect(typeof result).toBe("string");
});
});Testing Input Validation
// __tests__/validation.test.ts
import { describe, it, expect } from "vitest";
import { z } from "zod";
import { MyToolSchema } from "../src/schemas";
describe("MyToolSchema", () => {
it("should accept valid input", () => {
const input = { userId: "abc123", action: "read" };
expect(() => MyToolSchema.parse(input)).not.toThrow();
});
it("should reject missing required fields", () => {
const input = { action: "read" };
expect(() => MyToolSchema.parse(input)).toThrow();
});
it("should reject invalid userId format", () => {
const input = { userId: "invalid!!!", action: "read" };
expect(() => MyToolSchema.parse(input)).toThrow();
});
it("should reject invalid enum values", () => {
const input = { userId: "abc123", action: "invalid" };
expect(() => MyToolSchema.parse(input)).toThrow();
});
it("should apply defaults", () => {
const input = { userId: "abc123", action: "read" };
const result = MyToolSchema.parse(input);
expect(result.options?.limit).toBe(10);
});
});Testing Rate Limiter
// __tests__/rate-limiter.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { RateLimiter } from "../src/rate-limiter";
describe("RateLimiter", () => {
let limiter: RateLimiter;
beforeEach(() => {
vi.useFakeTimers();
limiter = new RateLimiter(10, 60000); // 10 per minute
});
it("should allow requests under limit", () => {
for (let i = 0; i < 10; i++) {
expect(limiter.canProceed("user1")).toBe(true);
}
});
it("should block requests over limit", () => {
for (let i = 0; i < 10; i++) {
limiter.canProceed("user1");
}
expect(limiter.canProceed("user1")).toBe(false);
});
it("should reset after window expires", () => {
for (let i = 0; i < 10; i++) {
limiter.canProceed("user1");
}
// Advance time by 1 minute
vi.advanceTimersByTime(60000);
expect(limiter.canProceed("user1")).toBe(true);
});
it("should track users independently", () => {
for (let i = 0; i < 10; i++) {
limiter.canProceed("user1");
}
expect(limiter.canProceed("user1")).toBe(false);
expect(limiter.canProceed("user2")).toBe(true);
});
});Testing API Client
// __tests__/api-client.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { apiRequest } from "../src/api-client";
// Mock fetch
global.fetch = vi.fn();
describe("apiRequest", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should make successful request", async () => {
(fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: "test" }),
});
const result = await apiRequest("/endpoint");
expect(result).toEqual({ data: "test" });
});
it("should retry on 5xx errors", async () => {
(fetch as any)
.mockResolvedValueOnce({ ok: false, status: 500, text: async () => "Error" })
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: "success" }) });
const result = await apiRequest("/endpoint");
expect(result).toEqual({ data: "success" });
expect(fetch).toHaveBeenCalledTimes(2);
});
it("should not retry on 4xx errors", async () => {
(fetch as any).mockResolvedValueOnce({
ok: false,
status: 400,
text: async () => "Bad request",
});
await expect(apiRequest("/endpoint")).rejects.toThrow();
expect(fetch).toHaveBeenCalledTimes(1);
});
it("should handle timeout", async () => {
vi.useFakeTimers();
(fetch as any).mockImplementationOnce(() => new Promise(() => {})); // Never resolves
const promise = apiRequest("/endpoint", { timeout: 1000 });
vi.advanceTimersByTime(1000);
await expect(promise).rejects.toThrow("timeout");
});
});Integration Testing
Full Server Test
// __tests__/integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { spawn, ChildProcess } from "child_process";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
describe("MCP Server Integration", () => {
let client: Client;
let serverProcess: ChildProcess;
beforeAll(async () => {
const transport = new StdioClientTransport({
command: "node",
args: ["dist/index.js"],
env: {
...process.env,
API_KEY: "test-key",
API_BASE_URL: "http://localhost:3000",
},
});
client = new Client({ name: "test", version: "1.0.0" }, { capabilities: {} });
await client.connect(transport);
});
afterAll(async () => {
await client.close();
});
it("should list tools", async () => {
const { tools } = await client.listTools();
expect(tools).toBeInstanceOf(Array);
expect(tools.length).toBeGreaterThan(0);
expect(tools[0]).toHaveProperty("name");
expect(tools[0]).toHaveProperty("inputSchema");
});
it("should execute tool successfully", async () => {
const result = await client.callTool({
name: "example_tool",
arguments: { input: "test" },
});
expect(result.content).toBeInstanceOf(Array);
expect(result.content[0].type).toBe("text");
});
it("should handle invalid arguments", async () => {
await expect(
client.callTool({
name: "example_tool",
arguments: { input: "" },
})
).rejects.toThrow();
});
});Debugging Techniques
Structured Logging
// Add to your server
const DEBUG = process.env.DEBUG === "true";
function debug(message: string, data?: unknown) {
if (DEBUG) {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
level: "debug",
message,
data,
}));
}
}
// In handlers
debug("Tool called", { name, args });
debug("API response", { status, body });Request Tracing
import { randomUUID } from "crypto";
interface RequestContext {
requestId: string;
startTime: number;
tool: string;
}
const activeRequests = new Map<string, RequestContext>();
function startRequest(tool: string): string {
const requestId = randomUUID();
activeRequests.set(requestId, {
requestId,
startTime: Date.now(),
tool,
});
console.error(`[${requestId}] START ${tool}`);
return requestId;
}
function endRequest(requestId: string, success: boolean) {
const ctx = activeRequests.get(requestId);
if (ctx) {
const duration = Date.now() - ctx.startTime;
console.error(`[${requestId}] END ${ctx.tool} ${success ? "OK" : "FAIL"} ${duration}ms`);
activeRequests.delete(requestId);
}
}Error Debugging
// Detailed error logging
function logError(error: unknown, context: Record<string, unknown>) {
const errorInfo = {
timestamp: new Date().toISOString(),
type: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
context,
};
console.error("ERROR:", JSON.stringify(errorInfo, null, 2));
}
// Usage in handlers
try {
// ... handler code
} catch (error) {
logError(error, { tool: name, args: sanitizedArgs });
throw error;
}Performance Profiling
Simple Timing
function withTiming<T>(
name: string,
fn: () => Promise<T>
): Promise<T> {
const start = process.hrtime.bigint();
return fn().finally(() => {
const end = process.hrtime.bigint();
const durationMs = Number(end - start) / 1_000_000;
console.error(`TIMING ${name}: ${durationMs.toFixed(2)}ms`);
});
}
// Usage
const result = await withTiming("api_call", () => apiRequest("/endpoint"));Memory Monitoring
function logMemory() {
const usage = process.memoryUsage();
console.error("MEMORY:", {
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(usage.external / 1024 / 1024)}MB`,
});
}
// Log periodically
setInterval(logMemory, 60000);Common Issues and Solutions
Issue: Server Hangs on Startup
Cause: Blocking initialization (missing await, sync I/O) Solution: Ensure all initialization is async
// ❌ Bad
const config = fs.readFileSync("config.json"); // Blocks
// ✅ Good
const config = await fs.promises.readFile("config.json");Issue: Memory Leaks
Cause: Unbounded caches, event listener accumulation Solution: Set limits, cleanup old entries
// Add TTL and max size to caches
class BoundedCache<T> {
private store = new Map<string, { value: T; expires: number }>();
private maxSize: number;
constructor(maxSize: number = 1000) {
this.maxSize = maxSize;
}
set(key: string, value: T, ttlMs: number) {
// Evict oldest if at capacity
if (this.store.size >= this.maxSize) {
const oldest = this.store.keys().next().value;
this.store.delete(oldest);
}
this.store.set(key, { value, expires: Date.now() + ttlMs });
}
}Issue: Rate Limit Errors from API
Cause: Not respecting Retry-After header Solution: Parse and respect rate limit headers
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After");
const waitMs = retryAfter
? parseInt(retryAfter) * 1000
: 60000; // Default 1 minute
console.error(`Rate limited, waiting ${waitMs}ms`);
await new Promise(r => setTimeout(r, waitMs));
// Retry...
}Issue: Timeout Errors
Cause: No timeout on external requests Solution: Always set timeouts
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}MCP Tool Design Patterns
Tool Naming Conventions
Action-Oriented Names
// ✅ Good: verb_noun format
"get_user" // Read operation
"create_document" // Create operation
"update_settings" // Update operation
"delete_item" // Delete operation
"search_products" // Search operation
"analyze_sentiment" // Analysis operation
"validate_schema" // Validation operation
// ❌ Bad: Vague or generic
"process" // What does it process?
"handle" // Handle what?
"data" // Not a verb
"user_stuff" // InformalConsistent Prefixes
// CRUD operations
"create_*", "get_*", "list_*", "update_*", "delete_*"
// Bulk operations
"batch_create_*", "bulk_update_*"
// Search/filter
"search_*", "find_*", "filter_*"
// Analysis
"analyze_*", "compute_*", "calculate_*"
// Validation
"validate_*", "check_*", "verify_*"Input Schema Patterns
Basic Types with Constraints
{
type: "object",
properties: {
// String with length and pattern
username: {
type: "string",
minLength: 3,
maxLength: 50,
pattern: "^[a-zA-Z0-9_-]+$",
description: "Username (alphanumeric, underscore, hyphen)"
},
// Number with range
age: {
type: "integer",
minimum: 0,
maximum: 150,
description: "User age in years"
},
// Enum for fixed options
status: {
type: "string",
enum: ["active", "inactive", "pending"],
description: "Account status"
},
// Array with item constraints
tags: {
type: "array",
items: { type: "string", maxLength: 50 },
maxItems: 10,
uniqueItems: true,
description: "Tags for categorization"
},
// Boolean with default
sendNotification: {
type: "boolean",
default: true,
description: "Whether to send notification"
}
},
required: ["username"],
additionalProperties: false
}Complex Object Schemas
{
type: "object",
properties: {
filter: {
type: "object",
properties: {
dateRange: {
type: "object",
properties: {
start: { type: "string", format: "date-time" },
end: { type: "string", format: "date-time" }
},
required: ["start", "end"]
},
status: {
type: "array",
items: { type: "string", enum: ["open", "closed", "pending"] }
},
assignee: { type: "string" }
}
},
pagination: {
type: "object",
properties: {
page: { type: "integer", minimum: 1, default: 1 },
pageSize: { type: "integer", minimum: 1, maximum: 100, default: 20 }
}
},
sort: {
type: "object",
properties: {
field: { type: "string", enum: ["created", "updated", "priority"] },
direction: { type: "string", enum: ["asc", "desc"], default: "desc" }
}
}
}
}Optional vs Required Fields
// Make required fields explicit
{
required: ["userId", "action"], // Must be provided
properties: {
userId: { type: "string" }, // Required
action: { type: "string" }, // Required
reason: { type: "string" }, // Optional
notify: { type: "boolean", default: false } // Optional with default
}
}Output Format Patterns
Consistent Success Response
interface SuccessResponse<T> {
success: true;
data: T;
metadata?: {
requestId: string;
timestamp: string;
duration: number;
};
}
// Example
{
success: true,
data: {
user: {
id: "123",
name: "John",
email: "john@example.com"
}
},
metadata: {
requestId: "req-abc123",
timestamp: "2024-01-15T10:30:00Z",
duration: 45
}
}Consistent Error Response
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: Record<string, unknown>;
};
metadata?: {
requestId: string;
timestamp: string;
};
}
// Example
{
success: false,
error: {
code: "VALIDATION_ERROR",
message: "Invalid input parameters",
details: {
field: "email",
issue: "Invalid email format"
}
},
metadata: {
requestId: "req-xyz789",
timestamp: "2024-01-15T10:30:00Z"
}
}List Response with Pagination
interface ListResponse<T> {
success: true;
data: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
};
}
// Example
{
success: true,
data: [
{ id: "1", name: "Item 1" },
{ id: "2", name: "Item 2" }
],
pagination: {
page: 1,
pageSize: 20,
total: 157,
totalPages: 8,
hasNext: true,
hasPrevious: false
}
}Tool Description Best Practices
Good Descriptions
{
name: "search_documents",
description: `Search documents by content, metadata, or full-text query.
Supports:
- Full-text search across document content
- Filtering by date range, author, tags
- Pagination and sorting
Returns matching documents with relevance scores.
Example: Search for "quarterly report" in finance documents from 2024.`,
inputSchema: { /* ... */ }
}Document Edge Cases
{
name: "delete_user",
description: `Permanently delete a user and all associated data.
WARNING: This action is irreversible.
What gets deleted:
- User profile
- All user documents
- Activity history
- Preferences
What is preserved:
- Audit logs (for compliance)
- Shared documents owned by others
Requires: admin role or user deleting own account.`,
inputSchema: { /* ... */ }
}Tool Grouping Strategies
By Domain
// User management tools
"user_create", "user_get", "user_update", "user_delete", "user_list"
// Document tools
"document_create", "document_get", "document_search", "document_delete"
// Analytics tools
"analytics_get_summary", "analytics_get_trends", "analytics_export"By Operation Type
// Read-only tools (safe)
"get_*", "list_*", "search_*", "count_*"
// Write tools (mutating)
"create_*", "update_*", "delete_*"
// Dangerous tools (require extra confirmation)
"admin_*", "bulk_delete_*", "purge_*"Idempotency Patterns
Idempotent Operations
// GET operations are naturally idempotent
async function getUser(userId: string) {
return db.users.findById(userId);
}
// PUT/UPDATE can be idempotent with version checks
async function updateUser(userId: string, data: UserData, version: number) {
const result = await db.users.updateOne(
{ _id: userId, version },
{ $set: { ...data, version: version + 1 } }
);
if (result.modifiedCount === 0) {
throw new Error("Version conflict - retry with latest version");
}
}
// DELETE is idempotent (deleting non-existent is OK)
async function deleteUser(userId: string) {
await db.users.deleteOne({ _id: userId });
return { deleted: true }; // Same result even if already deleted
}Idempotency Keys for POST
const processedRequests = new Map<string, unknown>();
async function createOrder(
idempotencyKey: string,
orderData: OrderData
): Promise<Order> {
// Check if already processed
if (processedRequests.has(idempotencyKey)) {
return processedRequests.get(idempotencyKey) as Order;
}
// Process the order
const order = await db.orders.create(orderData);
// Cache result
processedRequests.set(idempotencyKey, order);
// Cleanup old keys after 24 hours
setTimeout(() => processedRequests.delete(idempotencyKey), 86400000);
return order;
}Batch Operation Patterns
Batch with Individual Results
interface BatchResult<T> {
success: true;
results: Array<{
id: string;
status: "success" | "error";
data?: T;
error?: string;
}>;
summary: {
total: number;
succeeded: number;
failed: number;
};
}
// Example response
{
success: true,
results: [
{ id: "1", status: "success", data: { /* ... */ } },
{ id: "2", status: "error", error: "Not found" },
{ id: "3", status: "success", data: { /* ... */ } }
],
summary: {
total: 3,
succeeded: 2,
failed: 1
}
}Batch with Atomic Rollback
async function batchCreateWithRollback(items: Item[]): Promise<BatchResult> {
const session = await db.startSession();
session.startTransaction();
try {
const results = [];
for (const item of items) {
const created = await db.items.create([item], { session });
results.push(created[0]);
}
await session.commitTransaction();
return { success: true, data: results };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}Progress Reporting for Long Operations
Streaming Progress
{
name: "process_large_file",
description: "Process a large file with progress updates",
inputSchema: {
type: "object",
properties: {
fileId: { type: "string" },
includeProgress: { type: "boolean", default: true }
}
}
}
// Response with progress
{
success: true,
data: {
status: "processing",
progress: {
current: 45000,
total: 100000,
percentage: 45,
estimatedTimeRemaining: "2 minutes"
}
}
}Job-Based Pattern
// Start job
{
name: "start_export_job",
description: "Start an async export job",
}
// Returns: { jobId: "job-123" }
// Check status
{
name: "get_job_status",
description: "Check status of an async job",
}
// Returns: { jobId: "job-123", status: "running", progress: 75 }
// Get result
{
name: "get_job_result",
description: "Get result of completed job",
}
// Returns: { jobId: "job-123", status: "completed", result: { ... } }#!/usr/bin/env node
/**
* Authenticated API MCP Server Template
*
* Production-ready MCP server for external API integration with:
* - Secure credential management
* - Rate limiting
* - Retry with exponential backoff
* - Response caching
* - Comprehensive error handling
*
* Usage:
* 1. Set environment variables (API_KEY, API_BASE_URL)
* 2. Customize the API client methods
* 3. Add your tool definitions
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// =============================================================================
// Configuration
// =============================================================================
interface Config {
name: string;
version: string;
apiBaseUrl: string;
apiKey: string;
rateLimitPerMinute: number;
cacheTtlMs: number;
maxRetries: number;
timeoutMs: number;
}
function loadConfig(): Config {
const apiKey = process.env.API_KEY;
const apiBaseUrl = process.env.API_BASE_URL;
if (!apiKey) {
throw new Error("API_KEY environment variable is required");
}
if (!apiBaseUrl) {
throw new Error("API_BASE_URL environment variable is required");
}
return {
name: "authenticated-api-mcp",
version: "1.0.0",
apiBaseUrl,
apiKey,
rateLimitPerMinute: parseInt(process.env.RATE_LIMIT || "60"),
cacheTtlMs: parseInt(process.env.CACHE_TTL_MS || "300000"), // 5 min
maxRetries: parseInt(process.env.MAX_RETRIES || "3"),
timeoutMs: parseInt(process.env.TIMEOUT_MS || "30000"), // 30 sec
};
}
const CONFIG = loadConfig();
// =============================================================================
// Rate Limiter
// =============================================================================
class RateLimiter {
private requests: number[] = [];
canProceed(): boolean {
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
// Remove old requests
this.requests = this.requests.filter(t => t > windowStart);
if (this.requests.length >= CONFIG.rateLimitPerMinute) {
return false;
}
this.requests.push(now);
return true;
}
getWaitTime(): number {
if (this.requests.length === 0) return 0;
const oldestInWindow = Math.min(...this.requests);
const windowEnd = oldestInWindow + 60000;
return Math.max(0, windowEnd - Date.now());
}
}
const rateLimiter = new RateLimiter();
// =============================================================================
// Cache
// =============================================================================
interface CacheEntry<T> {
value: T;
expires: number;
}
class Cache<T> {
private store = new Map<string, CacheEntry<T>>();
get(key: string): T | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expires) {
this.store.delete(key);
return undefined;
}
return entry.value;
}
set(key: string, value: T, ttlMs: number = CONFIG.cacheTtlMs): void {
this.store.set(key, {
value,
expires: Date.now() + ttlMs,
});
}
clear(): void {
this.store.clear();
}
}
const responseCache = new Cache<unknown>();
// =============================================================================
// API Client
// =============================================================================
interface ApiError {
status: number;
message: string;
retryable: boolean;
}
async function apiRequest<T>(
endpoint: string,
options: {
method?: string;
body?: unknown;
useCache?: boolean;
cacheKey?: string;
} = {}
): Promise<T> {
const { method = "GET", body, useCache = true, cacheKey } = options;
// Check cache for GET requests
const cacheKeyFinal = cacheKey || `${method}:${endpoint}`;
if (useCache && method === "GET") {
const cached = responseCache.get(cacheKeyFinal);
if (cached) {
console.error(`Cache hit: ${cacheKeyFinal}`);
return cached as T;
}
}
// Check rate limit
if (!rateLimiter.canProceed()) {
const waitTime = rateLimiter.getWaitTime();
throw new McpError(
ErrorCode.InvalidRequest,
`Rate limit exceeded. Retry after ${Math.ceil(waitTime / 1000)} seconds.`
);
}
// Retry logic
let lastError: ApiError | null = null;
for (let attempt = 0; attempt <= CONFIG.maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CONFIG.timeoutMs);
const response = await fetch(`${CONFIG.apiBaseUrl}${endpoint}`, {
method,
headers: {
"Authorization": `Bearer ${CONFIG.apiKey}`,
"Content-Type": "application/json",
"User-Agent": `${CONFIG.name}/${CONFIG.version}`,
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
const errorBody = await response.text();
lastError = {
status: response.status,
message: errorBody,
retryable: response.status >= 500 || response.status === 429,
};
if (!lastError.retryable) {
break; // Don't retry client errors
}
// Handle rate limit from API
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After");
const waitMs = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * Math.pow(2, attempt), 30000);
console.error(`Rate limited by API, waiting ${waitMs}ms...`);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
// Exponential backoff for server errors
const backoffMs = Math.min(1000 * Math.pow(2, attempt), 30000);
console.error(`Request failed (attempt ${attempt + 1}), retrying in ${backoffMs}ms...`);
await new Promise(r => setTimeout(r, backoffMs));
continue;
}
const data = await response.json();
// Cache successful GET responses
if (useCache && method === "GET") {
responseCache.set(cacheKeyFinal, data);
}
return data as T;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
lastError = {
status: 0,
message: "Request timeout",
retryable: true,
};
} else {
throw error;
}
}
}
// All retries exhausted
throw new McpError(
lastError?.status === 401 || lastError?.status === 403
? ErrorCode.InvalidRequest
: ErrorCode.InternalError,
`API request failed: ${lastError?.message || "Unknown error"}`
);
}
// =============================================================================
// Input Schemas
// =============================================================================
const GetResourceSchema = z.object({
resourceId: z.string().min(1).max(100),
includeDetails: z.boolean().default(false),
});
const SearchResourcesSchema = z.object({
query: z.string().min(1).max(500),
limit: z.number().int().min(1).max(100).default(20),
offset: z.number().int().min(0).default(0),
});
const CreateResourceSchema = z.object({
name: z.string().min(1).max(200),
data: z.record(z.unknown()),
});
// =============================================================================
// Tool Implementations
// =============================================================================
interface Resource {
id: string;
name: string;
data: Record<string, unknown>;
}
interface SearchResult {
items: Resource[];
total: number;
}
async function getResource(args: z.infer<typeof GetResourceSchema>): Promise<Resource> {
return apiRequest<Resource>(`/resources/${args.resourceId}`);
}
async function searchResources(args: z.infer<typeof SearchResourcesSchema>): Promise<SearchResult> {
const params = new URLSearchParams({
q: args.query,
limit: args.limit.toString(),
offset: args.offset.toString(),
});
return apiRequest<SearchResult>(`/resources/search?${params}`);
}
async function createResource(args: z.infer<typeof CreateResourceSchema>): Promise<Resource> {
return apiRequest<Resource>("/resources", {
method: "POST",
body: args,
useCache: false,
});
}
// =============================================================================
// Tool Definitions
// =============================================================================
const TOOLS = [
{
name: "get_resource",
description: "Get a resource by ID",
inputSchema: {
type: "object" as const,
properties: {
resourceId: { type: "string", description: "Resource ID" },
includeDetails: { type: "boolean", default: false },
},
required: ["resourceId"],
},
},
{
name: "search_resources",
description: "Search resources by query",
inputSchema: {
type: "object" as const,
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
offset: { type: "integer", minimum: 0, default: 0 },
},
required: ["query"],
},
},
{
name: "create_resource",
description: "Create a new resource",
inputSchema: {
type: "object" as const,
properties: {
name: { type: "string", description: "Resource name" },
data: { type: "object", description: "Resource data" },
},
required: ["name", "data"],
},
},
{
name: "clear_cache",
description: "Clear the response cache",
inputSchema: { type: "object" as const, properties: {} },
},
];
// =============================================================================
// Server Setup
// =============================================================================
const server = new Server(
{ name: CONFIG.name, version: CONFIG.version },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: unknown;
switch (name) {
case "get_resource":
result = await getResource(GetResourceSchema.parse(args));
break;
case "search_resources":
result = await searchResources(SearchResourcesSchema.parse(args));
break;
case "create_resource":
result = await createResource(CreateResourceSchema.parse(args));
break;
case "clear_cache":
responseCache.clear();
result = { success: true, message: "Cache cleared" };
break;
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new McpError(
ErrorCode.InvalidParams,
`Validation error: ${error.errors.map(e => `${e.path}: ${e.message}`).join(", ")}`
);
}
throw error;
}
});
// =============================================================================
// Start Server
// =============================================================================
async function main() {
console.error(`Starting ${CONFIG.name} v${CONFIG.version}`);
console.error(`API: ${CONFIG.apiBaseUrl}`);
console.error(`Rate limit: ${CONFIG.rateLimitPerMinute}/min`);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
#!/usr/bin/env node
/**
* Basic MCP Server Template
*
* A minimal, production-ready MCP server with:
* - Proper error handling
* - Input validation
* - Structured logging
* - Graceful shutdown
*
* Usage:
* 1. Copy this template
* 2. Add your tools in the tools array
* 3. Implement tool handlers in the switch statement
* 4. Run with: node dist/index.js
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// =============================================================================
// Configuration
// =============================================================================
const CONFIG = {
name: "my-mcp-server",
version: "1.0.0",
description: "Description of your MCP server",
};
// =============================================================================
// Input Schemas (Zod)
// =============================================================================
const ExampleToolSchema = z.object({
input: z.string().min(1).max(10000),
options: z.object({
format: z.enum(["json", "text"]).default("json"),
verbose: z.boolean().default(false),
}).optional(),
});
// =============================================================================
// Tool Definitions
// =============================================================================
const TOOLS = [
{
name: "example_tool",
description: `Example tool that processes input.
Features:
- Validates input
- Returns structured output
- Supports JSON and text formats`,
inputSchema: {
type: "object" as const,
properties: {
input: {
type: "string",
description: "Input to process",
},
options: {
type: "object",
properties: {
format: {
type: "string",
enum: ["json", "text"],
default: "json",
},
verbose: {
type: "boolean",
default: false,
},
},
},
},
required: ["input"],
},
},
{
name: "health_check",
description: "Check server health and status",
inputSchema: {
type: "object" as const,
properties: {},
},
},
];
// =============================================================================
// Tool Implementations
// =============================================================================
async function handleExampleTool(args: z.infer<typeof ExampleToolSchema>) {
// Your implementation here
const result = {
processed: args.input.toUpperCase(),
length: args.input.length,
timestamp: new Date().toISOString(),
};
if (args.options?.format === "text") {
return `Processed: ${result.processed} (${result.length} chars)`;
}
return result;
}
async function handleHealthCheck() {
return {
status: "healthy",
version: CONFIG.version,
uptime: process.uptime(),
timestamp: new Date().toISOString(),
};
}
// =============================================================================
// Server Setup
// =============================================================================
const server = new Server(
{
name: CONFIG.name,
version: CONFIG.version,
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// =============================================================================
// Request Handlers
// =============================================================================
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const startTime = Date.now();
try {
let result: unknown;
switch (name) {
case "example_tool": {
const validated = ExampleToolSchema.parse(args);
result = await handleExampleTool(validated);
break;
}
case "health_check": {
result = await handleHealthCheck();
break;
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
const duration = Date.now() - startTime;
console.error(`Tool ${name} completed in ${duration}ms`);
return {
content: [
{
type: "text",
text: typeof result === "string"
? result
: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`Tool ${name} failed after ${duration}ms:`, error);
if (error instanceof z.ZodError) {
throw new McpError(
ErrorCode.InvalidParams,
`Validation error: ${error.errors.map(e => e.message).join(", ")}`
);
}
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : "Unknown error"
);
}
});
// List resources (optional)
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: `${CONFIG.name}://status`,
name: "Server Status",
description: "Current server status and configuration",
mimeType: "application/json",
},
],
}));
// Read resources (optional)
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === `${CONFIG.name}://status`) {
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify({
name: CONFIG.name,
version: CONFIG.version,
status: "running",
uptime: process.uptime(),
}, null, 2),
},
],
};
}
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
});
// =============================================================================
// Graceful Shutdown
// =============================================================================
function shutdown(signal: string) {
console.error(`Received ${signal}, shutting down...`);
// Add cleanup logic here (close connections, etc.)
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
// =============================================================================
// Start Server
// =============================================================================
async function main() {
console.error(`Starting ${CONFIG.name} v${CONFIG.version}...`);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${CONFIG.name} running on stdio`);
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});