skills/hack23/riksdagsmonitor/GitHub Agentic Workflows MCP Configuration

GitHub Agentic Workflows MCP Configuration

SKILL.md

🔌 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


✅ 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

Weekly Installs
0
GitHub Stars
2
First Seen
Jan 1, 1970