Skip to content

MCP Best Practices and Optimization

Master the best practices for deploying, securing, and optimizing MCP servers in production environments. Learn from real-world experiences and avoid common pitfalls.

Key Principle: Only use MCP servers from trusted sources. Third-party servers have the same access as any code you run locally.

  1. Review Source Code

    • Check GitHub repository for the server
    • Look for recent updates and active maintenance
    • Review issues and security advisories
    • Verify the author’s reputation
  2. Analyze Permissions

    • What data does the server access?
    • What external APIs does it call?
    • Does it require write permissions?
    • Can it execute system commands?
  3. Test in Isolation

    • Run in a Docker container first
    • Use a virtual machine for untrusted servers
    • Monitor network traffic during testing
    • Check file system changes
ComponentSecurity MeasurePriority
AuthenticationUse environment variables for secretsCritical
NetworkRestrict to localhost for sensitive dataHigh
PermissionsRun with minimal required privilegesCritical
Data AccessUse read-only credentials when possibleHigh
LoggingAudit all operations without exposing secretsMedium
UpdatesKeep servers and dependencies updatedHigh

Best Practice: Never hardcode credentials

Terminal window
# .env.local (add to .gitignore)
DATABASE_URL=postgresql://readonly:pass@localhost/db
API_KEY=sk-proj-...
OAUTH_CLIENT_SECRET=...
# Load in shell
export $(cat .env.local | xargs)
# Use in MCP config
claude mcp add db -e DATABASE_URL=$DATABASE_URL -- \
npx -y @modelcontextprotocol/server-postgres

Local Servers (stdio)

Most Secure Option

  • No network exposure
  • Process isolation
  • Direct file system access control
  • Suitable for sensitive operations

Remote Servers (SSE/HTTP)

Requires Extra Care

  • Use HTTPS/TLS always
  • Implement rate limiting
  • Add authentication layers
  • Monitor for suspicious activity
Terminal window
# Example: Restrict MCP server to local access only
# iptables rule for Linux
sudo iptables -A INPUT -p tcp --dport 3845 -s 127.0.0.1 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 3845 -j DROP
# macOS pf rule
echo "block in proto tcp to any port 3845" | sudo pfctl -ef -
# Windows Firewall PowerShell
New-NetFirewallRule -DisplayName "Block MCP External" `
-Direction Inbound -LocalPort 3845 -Protocol TCP `
-Action Block -RemoteAddress !127.0.0.1
// Example: Sanitizing external content
server.tool(
'fetch_webpage',
'Safely fetch and process webpage content',
{ url: z.string().url() },
async ({ url }) => {
// Validate URL domain
const allowedDomains = ['docs.company.com', 'api.company.com'];
const urlObj = new URL(url);
if (!allowedDomains.includes(urlObj.hostname)) {
throw new Error('URL domain not allowed');
}
// Fetch with timeout and size limit
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { 'User-Agent': 'MCP-Server/1.0' },
});
// Limit response size
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength) > 1_000_000) {
throw new Error('Response too large');
}
const text = await response.text();
// Sanitize content
const sanitized = text
.replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove scripts
.replace(/\bignore previous instructions\b/gi, '[REDACTED]') // Basic injection filter
.substring(0, 50000); // Truncate to reasonable size
return {
content: [{
type: 'text',
text: `Content from ${url}:\n\n${sanitized}`,
}],
};
} finally {
clearTimeout(timeout);
}
}
);

The Challenge: Large responses can fill AI context windows, reducing effectiveness and increasing costs.

