MCP Server Integration Guide for Claude Code
How to build and integrate Model Context Protocol servers that connect Claude Code to your internal systems, databases, and APIs.
Understanding MCP
Model Context Protocol (MCP) is an open standard that enables AI assistants like Claude Code to interact with external systems in a structured, secure way. Think of MCP as the bridge between Claude’s intelligence and your organization’s data and tools.
Without MCP, Claude Code operates with whatever context you paste into the conversation. You copy code snippets, describe your architecture, explain your systems. It works, but it’s manual and limited.
With MCP, Claude Code can:
- Query your internal documentation database directly
- Read your codebase structure and patterns
- Access your API specifications
- Look up information in your knowledge base
- Execute approved operations in your systems
The key insight: MCP servers don’t give Claude Code direct access to systems. They provide a curated, controlled interface. You decide exactly what capabilities to expose and how.
MCP Architecture
The MCP architecture has three components:
Clients (Claude Code): Request tools and resources from servers. Claude Code is an MCP client—it discovers what servers offer and uses those capabilities.
Servers: Expose specific tools and resources. A server might provide access to your documentation, your API specs, your codebase structure, or specific operations.
Transport: The communication layer between clients and servers. MCP supports stdio (local) and HTTP/SSE (remote) transports.
┌─────────────┐ MCP Protocol ┌─────────────┐ Internal ┌─────────────┐
│ │ ◄──────────────────► │ │ ◄──────────────► │ │
│ Claude Code │ (stdio/HTTP) │ MCP Server │ (API/SQL/etc) │ Your │
│ (Client) │ │ │ │ Systems │
└─────────────┘ └─────────────┘ └─────────────┘
Tools vs. Resources
MCP servers expose two types of capabilities:
Tools are actions Claude Code can take. They’re like functions:
// Example: A tool to search documentation
tool: search_docs
inputs:
query: string // Search query
limit: number // Max results
output:
results: Array<{title, excerpt, url}>
Claude Code decides when to use tools based on the conversation. “Find documentation about authentication” would trigger the search_docs tool.
Resources are data Claude Code can access. They’re like files or databases:
// Example: A resource providing API specs
resource: api_spec
uri: api://specs/users
output:
openapi: string // OpenAPI specification
Resources provide context. Tools enable actions. Most useful MCP servers provide both.
Building MCP Servers
Let’s build a practical MCP server. We’ll create a documentation server that gives Claude Code access to your internal docs.
Server Structure
A minimal MCP server in TypeScript:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server(
{
name: 'docs-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Define tools
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'search_docs',
description: 'Search internal documentation',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', default: 10 },
},
required: ['query'],
},
},
],
}));
// Handle tool calls
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'search_docs') {
const results = await searchDocumentation(args.query, args.limit);
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
}
throw new Error(`Unknown tool: ${name}`);
});
// Define resources
server.setRequestHandler('resources/list', async () => ({
resources: [
{
uri: 'docs://architecture/overview',
name: 'Architecture Overview',
description: 'System architecture documentation',
mimeType: 'text/markdown',
},
],
}));
// Handle resource reads
server.setRequestHandler('resources/read', async (request) => {
const { uri } = request.params;
if (uri === 'docs://architecture/overview') {
const content = await readDocFile('architecture/overview.md');
return { contents: [{ uri, text: content, mimeType: 'text/markdown' }] };
}
throw new Error(`Unknown resource: ${uri}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
Integration Patterns
Different systems require different integration approaches.
Database Integration
Connect Claude Code to your internal databases for queries (not modifications):
// Database server example
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'query_users') {
// Use parameterized queries to prevent injection
const result = await db.query(
'SELECT id, name, email FROM users WHERE department = $1 LIMIT $2',
[args.department, args.limit || 10]
);
return {
content: [{
type: 'text',
text: JSON.stringify(result.rows)
}]
};
}
});
API Integration
Expose your internal APIs through MCP:
// API gateway server
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'get_user_details') {
const response = await fetch(
`${INTERNAL_API}/users/${args.userId}`,
{
headers: {
'Authorization': `Bearer ${SERVICE_TOKEN}`,
'X-Request-Source': 'mcp-server',
},
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Filter sensitive fields before returning
const safeData = {
id: data.id,
name: data.name,
department: data.department,
// Exclude: email, phone, address, etc.
};
return {
content: [{
type: 'text',
text: JSON.stringify(safeData)
}]
};
}
});
File System Integration
Provide access to specific directories:
// Code context server
server.setRequestHandler('resources/list', async () => {
const files = await glob('src/**/*.ts');
return {
resources: files.map(file => ({
uri: `file://${file}`,
name: path.basename(file),
description: `Source file: ${file}`,
mimeType: 'text/typescript',
})),
};
});
server.setRequestHandler('resources/read', async (request) => {
const { uri } = request.params;
// Security: Validate path is within allowed directory
const filePath = uri.replace('file://', '');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(ALLOWED_DIR)) {
throw new Error('Access denied: Path outside allowed directory');
}
const content = await fs.readFile(resolvedPath, 'utf-8');
return {
contents: [{
uri,
text: content,
mimeType: 'text/typescript',
}],
};
});
Security Considerations
MCP servers can be powerful attack vectors if not properly secured. Security operates at multiple levels.
Authentication
Who can connect to your MCP server?
Local Servers (stdio transport):
- Process-level security: Only Claude Code process can connect
- User-level access: Inherits user’s permissions
- Low risk for read-only operations
Remote Servers (HTTP transport):
- Require authentication for all connections
- Use OAuth2 or JWT tokens
- Validate tokens on every request
// Authentication middleware for HTTP transport
async function authenticateRequest(request: Request): Promise<User> {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
throw new AuthError('Missing authorization token');
}
try {
const payload = await verifyJWT(token);
return await getUser(payload.sub);
} catch (error) {
throw new AuthError('Invalid token');
}
}
Authorization
What can authenticated users do?
Tool-Level Authorization:
const toolPermissions = {
'search_docs': ['all_users'],
'query_database': ['engineers', 'data_team'],
'modify_settings': ['admins'],
};
server.setRequestHandler('tools/call', async (request) => {
const user = await authenticateRequest(request);
const { name } = request.params;
const allowedRoles = toolPermissions[name] || [];
if (!allowedRoles.includes('all_users') &&
!user.roles.some(r => allowedRoles.includes(r))) {
throw new AuthError(`User lacks permission for tool: ${name}`);
}
// Proceed with tool execution
});
Resource-Level Authorization:
server.setRequestHandler('resources/read', async (request) => {
const user = await authenticateRequest(request);
const { uri } = request.params;
// Check if user has access to this resource
const hasAccess = await checkResourceAccess(user, uri);
if (!hasAccess) {
throw new AuthError(`Access denied to resource: ${uri}`);
}
// Proceed with read
});
Input Validation
Never trust inputs from Claude Code:
import { z } from 'zod';
const SearchDocsInput = z.object({
query: z.string().min(1).max(500),
limit: z.number().int().min(1).max(100).default(10),
department: z.enum(['engineering', 'product', 'design']).optional(),
});
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'search_docs') {
// Validate inputs
const validatedArgs = SearchDocsInput.parse(args);
// Use validated inputs
const results = await searchDocs(validatedArgs);
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
}
});
Rate Limiting
Prevent abuse and protect backend systems:
import { RateLimiter } from 'limiter';
const limiters = new Map<string, RateLimiter>();
function getRateLimiter(userId: string): RateLimiter {
if (!limiters.has(userId)) {
limiters.set(userId, new RateLimiter({
tokensPerInterval: 100,
interval: 'minute',
}));
}
return limiters.get(userId)!;
}
server.setRequestHandler('tools/call', async (request) => {
const user = await authenticateRequest(request);
const limiter = getRateLimiter(user.id);
if (!await limiter.removeTokens(1)) {
throw new RateLimitError('Rate limit exceeded');
}
// Proceed with request
});
Audit Logging
Log all MCP server activity:
interface AuditLog {
timestamp: Date;
userId: string;
action: 'tool_call' | 'resource_read';
target: string;
inputs: object;
result: 'success' | 'error';
duration: number;
errorMessage?: string;
}
async function logAudit(log: AuditLog): Promise<void> {
await auditDatabase.insert(log);
// Alert on suspicious patterns
if (await detectAnomalous(log)) {
await alertSecurityTeam(log);
}
}
Deployment Patterns
How you deploy MCP servers affects security, performance, and maintainability.
Local Deployment
For development and personal use:
// Claude Code configuration
{
"mcpServers": {
"docs": {
"command": "node",
"args": ["/path/to/docs-server/index.js"],
"env": {
"DOCS_PATH": "/path/to/docs"
}
}
}
}
Pros: Simple, fast, no network overhead Cons: Only works locally, no central management
Containerized Deployment
For team-wide access:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yaml
services:
mcp-docs:
build: ./mcp-servers/docs
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- AUTH_SECRET=${AUTH_SECRET}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
Kubernetes Deployment
For enterprise scale:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-docs-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-docs
template:
metadata:
labels:
app: mcp-docs
spec:
containers:
- name: mcp-docs
image: company/mcp-docs:1.0.0
ports:
- containerPort: 3000
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: mcp-secrets
key: database-url
Performance Optimization
MCP servers sit between Claude Code and your systems. Performance matters.
Connection Pooling
Don’t create new connections per request:
import { Pool } from 'pg';
// Create pool once at startup
const pool = new Pool({
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Reuse connections for requests
server.setRequestHandler('tools/call', async (request) => {
const client = await pool.connect();
try {
const result = await client.query('SELECT ...');
return { content: [{ type: 'text', text: JSON.stringify(result.rows) }] };
} finally {
client.release();
}
});
Caching
Cache frequently accessed data:
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 1000 * 60 * 5, // 5 minutes
});
async function getCachedDocs(query: string): Promise<any[]> {
const cacheKey = `docs:${query}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const results = await searchDocs(query);
cache.set(cacheKey, results);
return results;
}
Timeouts
Set reasonable timeouts:
const TOOL_TIMEOUT = 30000; // 30 seconds
server.setRequestHandler('tools/call', async (request) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT);
try {
const result = await executeToolWithSignal(
request,
controller.signal
);
return result;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Tool execution timed out');
}
throw error;
} finally {
clearTimeout(timeout);
}
});
Getting Started
Ready to build MCP servers for your organization?
- Start with read-only: Build a documentation or codebase server first
- Implement authentication: Even for internal servers
- Add logging: You need visibility into what’s being accessed
- Test thoroughly: MCP servers need testing like any production code
- Deploy carefully: Start with limited access, expand gradually
For teams wanting structured guidance on MCP server development, learn about our consulting services or read our guide on Claude Code Plugin Architecture.
This article is part of our plugin architecture series. For skill development, see Building Custom Claude Code Skills. For security considerations, read AI Governance and Security for Teams.