name: mcp-2025-patterns description: Current best practices for Model Context Protocol server design, implementation, and integration. Updated patterns for 2025 MCP ecosystem including multi-server orchestration, security, and performance. version: 1.0.0 last_updated: 2025-12-19 external_version: "MCP Specification 2025" changelog: |
- 1.0.0: Initial skill with 2025 MCP patterns and best practices
MCP 2025 Patterns
This skill covers current best practices for Model Context Protocol (MCP) server design, implementation, and integration as of late 2025.
MCP Architecture Fundamentals
Core Concepts
┌─────────────────────────────────────────────────────────────┐
│ Claude Code (Host) │
├─────────────────────────────────────────────────────────────┤
│ MCP Client Layer │
│ ├── Server Discovery & Connection │
│ ├── Tool Registration & Invocation │
│ ├── Resource Management │
│ └── Prompt Templates │
├─────────────────────────────────────────────────────────────┤
│ MCP Servers (Multiple) │
│ ├── github-mcp (repositories, issues, PRs) │
│ ├── notion-mcp (pages, databases, blocks) │
│ ├── linear-mcp (issues, projects, cycles) │
│ ├── custom-mcp (your domain-specific tools) │
│ └── ... │
└─────────────────────────────────────────────────────────────┘
MCP Server Components
- Tools: Functions the AI can invoke
- Resources: Data the AI can read
- Prompts: Template prompts for common tasks
- Notifications: Server-to-client events
Server Design Patterns
Pattern 1: Single Responsibility Server
// Good: Focused server for one domain
const server = new MCPServer({
name: "github-mcp",
version: "1.0.0"
});
// Tools all relate to GitHub
server.addTool("create_issue", createIssueHandler);
server.addTool("list_pulls", listPullsHandler);
server.addTool("merge_pr", mergePRHandler);
Pattern 2: Resource-First Design
// Define resources before tools
server.addResource({
uri: "github://repos/{owner}/{repo}",
name: "Repository",
description: "GitHub repository data and metadata",
mimeType: "application/json"
});
// Tools operate on resources
server.addTool({
name: "get_repo",
description: "Fetch repository details",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" }
},
required: ["owner", "repo"]
}
});
Pattern 3: Hierarchical Tool Organization
// Organize tools by domain/action
const tools = {
// Issues domain
"issues_create": createIssue,
"issues_update": updateIssue,
"issues_list": listIssues,
"issues_close": closeIssue,
// PRs domain
"pulls_create": createPR,
"pulls_merge": mergePR,
"pulls_review": reviewPR,
// Repos domain
"repos_list": listRepos,
"repos_create": createRepo
};
Tool Design Best Practices
Clear, Action-Oriented Names
// Good: Clear action + noun
"create_issue"
"list_repositories"
"merge_pull_request"
"search_code"
// Bad: Vague or ambiguous
"do_github"
"handle_request"
"process_data"
Comprehensive Input Schemas
server.addTool({
name: "create_issue",
description: "Create a new GitHub issue with title, body, labels, and assignees",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner (user or org)"
},
repo: {
type: "string",
description: "Repository name"
},
title: {
type: "string",
description: "Issue title",
minLength: 1,
maxLength: 256
},
body: {
type: "string",
description: "Issue body (Markdown supported)"
},
labels: {
type: "array",
items: { type: "string" },
description: "Labels to apply"
},
assignees: {
type: "array",
items: { type: "string" },
description: "GitHub usernames to assign"
}
},
required: ["owner", "repo", "title"]
}
});
Structured Output Formats
// Return structured, predictable data
async function createIssue(params) {
const issue = await github.issues.create({...});
return {
success: true,
issue: {
number: issue.number,
url: issue.html_url,
title: issue.title,
state: issue.state,
created_at: issue.created_at
},
// Include actionable next steps
suggested_actions: [
`Add labels: /api/issues/${issue.number}/labels`,
`Assign team: /api/issues/${issue.number}/assignees`
]
};
}
Security Patterns
Pattern 1: Credential Isolation
// Store credentials in environment, not code
const server = new MCPServer({
name: "secure-server"
});
// Validate env vars at startup
const requiredEnv = ["API_KEY", "API_SECRET"];
for (const key of requiredEnv) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
// Never log or expose credentials
server.addTool("secure_action", async (params) => {
// Use env vars directly, never pass as params
const client = new APIClient({
key: process.env.API_KEY // Not from params
});
});
Pattern 2: Input Validation
import { z } from "zod";
const CreateIssueSchema = z.object({
owner: z.string().regex(/^[a-zA-Z0-9-]+$/),
repo: z.string().regex(/^[a-zA-Z0-9._-]+$/),
title: z.string().min(1).max(256),
body: z.string().max(65536).optional()
});
server.addTool("create_issue", async (params) => {
// Validate before processing
const validated = CreateIssueSchema.parse(params);
// Safe to use validated data
return await github.issues.create(validated);
});
Pattern 3: Rate Limiting
import { RateLimiter } from "rate-limiter";
const limiter = new RateLimiter({
tokensPerInterval: 100,
interval: "minute"
});
server.addTool("api_call", async (params) => {
// Check rate limit before proceeding
if (!await limiter.tryRemoveTokens(1)) {
return {
success: false,
error: "Rate limit exceeded. Try again in a minute.",
retry_after: 60
};
}
return await performAPICall(params);
});
Pattern 4: Audit Logging
server.addTool("sensitive_action", async (params, context) => {
// Log all sensitive operations
await auditLog({
action: "sensitive_action",
params: sanitize(params), // Remove secrets
user: context.user,
timestamp: new Date().toISOString(),
result: "pending"
});
try {
const result = await performAction(params);
await auditLog.update({ result: "success" });
return result;
} catch (error) {
await auditLog.update({ result: "error", error: error.message });
throw error;
}
});
Multi-Server Orchestration
Pattern 1: Server Composition
// In Claude Code settings, compose servers:
{
"mcpServers": {
"github": {
"command": "mcp-github",
"env": { "GITHUB_TOKEN": "..." }
},
"linear": {
"command": "mcp-linear",
"env": { "LINEAR_API_KEY": "..." }
},
"notion": {
"command": "mcp-notion",
"env": { "NOTION_TOKEN": "..." }
}
}
}
Pattern 2: Cross-Server Workflows
When handling complex tasks, coordinate across servers:
1. Get issue from GitHub (github-mcp)
2. Create linked Linear ticket (linear-mcp)
3. Update project doc in Notion (notion-mcp)
4. Comment back on GitHub with links (github-mcp)
Claude orchestrates automatically based on task.
Pattern 3: Server Health Monitoring
// Each server should expose health endpoint
server.addTool("health_check", async () => {
const checks = await Promise.all([
checkAPIConnection(),
checkDatabaseConnection(),
checkCacheConnection()
]);
return {
status: checks.every(c => c.ok) ? "healthy" : "degraded",
checks: checks,
timestamp: new Date().toISOString()
};
});
Performance Patterns
Pattern 1: Connection Pooling
// Reuse connections across requests
const pool = new ConnectionPool({
max: 10,
idleTimeout: 30000
});
server.addTool("db_query", async (params) => {
const conn = await pool.acquire();
try {
return await conn.query(params.sql);
} finally {
pool.release(conn);
}
});
Pattern 2: Caching
import { LRUCache } from "lru-cache";
const cache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 5 // 5 minutes
});
server.addTool("get_user", async (params) => {
const cacheKey = `user:${params.id}`;
// Check cache first
const cached = cache.get(cacheKey);
if (cached) return cached;
// Fetch and cache
const user = await fetchUser(params.id);
cache.set(cacheKey, user);
return user;
});
Pattern 3: Batch Operations
// Support batch operations to reduce round trips
server.addTool("batch_create_issues", async (params) => {
const { issues } = params;
// Process in parallel with concurrency limit
const results = await pMap(
issues,
issue => createIssue(issue),
{ concurrency: 5 }
);
return {
created: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
};
});
Error Handling
Structured Error Responses
class MCPError extends Error {
constructor(code, message, details = {}) {
super(message);
this.code = code;
this.details = details;
}
toResponse() {
return {
success: false,
error: {
code: this.code,
message: this.message,
details: this.details,
recoverable: this.isRecoverable(),
suggested_action: this.getSuggestedAction()
}
};
}
}
// Usage
throw new MCPError(
"RATE_LIMITED",
"GitHub API rate limit exceeded",
{
limit: 5000,
remaining: 0,
reset_at: "2025-12-19T12:00:00Z"
}
);
Graceful Degradation
server.addTool("enriched_search", async (params) => {
const results = await primarySearch(params);
// Try to enrich, but don't fail if enrichment fails
try {
return await enrichResults(results);
} catch (enrichError) {
console.warn("Enrichment failed, returning basic results", enrichError);
return {
...results,
enrichment_status: "failed",
enrichment_error: enrichError.message
};
}
});
Testing Patterns
Unit Testing Tools
import { describe, it, expect, vi } from "vitest";
describe("create_issue tool", () => {
it("creates issue with valid params", async () => {
const mockGithub = vi.fn().mockResolvedValue({
number: 123,
html_url: "https://github.com/..."
});
const result = await createIssueTool({
owner: "test",
repo: "test-repo",
title: "Test issue"
}, { github: mockGithub });
expect(result.success).toBe(true);
expect(result.issue.number).toBe(123);
});
it("validates required params", async () => {
await expect(createIssueTool({ owner: "test" }))
.rejects.toThrow("Missing required: repo, title");
});
});
Integration Testing
describe("MCP Server Integration", () => {
let server;
let client;
beforeAll(async () => {
server = await startMCPServer();
client = await connectMCPClient(server.url);
});
it("lists available tools", async () => {
const tools = await client.listTools();
expect(tools).toContain("create_issue");
expect(tools).toContain("list_repositories");
});
it("executes tool and returns result", async () => {
const result = await client.callTool("health_check", {});
expect(result.status).toBe("healthy");
});
});
FrankX System Integration
Current MCP Servers in Use
- github-mcp: Repository management, issues, PRs
- linear-mcp: Project management, issues, cycles
- notion-mcp: Documentation, databases, pages
- nano-banana-mcp: Image generation
- n8n-mcp: Workflow automation
- playwright-mcp: Browser automation
Best Practices for FrankX
- Server per domain: Keep MCPs focused (GitHub for code, Linear for tasks)
- Consistent naming:
mcp__<server>__<action>format - Graceful fallbacks: If MCP unavailable, suggest alternative
- Context awareness: Use right MCP for right task automatically
MCP servers extend Claude's capabilities with real-world integrations. Design them with clear responsibility, robust error handling, and security in mind.