- HTTP_REMOTE_DEPLOYMENT_PLAN.md: High-level architecture and implementation plan - HTTP_IMPLEMENTATION_GUIDE.md: Detailed technical implementation with code examples - HTTP_IMPLEMENTATION_ROADMAP.md: Day-by-day implementation checklist and milestones - HTTP_REMOTE_SUMMARY.md: Executive summary with key findings and recommendations - Updated README.md with references to future HTTP deployment plans Key findings: - Claude Desktop currently only supports stdio transport (local execution) - mcp-remote adapter enables remote server connectivity as a bridge solution - Implementation requires adding StreamableHTTPServerTransport support - Dual-mode operation will maintain backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
20 KiB
20 KiB
HTTP Implementation Technical Guide
Deep Technical Analysis
Current MCP Transport Mechanism
The current implementation uses StdioServerTransport which:
- Reads JSON-RPC messages from stdin
- Writes responses to stdout
- Maintains a single, persistent connection
- Has implicit trust (local execution)
Target HTTP Transport Mechanism
The StreamableHTTPServerTransport:
- Accepts HTTP POST requests with JSON-RPC payloads
- Can upgrade to Server-Sent Events (SSE) for server-initiated messages
- Requires session management for state persistence
- Needs explicit authentication
Detailed Implementation Steps
Step 1: Install Required Dependencies
npm install express cors helmet compression dotenv
npm install --save-dev @types/express @types/cors
Step 2: Create HTTP Server Structure
// src/mcp/transports/http-transport.ts
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import { randomUUID } from 'crypto';
import {
StreamableHTTPServerTransport,
StreamableHTTPServerTransportOptions
} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { logger } from '../../utils/logger';
export interface HTTPServerConfig {
port: number;
host: string;
authToken?: string;
corsOrigins?: string[];
sessionTimeout?: number; // in milliseconds
maxSessions?: number;
}
export interface MCPSession {
id: string;
transport: StreamableHTTPServerTransport;
server: any; // Your MCP server instance
createdAt: Date;
lastActivity: Date;
metadata?: Record<string, any>;
}
export class HTTPTransportServer {
private app: express.Application;
private sessions: Map<string, MCPSession> = new Map();
private config: HTTPServerConfig;
private cleanupInterval?: NodeJS.Timeout;
constructor(config: HTTPServerConfig) {
this.config = {
sessionTimeout: 30 * 60 * 1000, // 30 minutes default
maxSessions: 100,
...config
};
this.app = express();
this.setupMiddleware();
this.setupRoutes();
this.startSessionCleanup();
}
private setupMiddleware(): void {
// Security headers
this.app.use(helmet());
// CORS configuration
this.app.use(cors({
origin: this.config.corsOrigins || true,
credentials: true,
methods: ['POST', 'GET', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'MCP-Session-ID']
}));
// Compression
this.app.use(compression());
// JSON parsing with size limit
this.app.use(express.json({ limit: '10mb' }));
// Request logging
this.app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
sessionId: req.headers['mcp-session-id'],
ip: req.ip
});
next();
});
}
private setupRoutes(): void {
// Health check endpoint
this.app.get('/health', (req, res) => {
res.json({
status: 'ok',
sessions: this.sessions.size,
uptime: process.uptime()
});
});
// Main MCP endpoint
this.app.post('/mcp',
this.authenticateRequest.bind(this),
this.handleMCPRequest.bind(this)
);
// Session management endpoint
this.app.get('/sessions',
this.authenticateRequest.bind(this),
(req, res) => {
const sessionInfo = Array.from(this.sessions.entries()).map(([id, session]) => ({
id,
createdAt: session.createdAt,
lastActivity: session.lastActivity,
metadata: session.metadata
}));
res.json({ sessions: sessionInfo });
}
);
}
private authenticateRequest(req: Request, res: Response, next: NextFunction): void {
if (!this.config.authToken) {
return next();
}
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
if (token !== this.config.authToken) {
logger.warn('Authentication failed', { ip: req.ip });
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
private async handleMCPRequest(req: Request, res: Response): Promise<void> {
try {
const sessionId = req.headers['mcp-session-id'] as string;
let session = sessionId ? this.sessions.get(sessionId) : null;
// Create new session if needed
if (!session) {
if (this.sessions.size >= this.config.maxSessions!) {
return res.status(503).json({ error: 'Server at capacity' });
}
session = await this.createSession();
res.setHeader('MCP-Session-ID', session.id);
}
// Update last activity
session.lastActivity = new Date();
// Handle the request through the transport
await session.transport.handleRequest(req, res);
} catch (error) {
logger.error('Error handling MCP request', error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
private async createSession(): Promise<MCPSession> {
const id = randomUUID();
const transport = new StreamableHTTPServerTransport();
// Create your MCP server instance here
const { N8NDocumentationMCPServer } = await import('../server-update');
const server = new N8NDocumentationMCPServer();
// Connect transport to server
await server.connect(transport);
const session: MCPSession = {
id,
transport,
server,
createdAt: new Date(),
lastActivity: new Date()
};
this.sessions.set(id, session);
logger.info('Created new session', { sessionId: id });
return session;
}
private startSessionCleanup(): void {
this.cleanupInterval = setInterval(() => {
const now = Date.now();
const timeout = this.config.sessionTimeout!;
for (const [id, session] of this.sessions.entries()) {
if (now - session.lastActivity.getTime() > timeout) {
this.destroySession(id);
}
}
}, 60000); // Check every minute
}
private destroySession(id: string): void {
const session = this.sessions.get(id);
if (session) {
// Cleanup server resources
if (session.server && typeof session.server.close === 'function') {
session.server.close();
}
this.sessions.delete(id);
logger.info('Destroyed session', { sessionId: id });
}
}
public start(): void {
this.app.listen(this.config.port, this.config.host, () => {
logger.info(`HTTP MCP Server listening on ${this.config.host}:${this.config.port}`);
});
}
public stop(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
// Cleanup all sessions
for (const id of this.sessions.keys()) {
this.destroySession(id);
}
}
}
Step 3: Modify MCP Server for Transport Flexibility
// src/mcp/server-update.ts modifications
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Transport } from '@modelcontextprotocol/sdk/types.js';
export class N8NDocumentationMCPServer {
private server: Server;
// ... existing code ...
// Add connect method to accept any transport
async connect(transport: Transport): Promise<void> {
await this.ensureInitialized();
await this.server.connect(transport);
logger.info('MCP Server connected with transport', {
transportType: transport.constructor.name
});
}
// Modify run method to be transport-agnostic
async run(transport?: Transport): Promise<void> {
await this.ensureInitialized();
if (!transport) {
// Default to stdio for backward compatibility
transport = new StdioServerTransport();
}
await this.connect(transport);
logger.info('n8n Documentation MCP Server running');
}
}
Step 4: Create Unified Entry Point
// src/mcp/index-universal.ts
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from './server-update';
import { HTTPTransportServer } from './transports/http-transport';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { logger } from '../utils/logger';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
interface CLIArgs {
mode: 'stdio' | 'http';
port?: number;
host?: string;
authToken?: string;
}
function parseArgs(): CLIArgs {
const args = process.argv.slice(2);
const config: CLIArgs = {
mode: 'stdio' // default
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--mode':
config.mode = args[++i] as 'stdio' | 'http';
break;
case '--port':
config.port = parseInt(args[++i]);
break;
case '--host':
config.host = args[++i];
break;
case '--auth-token':
config.authToken = args[++i];
break;
}
}
// Allow environment variables to override
config.mode = (process.env.MCP_MODE as any) || config.mode;
config.port = parseInt(process.env.MCP_PORT || '') || config.port || 3000;
config.host = process.env.MCP_HOST || config.host || '0.0.0.0';
config.authToken = process.env.MCP_AUTH_TOKEN || config.authToken;
return config;
}
async function main() {
try {
const config = parseArgs();
logger.info('Starting MCP server', config);
if (config.mode === 'http') {
// HTTP mode - server manages its own lifecycle
const httpServer = new HTTPTransportServer({
port: config.port!,
host: config.host!,
authToken: config.authToken,
corsOrigins: process.env.MCP_CORS_ORIGINS?.split(','),
sessionTimeout: parseInt(process.env.MCP_SESSION_TIMEOUT || '') || undefined,
maxSessions: parseInt(process.env.MCP_MAX_SESSIONS || '') || undefined
});
httpServer.start();
// Graceful shutdown
process.on('SIGINT', () => {
logger.info('Shutting down HTTP server...');
httpServer.stop();
process.exit(0);
});
} else {
// Stdio mode - traditional single instance
const server = new N8NDocumentationMCPServer();
await server.run(); // Uses stdio by default
}
} catch (error) {
logger.error('Failed to start MCP server', error);
process.exit(1);
}
}
if (require.main === module) {
main();
}
Step 5: Environment Configuration
# .env.example
# Server mode: stdio or http
MCP_MODE=http
# HTTP server configuration
MCP_PORT=3000
MCP_HOST=0.0.0.0
MCP_AUTH_TOKEN=your-secure-token-here
# CORS origins (comma-separated)
MCP_CORS_ORIGINS=https://claude.ai,http://localhost:3000
# Session management
MCP_SESSION_TIMEOUT=1800000 # 30 minutes in milliseconds
MCP_MAX_SESSIONS=100
# Existing configuration
NODE_ENV=production
LOG_LEVEL=info
Step 6: Docker Configuration for Remote Deployment
# Dockerfile.http
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Install production dependencies only
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/data ./data
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
# Start the server
CMD ["node", "dist/mcp/index-universal.js", "--mode", "http"]
Step 7: Production Deployment Script
#!/bin/bash
# deploy.sh
# Configuration
DOMAIN="mcp.your-domain.com"
EMAIL="your-email@example.com"
AUTH_TOKEN=$(openssl rand -base64 32)
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y docker.io docker-compose nginx certbot python3-certbot-nginx
# Clone repository
git clone https://github.com/yourusername/n8n-mcp.git
cd n8n-mcp
# Create .env file
cat > .env << EOF
MCP_MODE=http
MCP_PORT=3000
MCP_HOST=0.0.0.0
MCP_AUTH_TOKEN=$AUTH_TOKEN
MCP_CORS_ORIGINS=https://claude.ai
NODE_ENV=production
LOG_LEVEL=info
EOF
# Build and run with Docker
docker build -f Dockerfile.http -t n8n-mcp-http .
docker run -d \
--name n8n-mcp \
--restart always \
-p 127.0.0.1:3000:3000 \
--env-file .env \
n8n-mcp-http
# Configure Nginx
sudo tee /etc/nginx/sites-available/mcp << EOF
server {
listen 80;
server_name $DOMAIN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $DOMAIN;
# SSL will be configured by certbot
location /mcp {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
# Timeouts for long-running requests
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /health {
proxy_pass http://127.0.0.1:3000;
}
}
EOF
# Enable site
sudo ln -s /etc/nginx/sites-available/mcp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Get SSL certificate
sudo certbot --nginx -d $DOMAIN --email $EMAIL --agree-tos --non-interactive
echo "Deployment complete!"
echo "Your MCP server is available at: https://$DOMAIN/mcp"
echo "Auth token: $AUTH_TOKEN"
echo "Save this token - you'll need it for client configuration"
Step 8: Client Configuration with mcp-remote
// claude_desktop_config.json for remote server
{
"mcpServers": {
"n8n-remote": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"connect",
"https://mcp.your-domain.com/mcp"
],
"env": {
"MCP_AUTH_TOKEN": "your-auth-token-here"
}
}
}
}
Step 9: Monitoring and Logging
// src/utils/monitoring.ts
import { Request, Response, NextFunction } from 'express';
export interface RequestMetrics {
path: string;
method: string;
statusCode: number;
duration: number;
sessionId?: string;
timestamp: Date;
}
export class MonitoringService {
private metrics: RequestMetrics[] = [];
public middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const metric: RequestMetrics = {
path: req.path,
method: req.method,
statusCode: res.statusCode,
duration: Date.now() - start,
sessionId: req.headers['mcp-session-id'] as string,
timestamp: new Date()
};
this.metrics.push(metric);
// Keep only last 1000 metrics in memory
if (this.metrics.length > 1000) {
this.metrics.shift();
}
});
next();
};
}
public getMetrics() {
return {
requests: this.metrics.length,
avgDuration: this.calculateAverage('duration'),
errorRate: this.calculateErrorRate(),
activeSessions: new Set(this.metrics.map(m => m.sessionId)).size
};
}
private calculateAverage(field: keyof RequestMetrics): number {
if (this.metrics.length === 0) return 0;
const sum = this.metrics.reduce((acc, m) => acc + (m[field] as number || 0), 0);
return sum / this.metrics.length;
}
private calculateErrorRate(): number {
if (this.metrics.length === 0) return 0;
const errors = this.metrics.filter(m => m.statusCode >= 400).length;
return errors / this.metrics.length;
}
}
Security Considerations
1. Authentication Token Management
- Use strong, random tokens (minimum 32 characters)
- Rotate tokens regularly
- Never commit tokens to version control
- Use environment variables or secret management systems
2. Rate Limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/mcp', limiter);
3. Input Validation
- Validate JSON-RPC structure
- Limit request body size
- Sanitize any user inputs
- Use schema validation for MCP tool parameters
4. HTTPS/TLS
- Always use HTTPS in production
- Use strong TLS configurations
- Enable HSTS headers
- Consider certificate pinning for high-security deployments
Performance Optimization
1. Database Connection Pooling
Since we're using SQLite through our adapter, consider:
- Read-only replicas for query operations
- In-memory caching for frequently accessed nodes
- Connection pooling if switching to PostgreSQL
2. Response Caching
const nodeCache = new NodeCache({ stdTTL: 600 }); // 10 minute cache
// In your tool handlers
const cachedResult = nodeCache.get(cacheKey);
if (cachedResult) {
return cachedResult;
}
3. Compression
- Already implemented with compression middleware
- Consider additional optimizations for large responses
4. CDN Integration
- Serve static assets through CDN
- Cache API responses where appropriate
- Use geographic distribution for global access
Testing Strategy
1. Unit Tests
// src/test/http-transport.test.ts
describe('HTTPTransportServer', () => {
it('should create new session on first request', async () => {
// Test implementation
});
it('should reuse existing session', async () => {
// Test implementation
});
it('should cleanup expired sessions', async () => {
// Test implementation
});
});
2. Integration Tests
- Test full request/response cycle
- Verify authentication
- Test session persistence
- Validate error handling
3. Load Testing
# Using Apache Bench
ab -n 1000 -c 10 -H "Authorization: Bearer your-token" https://your-server/mcp
# Using k6
k6 run load-test.js
Troubleshooting Guide
Common Issues
-
Connection Refused
- Check firewall rules
- Verify nginx configuration
- Ensure Docker container is running
-
Authentication Failures
- Verify token format (Bearer prefix)
- Check environment variables
- Ensure token matches server configuration
-
Session Timeout
- Adjust MCP_SESSION_TIMEOUT
- Check client keep-alive settings
- Monitor server resources
-
Performance Issues
- Enable monitoring
- Check database query performance
- Review nginx access logs
- Monitor Docker container resources
Future Enhancements
-
WebSocket Support
- Implement full duplex communication
- Reduce latency for real-time updates
- Better support for server-initiated messages
-
OAuth2 Integration
- Support for third-party authentication
- User-specific access controls
- Integration with enterprise SSO
-
Multi-tenancy
- Separate databases per organization
- Role-based access control
- Usage tracking and quotas
-
Horizontal Scaling
- Redis for session storage
- Load balancer configuration
- Distributed caching
Conclusion
This implementation provides a robust foundation for running n8n-MCP as a remote HTTP service. The dual-mode support ensures backward compatibility while enabling new deployment scenarios. With proper security measures and monitoring in place, this solution can scale from single-user deployments to enterprise-wide installations.