server.tool(
'search_large_dataset',
'Search with automatic pagination',
{
query: z.string(),
page: z.number().optional().default(1),
pageSize: z.number().optional().default(20),
},
async ({ query, page, pageSize }) => {
const results = await searchDatabase(query, {
offset: (page - 1) * pageSize,
limit: pageSize,
});
const hasMore = results.total > page * pageSize;
return {
content: [{
type: 'text',
text: JSON.stringify({
results: results.items,
page,
pageSize,
total: results.total,
hasMore,
nextPage: hasMore ? page + 1 : null,
}, null, 2),
}],
};
}
);
class MCPCache {
private cache = new Map<string, { data: any; expires: number }>();
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
set(key: string, data: any, ttl = this.DEFAULT_TTL) {
this.cache.set(key, {
data,
expires: Date.now() + ttl,
});
}
get(key: string) {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.data;
}
// Implement cache key generation
static key(tool: string, params: any): string {
return `${tool}:${JSON.stringify(params, Object.keys(params).sort())}`;
}
}
const cache = new MCPCache();
server.tool(
'expensive_operation',
'Cached expensive operation',
{ input: z.string() },
async ({ input }) => {
const cacheKey = MCPCache.key('expensive_operation', { input });
const cached = cache.get(cacheKey);
if (cached) {
return {
content: [{
type: 'text',
text: `[Cached] ${cached}`,
}],
};
}
const result = await performExpensiveOperation(input);
cache.set(cacheKey, result);
return {
content: [{ type: 'text', text: result }],
};
}
);
// Redis-based caching for shared state
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
server.tool(
'get_team_metrics',
'Fetch team metrics with distributed cache',
{ teamId: z.string() },
async ({ teamId }) => {
const cacheKey = `metrics:${teamId}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return {
content: [{
type: 'text',
text: cached,
}],
};
}
// Compute metrics
const metrics = await computeTeamMetrics(teamId);
const result = JSON.stringify(metrics, null, 2);
// Cache with expiration
await redis.setex(cacheKey, 300, result); // 5 minute TTL
return {
content: [{ type: 'text', text: result }],
};
}
);
// Efficient database connection management
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000, // Fail fast on connection
});
// Health check
setInterval(async () => {
try {
await pool.query('SELECT 1');
} catch (error) {
console.error('Database health check failed:', error);
}
}, 30000);
// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
// Monitor and limit memory usage
const v8 = require('v8');
function getMemoryUsage() {
const heap = v8.getHeapStatistics();
return {
used: Math.round(heap.used_heap_size / 1024 / 1024),
total: Math.round(heap.total_heap_size / 1024 / 1024),
limit: Math.round(heap.heap_size_limit / 1024 / 1024),
};
}
// Memory pressure detection
server.tool(
'process_large_file',
'Process file with memory management',
{ filePath: z.string() },
async ({ filePath }) => {
const memory = getMemoryUsage();
// Check memory before processing
if (memory.used / memory.limit > 0.8) {
// Force garbage collection if available
if (global.gc) {
global.gc();
}
// If still high, refuse operation
const newMemory = getMemoryUsage();
if (newMemory.used / newMemory.limit > 0.8) {
return {
content: [{
type: 'text',
text: 'Server under memory pressure. Please try again later.',
}],
};
}
}
// Process with streaming to minimize memory
const stream = createReadStream(filePath);
const chunks = [];
for await (const chunk of stream) {
// Process chunk by chunk
chunks.push(processChunk(chunk));
// Yield control periodically
if (chunks.length % 100 === 0) {
await new Promise(resolve => setImmediate(resolve));
}
}
return {
content: [{
type: 'text',
text: `Processed ${chunks.length} chunks successfully`,
}],
};
}
);
  1. Structured Logging

    import winston from 'winston';
    const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
    ),
    transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'mcp-server.log' }),
    ],
    });
    // Log tool usage
    server.use((tool, params, next) => {
    const start = Date.now();
    logger.info('Tool called', { tool, params: sanitizeParams(params) });
    next().then(result => {
    logger.info('Tool completed', {
    tool,
    duration: Date.now() - start,
    success: true,
    });
    }).catch(error => {
    logger.error('Tool failed', {
    tool,
    duration: Date.now() - start,
    error: error.message,
    });
    });
    });
  2. Metrics Collection

    import { Counter, Histogram, register } from 'prom-client';
    const toolCallCounter = new Counter({
    name: 'mcp_tool_calls_total',
    help: 'Total number of tool calls',
    labelNames: ['tool', 'status'],
    });
    const toolDuration = new Histogram({
    name: 'mcp_tool_duration_seconds',
    help: 'Tool execution duration',
    labelNames: ['tool'],
    buckets: [0.1, 0.5, 1, 2, 5, 10],
    });
    // Expose metrics endpoint for Prometheus
    app.get('/metrics', (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(register.metrics());
    });
  3. Health Checks

    server.tool(
    'health_check',
    'System health status',
    {},
    async () => {
    const checks = await Promise.allSettled([
    checkDatabase(),
    checkExternalAPI(),
    checkDiskSpace(),
    checkMemory(),
    ]);
    const results = checks.map((check, index) => ({
    component: ['database', 'api', 'disk', 'memory'][index],
    status: check.status === 'fulfilled' ? 'healthy' : 'unhealthy',
    details: check.status === 'fulfilled' ? check.value : check.reason,
    }));
    const allHealthy = results.every(r => r.status === 'healthy');
    return {
    content: [{
    type: 'text',
    text: JSON.stringify({
    status: allHealthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    checks: results,
    }, null, 2),
    }],
    };
    }
    );
// Fallback strategies for external dependencies
server.tool(
'get_weather',
'Get weather with fallback providers',
{ location: z.string() },
async ({ location }) => {
const providers = [
{ name: 'primary', fn: () => getPrimaryWeather(location) },
{ name: 'secondary', fn: () => getSecondaryWeather(location) },
{ name: 'cache', fn: () => getCachedWeather(location) },
];
for (const provider of providers) {
try {
const result = await withTimeout(provider.fn(), 3000);
if (result) {
return {
content: [{
type: 'text',
text: `Weather from ${provider.name}: ${JSON.stringify(result)}`,
}],
};
}
} catch (error) {
logger.warn(`Weather provider ${provider.name} failed`, { error });
continue;
}
}
// All providers failed
return {
content: [{
type: 'text',
text: 'Weather data temporarily unavailable. Please try again later.',
}],
};
}
);
class CircuitBreaker {
private failures = 0;
private lastFailTime = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold = 5,
private timeout = 60000 // 1 minute
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailTime > this.timeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
if (this.state === 'half-open') {
this.state = 'closed';
this.failures = 0;
}
return result;
} catch (error) {
this.failures++;
this.lastFailTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
throw error;
}
}
}
const apiBreaker = new CircuitBreaker();
server.tool(
'call_external_api',
'Call external API with circuit breaker',
{ endpoint: z.string() },
async ({ endpoint }) => {
try {
const result = await apiBreaker.call(() =>
fetch(endpoint).then(r => r.json())
);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
if (error.message === 'Circuit breaker is open') {
return {
content: [{
type: 'text',
text: 'Service temporarily unavailable due to repeated failures',
}],
};
}
throw error;
}
}
);
// .mcp.json - Version controlled team configuration
{
"mcpServers": {
"team-database": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "${DATABASE_URL}", // From environment
"POOL_SIZE": "10",
"STATEMENT_TIMEOUT": "30s"
}
},
"team-apis": {
"command": "npx",
"args": ["-y", "@ourcompany/mcp-internal-apis"],
"env": {
"API_BASE_URL": "${INTERNAL_API_URL}",
"API_KEY": "${INTERNAL_API_KEY}"
}
}
}
}
# MCP Server: Internal Tools
## Overview
Provides access to internal company tools and services.
## Available Tools
### `search_knowledge_base`
Search internal documentation and wikis.
**Parameters:**
- `query` (string, required): Search query
- `category` (string, optional): Filter by category
- `limit` (number, optional): Max results (default: 10)
**Example:**

“Search for deployment procedures for the payment service”

### `get_service_status`
Check health and status of internal services.
**Parameters:**
- `service` (string, optional): Specific service name
**Example:**

“Check the status of the authentication service”

## Security Notes
- Requires VPN connection
- Uses service account with read-only access
- Logs all queries for audit purposes
## Troubleshooting
- Error "Connection refused": Ensure VPN is connected
- Error "Unauthorized": Check API key expiration

Compress Responses

Remove unnecessary whitespace and formatting from JSON responses when appropriate.

Use Summaries

Provide summaries by default, with options for detailed data only when needed.

Implement Filters

Allow filtering at the source to reduce data transfer and processing.

Cache Aggressively

Cache frequently requested data to avoid redundant processing.

import rateLimit from 'express-rate-limit';
// Per-user rate limiting for remote servers
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each user to 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
});
// Tool-specific rate limiting
const toolLimiters = new Map();
function getToolLimiter(tool: string, maxRequests = 10) {
if (!toolLimiters.has(tool)) {
toolLimiters.set(tool, new Map());
}
return (userId: string) => {
const userLimiters = toolLimiters.get(tool);
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
const requests = userLimiters.get(userId) || [];
const recentRequests = requests.filter(t => t > windowStart);
if (recentRequests.length >= maxRequests) {
throw new Error(`Rate limit exceeded for ${tool}`);
}
recentRequests.push(now);
userLimiters.set(userId, recentRequests);
};
}
// Support multiple API versions
server.tool(
'api_call',
'Versioned API endpoint',
{
endpoint: z.string(),
version: z.enum(['v1', 'v2']).optional().default('v2'),
},
async ({ endpoint, version }) => {
const handlers = {
v1: () => callV1API(endpoint),
v2: () => callV2API(endpoint),
};
const result = await handlers[version]();
// Add deprecation warning for old versions
if (version === 'v1') {
return {
content: [{
type: 'text',
text: `[DEPRECATION WARNING] API v1 will be removed on 2025-12-31. Please use v2.\n\n${JSON.stringify(result)}`,
}],
};
}
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
}
);
// Feature flags for gradual rollout
const features = {
enhancedSearch: process.env.FEATURE_ENHANCED_SEARCH === 'true',
aiSummaries: process.env.FEATURE_AI_SUMMARIES === 'true',
caching: process.env.FEATURE_CACHING !== 'false', // Default on
};
server.tool(
'search',
'Search with feature flags',
{ query: z.string() },
async ({ query }) => {
let results;
if (features.enhancedSearch) {
results = await enhancedSearch(query);
} else {
results = await basicSearch(query);
}
if (features.aiSummaries && results.length > 0) {
results = await addAISummaries(results);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
results,
features: Object.entries(features)
.filter(([_, enabled]) => enabled)
.map(([name]) => name),
}),
}],
};
}
);
  • Security: Environment variables for secrets, validate all inputs
  • Performance: Implement caching, paginate large results
  • Reliability: Add timeouts, retry logic, circuit breakers
  • Monitoring: Structure logs, collect metrics, implement health checks
  • Documentation: Keep README current, document all tools
  • Testing: Unit tests, integration tests, load testing
  • Updates: Regular dependency updates, security patches

Remember: MCP servers are powerful tools that require the same operational excellence as any production service. Treat them with the same care you would any critical infrastructure component.