All files / mcp mcp-health.ts

100% Statements 23/23
100% Branches 6/6
100% Functions 4/4
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                                                                                  7x                   7x                         9x 9x 8x 8x   9x                 3x 3x 3x 3x   3x 3x 3x       3x   1x 1x   1x 1x   1x 1x       3x                 1x      
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module MCP/MCPHealth
 * @description Aggregated health monitoring for MCP tools.
 *
 * The {@link MCPHealthMonitor} tracks per-tool circuit breaker state and
 * provides a consolidated health snapshot for observability dashboards
 * and operational alerting.
 *
 * @see {@link https://github.com/Hack23/ISMS-PUBLIC/blob/main/Secure_Development_Policy.md | Secure Development Policy}
 */
 
import type { CircuitState } from './mcp-retry.js';
import { CircuitBreaker } from './mcp-retry.js';
import type { CircuitBreakerOptions } from './mcp-retry.js';
 
/**
 * Per-tool health snapshot returned by {@link MCPHealthMonitor.getHealthSnapshot}.
 */
export interface ToolHealthEntry {
  /** Current circuit breaker state for the tool */
  state: CircuitState;
  /** Number of consecutive failures recorded */
  consecutiveFailures: number;
}
 
/**
 * Aggregated health snapshot across all registered MCP tools.
 */
export interface HealthSnapshot {
  /** Per-tool health status keyed by tool name */
  tools: ReadonlyMap<string, ToolHealthEntry>;
  /** Count of tools with circuit in OPEN state */
  openCircuits: number;
  /** Count of tools with circuit in HALF_OPEN state */
  halfOpenCircuits: number;
  /** Count of tools with circuit in CLOSED state */
  closedCircuits: number;
}
 
/**
 * Monitors circuit breaker health across multiple MCP tools.
 *
 * Each tool is backed by its own {@link CircuitBreaker} instance; the monitor
 * provides both per-tool access and aggregated health snapshots.
 */
export class MCPHealthMonitor {
  private readonly breakers = new Map<string, CircuitBreaker>();
  private readonly defaultOptions: CircuitBreakerOptions;
 
  /**
   * Create a new health monitor.
   *
   * @param defaultOptions - Default circuit breaker options applied when
   *   registering a new tool without explicit options
   */
  constructor(defaultOptions: CircuitBreakerOptions = {}) {
    this.defaultOptions = defaultOptions;
  }
 
  /**
   * Get or create the {@link CircuitBreaker} for the named tool.
   *
   * If the tool has not been registered yet a new breaker is created with the
   * monitor's default options.
   *
   * @param toolName - Unique identifier for the MCP tool
   * @returns The circuit breaker instance for the tool
   */
  getBreaker(toolName: string): CircuitBreaker {
    let breaker = this.breakers.get(toolName);
    if (!breaker) {
      breaker = new CircuitBreaker(this.defaultOptions);
      this.breakers.set(toolName, breaker);
    }
    return breaker;
  }
 
  /**
   * Return an aggregated health snapshot across all registered tools.
   *
   * @returns Snapshot with per-tool state and aggregate counters
   */
  getHealthSnapshot(): HealthSnapshot {
    let openCircuits = 0;
    let halfOpenCircuits = 0;
    let closedCircuits = 0;
    const tools = new Map<string, ToolHealthEntry>();
 
    for (const [name, breaker] of this.breakers) {
      const stats = breaker.getStats();
      tools.set(name, {
        state: stats.state,
        consecutiveFailures: stats.consecutiveFailures,
      });
      switch (stats.state) {
        case 'OPEN':
          openCircuits++;
          break;
        case 'HALF_OPEN':
          halfOpenCircuits++;
          break;
        case 'CLOSED':
          closedCircuits++;
          break;
      }
    }
 
    return { tools, openCircuits, halfOpenCircuits, closedCircuits };
  }
 
  /**
   * Return the names of all registered tools.
   *
   * @returns Array of tool names
   */
  getRegisteredTools(): readonly string[] {
    return [...this.breakers.keys()];
  }
}