π GitHub Agentic Workflows MCP Configuration
π Overview
This skill provides comprehensive guidance for configuring Model Context Protocol (MCP) servers in GitHub Agentic Workflows. MCP enables AI agents to interact with external tools and data sources through a standardized protocol. Understanding MCP configuration is essential for building powerful, extensible agentic workflows.
What is Model Context Protocol (MCP)?
Model Context Protocol (MCP) is a standardized protocol for connecting AI models to external tools, data sources, and services:
-
Standardized Interface: Consistent API for tool registration, discovery, and invocation
-
Multiple Transports: Support for stdio, HTTP, and Server-Sent Events (SSE)
-
Tool Discovery: Dynamic tool registration and capability discovery
-
Type Safety: JSON Schema validation for tool inputs and outputs
-
Lifecycle Management: Server startup, health checks, graceful shutdown
-
Error Handling: Structured error responses and retry mechanisms
Why Use MCP Servers?
MCP servers provide several benefits for agentic workflows:
-
β Extensibility: Add new tools without modifying agent code
-
β Reusability: Share MCP servers across multiple agents and projects
-
β Isolation: Run tools in separate processes for security and stability
-
β Standardization: Use community-maintained MCP servers
-
β Polyglot: Write servers in any language (Node.js, Python, Go, Rust)
-
β Discoverability: Agents automatically discover available tools
ποΈ MCP Architecture
System Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β GitHub Copilot Agent β β (AI Model + Orchestration) β ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ β Tool Calls β (JSON-RPC 2.0) βΌ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β MCP Client Runtime β β (Tool Discovery & Invocation) β βββ¬βββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββββ β stdio β HTTP β SSE βΌ βΌ βΌ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β Filesystem β β GitHub API β β Database β β MCP Server β β MCP Server β β MCP Server β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
Configuration File Structure
MCP servers are configured in .github/copilot-mcp.json :
{ "$schema": "https://github.com/modelcontextprotocol/schema/v1", "mcpServers": { "server-name": { "type": "local", "command": "command-to-run", "args": ["arg1", "arg2"], "env": { "ENV_VAR": "value" }, "tools": ["*"] } } }
π MCP Server Setup Patterns
Pattern 1: Local stdio Server
Use case: File system operations, git commands, local tools.
{ "mcpServers": { "filesystem": { "type": "local", "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/home/runner/work/myrepo/myrepo" ], "env": {}, "tools": ["*"] } } }
Implementation (Node.js):
// filesystem-mcp-server.js import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import fs from 'fs/promises'; import path from 'path';
class FileSystemMCPServer { constructor(rootPath) { this.rootPath = path.resolve(rootPath); this.server = new Server({ name: 'filesystem', version: '1.0.0', }, { capabilities: { tools: {}, }, });
this.setupTools();
}
setupTools() { // Register read_file tool this.server.setRequestHandler('tools/list', async () => ({ tools: [ { name: 'read_file', description: 'Read contents of a file', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path relative to root', }, }, required: ['path'], }, }, { name: 'write_file', description: 'Write contents to a file', inputSchema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' }, }, required: ['path', 'content'], }, }, { name: 'list_directory', description: 'List files in a directory', inputSchema: { type: 'object', properties: { path: { type: 'string' }, }, required: ['path'], }, }, ], }));
// Handle tool calls
this.server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'read_file':
return this.readFile(args.path);
case 'write_file':
return this.writeFile(args.path, args.content);
case 'list_directory':
return this.listDirectory(args.path);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
validatePath(filePath) { const resolved = path.resolve(this.rootPath, filePath); if (!resolved.startsWith(this.rootPath)) { throw new Error('Path outside root directory'); } return resolved; }
async readFile(filePath) { const validated = this.validatePath(filePath); const content = await fs.readFile(validated, 'utf8'); return { content: [ { type: 'text', text: content, }, ], }; }
async writeFile(filePath, content) {
const validated = this.validatePath(filePath);
await fs.writeFile(validated, content, 'utf8');
return {
content: [
{
type: 'text',
text: File written successfully: ${filePath},
},
],
};
}
async listDirectory(dirPath) { const validated = this.validatePath(dirPath); const entries = await fs.readdir(validated, { withFileTypes: true });
const files = entries.map(entry => ({
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file',
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(files, null, 2),
},
],
};
}
async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Filesystem MCP server started'); } }
// Start server const rootPath = process.argv[2] || process.cwd(); const server = new FileSystemMCPServer(rootPath); server.start().catch(console.error);
Usage:
Start server
npx -y @modelcontextprotocol/server-filesystem /workspace
Server communicates via stdin/stdout
Input (JSON-RPC request):
{"jsonrpc":"2.0","id":1,"method":"tools/list"}
Output (JSON-RPC response):
{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
Pattern 2: HTTP Server
Use case: Remote services, APIs, databases.
{ "mcpServers": { "github-api": { "type": "http", "url": "https://mcp.github.com/v1", "headers": { "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}" }, "tools": ["*"] } } }
Implementation (Node.js with Express):
// github-api-mcp-server.js import express from 'express'; import { Octokit } from '@octokit/rest';
const app = express(); app.use(express.json());
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, });
// MCP endpoint: List tools app.post('/mcp/tools/list', async (req, res) => { res.json({ tools: [ { name: 'github_create_issue', description: 'Create a GitHub issue', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, repo: { type: 'string' }, title: { type: 'string' }, body: { type: 'string' }, }, required: ['owner', 'repo', 'title'], }, }, { name: 'github_list_issues', description: 'List GitHub issues', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, repo: { type: 'string' }, state: { type: 'string', enum: ['open', 'closed', 'all'] }, }, required: ['owner', 'repo'], }, }, ], }); });
// MCP endpoint: Call tool app.post('/mcp/tools/call', async (req, res) => { const { name, arguments: args } = req.body;
try { switch (name) { case 'github_create_issue': { const { data } = await octokit.issues.create({ owner: args.owner, repo: args.repo, title: args.title, body: args.body, });
res.json({
content: [
{
type: 'text',
text: `Issue created: ${data.html_url}`,
},
],
});
break;
}
case 'github_list_issues': {
const { data } = await octokit.issues.listForRepo({
owner: args.owner,
repo: args.repo,
state: args.state || 'open',
});
res.json({
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
});
break;
}
default:
res.status(404).json({ error: 'Tool not found' });
}
} catch (error) { res.status(500).json({ error: error.message }); } });
// Health check app.get('/health', (req, res) => { res.json({ status: 'ok' }); });
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(GitHub API MCP server listening on port ${PORT});
});
Pattern 3: Server-Sent Events (SSE)
Use case: Real-time updates, streaming data, webhooks.
{ "mcpServers": { "realtime-monitor": { "type": "sse", "url": "https://monitor.example.com/events", "headers": { "Authorization": "Bearer ${{ secrets.API_TOKEN }}" }, "tools": ["*"] } } }
Implementation (Node.js with SSE):
// realtime-monitor-mcp-server.js import express from 'express';
const app = express();
const clients = new Set();
// SSE endpoint app.get('/events', (req, res) => { // Set headers for SSE res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive');
// Add client clients.add(res);
// Send initial connection event
res.write(event: connected\ndata: {"status":"connected"}\n\n);
// Remove client on disconnect req.on('close', () => { clients.delete(res); }); });
// Function to broadcast events to all clients
function broadcastEvent(eventType, data) {
const message = event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n;
for (const client of clients) { client.write(message); } }
// Tool: Subscribe to repository events app.post('/mcp/tools/call', express.json(), (req, res) => { const { name, arguments: args } = req.body;
if (name === 'subscribe_repo_events') { // Simulate subscription const subscription = { repo: args.repo, events: args.events, };
// Broadcast to SSE clients
broadcastEvent('tool_result', {
name: 'subscribe_repo_events',
result: `Subscribed to ${args.repo}`,
});
res.json({
content: [
{
type: 'text',
text: `Subscribed to events for ${args.repo}`,
},
],
});
} else { res.status(404).json({ error: 'Tool not found' }); } });
// Simulate events (for demo) setInterval(() => { broadcastEvent('repo_event', { type: 'push', repo: 'owner/repo', timestamp: new Date().toISOString(), }); }, 5000);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(Realtime monitor MCP server on port ${PORT});
});
π Transport Protocols
stdio Transport
Characteristics:
-
Process-to-process communication via stdin/stdout
-
Lowest latency
-
Best for local tools
-
Automatic lifecycle management
Advantages:
-
β Simple to implement
-
β No network overhead
-
β Automatic process cleanup
-
β Secure (no network exposure)
Disadvantages:
-
β Single client per server instance
-
β No remote access
-
β Requires process spawning
Configuration:
{ "mcpServers": { "local-tool": { "type": "local", "command": "node", "args": ["server.js"], "env": { "NODE_ENV": "production" }, "tools": ["*"] } } }
HTTP Transport
Characteristics:
-
RESTful JSON-RPC over HTTP/HTTPS
-
Stateless request/response
-
Can be load balanced
-
Supports authentication
Advantages:
-
β Remote server support
-
β Multiple concurrent clients
-
β Standard HTTP infrastructure
-
β Load balancing and scaling
Disadvantages:
-
β Higher latency
-
β Requires authentication
-
β Network security considerations
Configuration:
{ "mcpServers": { "remote-api": { "type": "http", "url": "https://api.example.com/mcp/v1", "headers": { "Authorization": "Bearer ${MCP_API_TOKEN}", "X-API-Version": "1.0" }, "timeout": 30000, "retries": 3, "tools": ["*"] } } }
Server-Sent Events (SSE) Transport
Characteristics:
-
One-way server-to-client streaming
-
Real-time event notifications
-
Automatic reconnection
-
HTTP-based
Advantages:
-
β Real-time updates
-
β Efficient for event streams
-
β Automatic reconnection
-
β Works through firewalls
Disadvantages:
-
β One-way only (server β client)
-
β Requires persistent connection
-
β Browser compatibility (not relevant for agents)
Configuration:
{ "mcpServers": { "event-stream": { "type": "sse", "url": "https://events.example.com/stream", "headers": { "Authorization": "Bearer ${EVENT_TOKEN}" }, "reconnect": true, "reconnectDelay": 5000, "tools": ["*"] } } }
β Configuration Validation
Schema Validation
// validate-mcp-config.js import Ajv from 'ajv'; import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true }); addFormats(ajv);
const mcpConfigSchema = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { mcpServers: { type: 'object', patternProperties: { '^[a-zA-Z0-9_-]+$': { oneOf: [ { // Local stdio server type: 'object', properties: { type: { const: 'local' }, command: { type: 'string', minLength: 1 }, args: { type: 'array', items: { type: 'string' } }, env: { type: 'object', patternProperties: { '^[A-Z_][A-Z0-9_]$': { type: 'string' }, }, }, tools: { oneOf: [ { type: 'array', items: { type: 'string' } }, { type: 'array', items: { const: '' }, maxItems: 1 }, ], }, }, required: ['type', 'command'], additionalProperties: false, }, { // HTTP server type: 'object', properties: { type: { const: 'http' }, url: { type: 'string', format: 'uri' }, headers: { type: 'object', patternProperties: { '^[A-Za-z0-9-]+$': { type: 'string' }, }, }, timeout: { type: 'integer', minimum: 1000 }, retries: { type: 'integer', minimum: 0 }, tools: { oneOf: [ { type: 'array', items: { type: 'string' } }, { type: 'array', items: { const: '' }, maxItems: 1 }, ], }, }, required: ['type', 'url'], additionalProperties: false, }, { // SSE server type: 'object', properties: { type: { const: 'sse' }, url: { type: 'string', format: 'uri' }, headers: { type: 'object', patternProperties: { '^[A-Za-z0-9-]+$': { type: 'string' }, }, }, reconnect: { type: 'boolean' }, reconnectDelay: { type: 'integer', minimum: 100 }, tools: { oneOf: [ { type: 'array', items: { type: 'string' } }, { type: 'array', items: { const: '' }, maxItems: 1 }, ], }, }, required: ['type', 'url'], additionalProperties: false, }, ], }, }, }, }, required: ['mcpServers'], additionalProperties: false, };
const validate = ajv.compile(mcpConfigSchema);
export function validateMCPConfig(config) { const valid = validate(config);
if (!valid) { const errors = validate.errors.map(err => ({ path: err.instancePath, message: err.message, params: err.params, }));
throw new Error(
`MCP configuration validation failed:\n${JSON.stringify(errors, null, 2)}`
);
}
return true; }
// Usage import fs from 'fs';
const config = JSON.parse( fs.readFileSync('.github/copilot-mcp.json', 'utf8') );
try { validateMCPConfig(config); console.log('β MCP configuration is valid'); } catch (error) { console.error('β Validation error:', error.message); process.exit(1); }
Runtime Validation
// runtime-validator.js class MCPConfigValidator { constructor(config) { this.config = config; }
async validateAll() { const errors = [];
for (const [name, server] of Object.entries(this.config.mcpServers)) {
try {
await this.validateServer(name, server);
} catch (error) {
errors.push({
server: name,
error: error.message,
});
}
}
if (errors.length > 0) {
throw new Error(
`MCP server validation failed:\n${JSON.stringify(errors, null, 2)}`
);
}
return true;
}
async validateServer(name, server) {
switch (server.type) {
case 'local':
await this.validateLocalServer(name, server);
break;
case 'http':
await this.validateHTTPServer(name, server);
break;
case 'sse':
await this.validateSSEServer(name, server);
break;
default:
throw new Error(Unknown server type: ${server.type});
}
}
async validateLocalServer(name, server) { // Check if command exists const { execSync } = require('child_process');
try {
execSync(`command -v ${server.command}`, { stdio: 'ignore' });
} catch (error) {
throw new Error(`Command not found: ${server.command}`);
}
// Validate environment variables
if (server.env) {
for (const [key, value] of Object.entries(server.env)) {
if (value.includes('${') && value.includes('}')) {
const envVar = value.match(/\$\{([^}]+)\}/)[1];
if (!process.env[envVar]) {
throw new Error(`Environment variable not set: ${envVar}`);
}
}
}
}
}
async validateHTTPServer(name, server) {
// Health check
try {
const response = await fetch(${server.url}/health, {
headers: server.headers || {},
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
throw new Error(`Health check failed: ${response.status}`);
}
} catch (error) {
throw new Error(`Cannot connect to HTTP server: ${error.message}`);
}
}
async validateSSEServer(name, server) { // Test SSE connection return new Promise((resolve, reject) => { const eventSource = new EventSource(server.url, { headers: server.headers || {}, });
const timeout = setTimeout(() => {
eventSource.close();
reject(new Error('SSE connection timeout'));
}, 5000);
eventSource.addEventListener('connected', () => {
clearTimeout(timeout);
eventSource.close();
resolve();
});
eventSource.onerror = (error) => {
clearTimeout(timeout);
eventSource.close();
reject(new Error(`SSE connection error: ${error.message}`));
};
});
} }
// Usage const validator = new MCPConfigValidator(config); await validator.validateAll();
π Server Lifecycle Management
Startup Sequence
// mcp-lifecycle-manager.js class MCPLifecycleManager { constructor(config) { this.config = config; this.servers = new Map(); this.health = new Map(); }
async startAll() { console.log('π Starting MCP servers...');
const promises = Object.entries(this.config.mcpServers).map(
async ([name, server]) => {
try {
await this.startServer(name, server);
console.log(`β
Started: ${name}`);
} catch (error) {
console.error(`β Failed to start ${name}:`, error.message);
throw error;
}
}
);
await Promise.all(promises);
console.log('β
All MCP servers started');
}
async startServer(name, config) {
switch (config.type) {
case 'local':
return this.startLocalServer(name, config);
case 'http':
return this.startHTTPClient(name, config);
case 'sse':
return this.startSSEClient(name, config);
default:
throw new Error(Unknown server type: ${config.type});
}
}
async startLocalServer(name, config) { const { spawn } = require('child_process');
// Spawn process
const process = spawn(config.command, config.args || [], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...config.env },
});
// Wait for server to be ready
await this.waitForReady(process);
this.servers.set(name, { type: 'local', process });
this.health.set(name, 'healthy');
// Monitor health
this.monitorHealth(name, process);
}
async waitForReady(process, timeout = 10000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Server startup timeout')); }, timeout);
process.stderr.once('data', (data) => {
const message = data.toString();
if (message.includes('started') || message.includes('listening')) {
clearTimeout(timer);
resolve();
}
});
process.once('error', (error) => {
clearTimeout(timer);
reject(error);
});
process.once('exit', (code) => {
clearTimeout(timer);
reject(new Error(`Process exited with code ${code}`));
});
});
}
monitorHealth(name, process) {
// Monitor process health
process.on('exit', (code) => {
console.error(β Server ${name} exited with code ${code});
this.health.set(name, 'unhealthy');
// Auto-restart
if (code !== 0) {
console.log(`π Restarting ${name}...`);
setTimeout(() => {
this.restartServer(name);
}, 5000);
}
});
process.on('error', (error) => {
console.error(`β Server ${name} error:`, error.message);
this.health.set(name, 'unhealthy');
});
// Periodic health check
setInterval(async () => {
const healthy = await this.checkHealth(name);
this.health.set(name, healthy ? 'healthy' : 'unhealthy');
}, 30000); // Every 30 seconds
}
async checkHealth(name) { const server = this.servers.get(name); if (!server) return false;
switch (server.type) {
case 'local':
// Check if process is running
return !server.process.killed;
case 'http':
// HTTP health check
try {
const response = await fetch(`${server.url}/health`, {
signal: AbortSignal.timeout(5000),
});
return response.ok;
} catch (error) {
return false;
}
case 'sse':
// Check SSE connection
return server.connected;
default:
return false;
}
}
async restartServer(name) { const config = this.config.mcpServers[name];
// Stop existing server
await this.stopServer(name);
// Start new instance
await this.startServer(name, config);
}
async stopServer(name) { const server = this.servers.get(name); if (!server) return;
switch (server.type) {
case 'local':
server.process.kill('SIGTERM');
// Wait for graceful shutdown
await new Promise((resolve) => {
const timeout = setTimeout(() => {
server.process.kill('SIGKILL');
resolve();
}, 5000);
server.process.once('exit', () => {
clearTimeout(timeout);
resolve();
});
});
break;
case 'http':
case 'sse':
// Close connections
if (server.connection) {
server.connection.close();
}
break;
}
this.servers.delete(name);
this.health.delete(name);
}
async stopAll() { console.log('π Stopping MCP servers...');
const promises = Array.from(this.servers.keys()).map(
async (name) => {
try {
await this.stopServer(name);
console.log(`β
Stopped: ${name}`);
} catch (error) {
console.error(`β Failed to stop ${name}:`, error.message);
}
}
);
await Promise.all(promises);
console.log('β
All MCP servers stopped');
}
getHealthStatus() { const status = {}; for (const [name, health] of this.health) { status[name] = health; } return status; } }
// Usage const manager = new MCPLifecycleManager(config);
// Start all servers await manager.startAll();
// Check health console.log('Health status:', manager.getHealthStatus());
// Graceful shutdown process.on('SIGTERM', async () => { await manager.stopAll(); process.exit(0); });
π Tool Discovery and Registration
Dynamic Tool Discovery
// tool-discovery.js class MCPToolDiscovery { constructor(servers) { this.servers = servers; this.tools = new Map(); }
async discoverAll() { console.log('π Discovering MCP tools...');
for (const [serverName, server] of this.servers) {
try {
const tools = await this.discoverTools(serverName, server);
for (const tool of tools) {
this.registerTool(serverName, tool);
}
console.log(`β
Discovered ${tools.length} tools from ${serverName}`);
} catch (error) {
console.error(`β Failed to discover tools from ${serverName}:`, error.message);
}
}
console.log(`β
Total tools discovered: ${this.tools.size}`);
}
async discoverTools(serverName, server) {
switch (server.type) {
case 'local':
return this.discoverLocalTools(server);
case 'http':
return this.discoverHTTPTools(server);
case 'sse':
return this.discoverSSETools(server);
default:
throw new Error(Unknown server type: ${server.type});
}
}
async discoverLocalTools(server) { // Send tools/list request via stdio return new Promise((resolve, reject) => { const request = { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {}, };
server.process.stdin.write(JSON.stringify(request) + '\n');
server.process.stdout.once('data', (data) => {
const response = JSON.parse(data.toString());
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.result.tools);
}
});
setTimeout(() => {
reject(new Error('Tool discovery timeout'));
}, 5000);
});
}
async discoverHTTPTools(server) {
const response = await fetch(${server.url}/tools/list, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...server.headers,
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
}),
});
const result = await response.json();
return result.result.tools;
}
registerTool(serverName, tool) {
const fullName = ${serverName}.${tool.name};
this.tools.set(fullName, {
server: serverName,
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
});
}
getTool(fullName) { return this.tools.get(fullName); }
listTools(filter = null) { const toolList = Array.from(this.tools.values());
if (filter) {
return toolList.filter(tool =>
tool.name.includes(filter) ||
tool.description.includes(filter)
);
}
return toolList;
}
async invokeTool(fullName, args) {
const tool = this.getTool(fullName);
if (!tool) {
throw new Error(Tool not found: ${fullName});
}
// Validate input
this.validateInput(tool.inputSchema, args);
// Get server
const server = this.servers.get(tool.server);
// Invoke tool
return this.invokeToolOnServer(server, tool.name, args);
}
validateInput(schema, input) { const Ajv = require('ajv'); const ajv = new Ajv(); const validate = ajv.compile(schema);
if (!validate(input)) {
throw new Error(
`Invalid tool input: ${JSON.stringify(validate.errors)}`
);
}
}
async invokeToolOnServer(server, toolName, args) { const request = { jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: toolName, arguments: args, }, };
switch (server.type) {
case 'local': {
return new Promise((resolve, reject) => {
server.process.stdin.write(JSON.stringify(request) + '\n');
server.process.stdout.once('data', (data) => {
const response = JSON.parse(data.toString());
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.result);
}
});
setTimeout(() => {
reject(new Error('Tool invocation timeout'));
}, 30000);
});
}
case 'http': {
const response = await fetch(`${server.url}/tools/call`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...server.headers,
},
body: JSON.stringify(request),
});
const result = await response.json();
if (result.error) {
throw new Error(result.error.message);
}
return result.result;
}
default:
throw new Error(`Unsupported server type: ${server.type}`);
}
} }
// Usage const discovery = new MCPToolDiscovery(manager.servers);
// Discover all tools await discovery.discoverAll();
// List tools console.log('Available tools:', discovery.listTools());
// Invoke tool const result = await discovery.invokeTool('filesystem.read_file', { path: 'src/index.js', });
console.log('Tool result:', result);
β οΈ Error Handling Patterns
Retry with Exponential Backoff
// retry-handler.js class RetryHandler { constructor(maxRetries = 3, baseDelay = 1000) { this.maxRetries = maxRetries; this.baseDelay = baseDelay; }
async execute(fn, context = {}) { let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry on certain errors
if (this.isNonRetryable(error)) {
throw error;
}
if (attempt < this.maxRetries) {
const delay = this.calculateDelay(attempt);
console.warn(
`Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay}ms...`
);
await this.sleep(delay);
}
}
}
throw new Error(
`Max retries (${this.maxRetries}) exceeded. Last error: ${lastError.message}`
);
}
isNonRetryable(error) { // Don't retry validation errors, auth errors, etc. return ( error.message.includes('validation') || error.message.includes('unauthorized') || error.message.includes('forbidden') || error.message.includes('not found') ); }
calculateDelay(attempt) { // Exponential backoff with jitter const exponentialDelay = this.baseDelay * Math.pow(2, attempt); const jitter = Math.random() * this.baseDelay; return Math.min(exponentialDelay + jitter, 30000); // Max 30s }
sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }
// Usage const retry = new RetryHandler();
const result = await retry.execute(async () => { return await discovery.invokeTool('github.create_issue', { owner: 'user', repo: 'repo', title: 'Bug report', }); });
Circuit Breaker
// circuit-breaker.js class CircuitBreaker { constructor(threshold = 5, timeout = 60000, resetTimeout = 300000) { this.threshold = threshold; this.timeout = timeout; this.resetTimeout = resetTimeout;
this.failures = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(fn) { if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime > this.resetTimeout) { // Try half-open state this.state = 'HALF_OPEN'; console.log('Circuit breaker entering HALF_OPEN state'); } else { throw new Error('Circuit breaker is OPEN'); } }
try {
const result = await this.executeWithTimeout(fn);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
async executeWithTimeout(fn) { return Promise.race([ fn(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), this.timeout) ), ]); }
onSuccess() { this.failures = 0; if (this.state === 'HALF_OPEN') { console.log('Circuit breaker entering CLOSED state'); this.state = 'CLOSED'; } }
onFailure() { this.failures++; this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
console.error('π΄ Circuit breaker tripped - entering OPEN state');
this.state = 'OPEN';
}
}
getState() { return { state: this.state, failures: this.failures, lastFailure: this.lastFailureTime, }; } }
// Usage const breaker = new CircuitBreaker();
try { const result = await breaker.execute(async () => { return await fetch('https://api.example.com/data'); }); } catch (error) { console.error('Request failed:', error.message); console.log('Circuit breaker state:', breaker.getState()); }
Graceful Degradation
// graceful-degradation.js class GracefulDegradation { constructor(primaryFn, fallbackFn) { this.primaryFn = primaryFn; this.fallbackFn = fallbackFn; this.primaryFailures = 0; this.useFallback = false; }
async execute(...args) { if (this.useFallback) { return this.executeFallback(...args); }
try {
const result = await this.primaryFn(...args);
this.primaryFailures = 0;
return result;
} catch (error) {
this.primaryFailures++;
console.warn(
`Primary function failed (${this.primaryFailures} times): ${error.message}`
);
if (this.primaryFailures >= 3) {
console.warn('Switching to fallback function');
this.useFallback = true;
}
return this.executeFallback(...args);
}
}
async executeFallback(...args) {
try {
return await this.fallbackFn(...args);
} catch (error) {
throw new Error(
Both primary and fallback functions failed: ${error.message}
);
}
}
reset() { this.primaryFailures = 0; this.useFallback = false; } }
// Usage const toolInvoker = new GracefulDegradation( // Primary: Use MCP server async (toolName, args) => { return await discovery.invokeTool(toolName, args); }, // Fallback: Use direct API async (toolName, args) => { console.warn('Using fallback implementation'); return await directAPICall(toolName, args); } );
const result = await toolInvoker.execute('github.create_issue', { owner: 'user', repo: 'repo', title: 'Bug', });
π Security Considerations
Authentication
{ "mcpServers": { "secure-api": { "type": "http", "url": "https://api.example.com/mcp/v1", "headers": { "Authorization": "Bearer ${MCP_API_TOKEN}", "X-API-Key": "${API_KEY}" }, "tools": ["*"] } } }
TLS/SSL
// tls-config.js import https from 'https'; import fs from 'fs';
const tlsOptions = { ca: fs.readFileSync('ca-cert.pem'), cert: fs.readFileSync('client-cert.pem'), key: fs.readFileSync('client-key.pem'), rejectUnauthorized: true, minVersion: 'TLSv1.3', };
const agent = new https.Agent(tlsOptions);
// Use with fetch const response = await fetch('https://secure-mcp.example.com', { agent, });
Input Validation
// Always validate tool inputs function validateToolInput(schema, input) { const Ajv = require('ajv'); const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
if (!validate(input)) { const errors = validate.errors.map(err => ({ path: err.instancePath, message: err.message, }));
throw new Error(
`Invalid tool input:\n${JSON.stringify(errors, null, 2)}`
);
} }
π Related Skills
-
gh-aw-security-architecture: Security for MCP servers
-
gh-aw-tools-ecosystem: Available MCP tools
-
gh-aw-safe-outputs: Output sanitization
-
github-actions-workflows: CI/CD integration
π References
-
Model Context Protocol Specification
-
MCP TypeScript SDK
-
MCP Python SDK
-
Official MCP Servers
-
JSON-RPC 2.0 Specification
β Remember
-
Validate MCP configuration with JSON Schema
-
Use appropriate transport protocol (stdio/HTTP/SSE)
-
Implement proper lifecycle management (startup, health, shutdown)
-
Discover tools dynamically at runtime
-
Validate tool inputs against schemas
-
Handle errors gracefully with retries and circuit breakers
-
Implement fallback mechanisms
-
Secure MCP servers with authentication and TLS
-
Monitor server health continuously
-
Log all MCP operations for audit
-
Test MCP servers in isolation before integration
-
Document custom MCP servers thoroughly
-
Version MCP server APIs
-
Implement graceful degradation
-
Use timeout for all MCP operations
Last Updated: 2026-02-17
Version: 1.0.0
License: Apache-2.0