From b9ca0d07a07118f9d160629b454af5ad2ab2ec4a Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 20:38:19 +0900 Subject: [PATCH] feat(mcp): expose nested execution metadata in parse_mode (#1312) Add executionPlan and teamsCapability fields to ParseModeResult. ModeHandler now injects TeamsCapabilityService, builds an ExecutionPlan from dispatchReady agents, and includes Teams runtime capability status in the parse_mode response. Nested plan (subagent+teams) is built when Teams coordination is enabled. All new fields are optional for backward compatibility. --- apps/mcp-server/src/agent/agent.types.ts | 180 +++--------------- apps/mcp-server/src/agent/execution-plan.ts | 2 +- .../src/agent/execution-plan.types.ts | 158 +++++++++++++++ apps/mcp-server/src/agent/index.ts | 1 + apps/mcp-server/src/keyword/keyword.types.ts | 14 ++ .../abstract-handler.integration.spec.ts | 9 + .../src/mcp/handlers/mode.handler.spec.ts | 118 ++++++++++++ .../src/mcp/handlers/mode.handler.ts | 105 ++++++++++ apps/mcp-server/src/mcp/mcp.service.spec.ts | 6 + 9 files changed, 441 insertions(+), 152 deletions(-) create mode 100644 apps/mcp-server/src/agent/execution-plan.types.ts diff --git a/apps/mcp-server/src/agent/agent.types.ts b/apps/mcp-server/src/agent/agent.types.ts index 03f3965d..7a9d7f33 100644 --- a/apps/mcp-server/src/agent/agent.types.ts +++ b/apps/mcp-server/src/agent/agent.types.ts @@ -1,4 +1,15 @@ -import type { Mode } from '../keyword/keyword.types'; +// Mode is defined in keyword.types but importing it directly creates a circular dependency +// (agent.types → keyword.types → execution-plan.types ← agent.types via re-export). +// Local alias breaks the cycle while preserving type safety. +type Mode = 'PLAN' | 'ACT' | 'EVAL' | 'AUTO'; + +import type { + DispatchedAgent, + TaskmaestroDispatch, + TeamsDispatch, + VisibilityConfig, + ExecutionPlan, +} from './execution-plan.types'; /** * Agent Stack - a pre-configured team of agents for common workflows @@ -69,156 +80,23 @@ export interface ParallelAgentSet { failedAgents?: FailedAgent[]; } -/** - * Dispatch parameters for a single agent, ready to use with Claude Code Task tool - */ -export interface DispatchParams { - subagent_type: 'general-purpose'; - prompt: string; - description: string; - run_in_background?: true; -} - -/** - * A dispatched agent with metadata and Task-tool-ready parameters - */ -export interface DispatchedAgent { - name: string; - displayName: string; - description: string; - dispatchParams: DispatchParams; -} - -/** - * Inner Teams coordination metadata embedded in a TaskMaestro pane assignment. - * Present only when the composable `taskmaestro+teams` strategy is active - * and the Teams capability gate is enabled. - */ -export interface InnerTeamsSpec { - type: 'teams'; - teamSpec: { - team_name: string; - description: string; - }; - teammates: Array<{ - name: string; - subagent_type: 'general-purpose'; - }>; -} - -/** - * A single TaskMaestro pane assignment with agent name and prompt. - * When the composable `taskmaestro+teams` strategy is active, - * `innerCoordination` carries the metadata a pane worker needs - * to bootstrap its inner Teams workflow. - */ -export interface TaskmaestroAssignment { - name: string; - displayName: string; - prompt: string; - /** Inner coordination metadata for composable taskmaestro+teams execution */ - innerCoordination?: InnerTeamsSpec; -} - -/** - * TaskMaestro dispatch configuration for parallel tmux pane execution - */ -export interface TaskmaestroDispatch { - sessionName: string; - paneCount: number; - assignments: TaskmaestroAssignment[]; -} - -/** - * A single teammate in a Teams dispatch configuration - */ -export interface TeamsTeammate { - name: string; - subagent_type: 'general-purpose'; - team_name: string; - prompt: string; -} - -/** - * Teams dispatch configuration for Claude Code native team coordination - */ -export interface TeamsDispatch { - team_name: string; - description: string; - teammates: TeamsTeammate[]; -} - -/** - * SendMessage-based progress reporting instructions for specialist visibility - */ -export interface VisibilityMessages { - onStart: string; - onFinding: string; - onComplete: string; -} - -/** - * Configuration for real-time specialist execution visibility via Teams messaging - */ -export interface VisibilityConfig { - reportTo: string; - format: string; - includeProgress: boolean; - messages?: VisibilityMessages; -} - -/** - * Execution layer: subagent strategy - */ -export interface SubagentLayer { - type: 'subagent'; - agents: DispatchedAgent[]; -} - -/** - * Execution layer: TaskMaestro tmux-based transport - */ -export interface TaskmaestroLayer { - type: 'taskmaestro'; - config: TaskmaestroDispatch; -} - -/** - * Execution layer: Claude Code native Teams coordination - */ -export interface TeamsLayer { - type: 'teams'; - config: TeamsDispatch; - visibility?: VisibilityConfig; -} - -/** - * Discriminated union of all execution layer types. - * Each layer represents a single execution strategy. - */ -export type ExecutionLayer = SubagentLayer | TaskmaestroLayer | TeamsLayer; - -/** - * Composable execution plan with outer transport and optional inner coordination. - * - * Simple plan: outerExecution only (e.g. subagent, taskmaestro, or teams) - * Nested plan: outerExecution + innerCoordination (e.g. taskmaestro + teams) - * - * @example - * // Simple subagent plan - * { outerExecution: { type: 'subagent', agents: [...] } } - * - * @example - * // Nested: TaskMaestro as transport, Teams as coordination - * { - * outerExecution: { type: 'taskmaestro', config: {...} }, - * innerCoordination: { type: 'teams', config: {...} } - * } - */ -export interface ExecutionPlan { - outerExecution: ExecutionLayer; - innerCoordination?: ExecutionLayer; -} +// Re-export execution plan types from dedicated file (avoids circular dep with keyword.types) +export type { + DispatchParams, + DispatchedAgent, + InnerTeamsSpec, + TaskmaestroAssignment, + TaskmaestroDispatch, + TeamsTeammate, + TeamsDispatch, + VisibilityMessages, + VisibilityConfig, + SubagentLayer, + TaskmaestroLayer, + TeamsLayer, + ExecutionLayer, + ExecutionPlan, +} from './execution-plan.types'; /** * Result of dispatching agents for execution diff --git a/apps/mcp-server/src/agent/execution-plan.ts b/apps/mcp-server/src/agent/execution-plan.ts index 86f1a83f..c24aa0a8 100644 --- a/apps/mcp-server/src/agent/execution-plan.ts +++ b/apps/mcp-server/src/agent/execution-plan.ts @@ -5,7 +5,7 @@ import type { TaskmaestroDispatch, TeamsDispatch, VisibilityConfig, -} from './agent.types'; +} from './execution-plan.types'; /** * Build a single-layer execution plan. diff --git a/apps/mcp-server/src/agent/execution-plan.types.ts b/apps/mcp-server/src/agent/execution-plan.types.ts new file mode 100644 index 00000000..86537be5 --- /dev/null +++ b/apps/mcp-server/src/agent/execution-plan.types.ts @@ -0,0 +1,158 @@ +/** + * Execution plan types extracted from agent.types.ts to break the + * circular dependency: agent.types → keyword.types → agent.types. + * + * These types are consumed by keyword.types.ts (ParseModeResult) and + * re-exported from agent.types.ts for backward compatibility. + */ + +/** + * Dispatch parameters for a single agent, ready to use with Claude Code Task tool + */ +export interface DispatchParams { + subagent_type: 'general-purpose'; + prompt: string; + description: string; + run_in_background?: true; +} + +/** + * A dispatched agent with metadata and Task-tool-ready parameters + */ +export interface DispatchedAgent { + name: string; + displayName: string; + description: string; + dispatchParams: DispatchParams; +} + +/** + * Inner Teams coordination metadata embedded in a TaskMaestro pane assignment. + * Present only when the composable `taskmaestro+teams` strategy is active + * and the Teams capability gate is enabled. + */ +export interface InnerTeamsSpec { + type: 'teams'; + teamSpec: { + team_name: string; + description: string; + }; + teammates: Array<{ + name: string; + subagent_type: 'general-purpose'; + }>; +} + +/** + * A single TaskMaestro pane assignment with agent name and prompt. + * When the composable `taskmaestro+teams` strategy is active, + * `innerCoordination` carries the metadata a pane worker needs + * to bootstrap its inner Teams workflow. + */ +export interface TaskmaestroAssignment { + name: string; + displayName: string; + prompt: string; + /** Inner coordination metadata for composable taskmaestro+teams execution */ + innerCoordination?: InnerTeamsSpec; +} + +/** + * TaskMaestro dispatch configuration for parallel tmux pane execution + */ +export interface TaskmaestroDispatch { + sessionName: string; + paneCount: number; + assignments: TaskmaestroAssignment[]; +} + +/** + * A single teammate in a Teams dispatch configuration + */ +export interface TeamsTeammate { + name: string; + subagent_type: 'general-purpose'; + team_name: string; + prompt: string; +} + +/** + * Teams dispatch configuration for Claude Code native team coordination + */ +export interface TeamsDispatch { + team_name: string; + description: string; + teammates: TeamsTeammate[]; +} + +/** + * SendMessage-based progress reporting instructions for specialist visibility + */ +export interface VisibilityMessages { + onStart: string; + onFinding: string; + onComplete: string; +} + +/** + * Configuration for real-time specialist execution visibility via Teams messaging + */ +export interface VisibilityConfig { + reportTo: string; + format: string; + includeProgress: boolean; + messages?: VisibilityMessages; +} + +/** + * Execution layer: subagent strategy + */ +export interface SubagentLayer { + type: 'subagent'; + agents: DispatchedAgent[]; +} + +/** + * Execution layer: TaskMaestro tmux-based transport + */ +export interface TaskmaestroLayer { + type: 'taskmaestro'; + config: TaskmaestroDispatch; +} + +/** + * Execution layer: Claude Code native Teams coordination + */ +export interface TeamsLayer { + type: 'teams'; + config: TeamsDispatch; + visibility?: VisibilityConfig; +} + +/** + * Discriminated union of all execution layer types. + * Each layer represents a single execution strategy. + */ +export type ExecutionLayer = SubagentLayer | TaskmaestroLayer | TeamsLayer; + +/** + * Composable execution plan with outer transport and optional inner coordination. + * + * Simple plan: outerExecution only (e.g. subagent, taskmaestro, or teams) + * Nested plan: outerExecution + innerCoordination (e.g. taskmaestro + teams) + * + * @example + * // Simple subagent plan + * { outerExecution: { type: 'subagent', agents: [...] } } + * + * @example + * // Nested: TaskMaestro as transport, Teams as coordination + * { + * outerExecution: { type: 'taskmaestro', config: {...} }, + * innerCoordination: { type: 'teams', config: {...} } + * } + */ +export interface ExecutionPlan { + outerExecution: ExecutionLayer; + innerCoordination?: ExecutionLayer; +} diff --git a/apps/mcp-server/src/agent/index.ts b/apps/mcp-server/src/agent/index.ts index 909af311..1f515c19 100644 --- a/apps/mcp-server/src/agent/index.ts +++ b/apps/mcp-server/src/agent/index.ts @@ -9,3 +9,4 @@ export * from './agent-prompt.builder'; export * from './agent-stack.schema'; export * from './agent-stack.loader'; export * from './execution-plan'; +export * from './execution-plan.types'; diff --git a/apps/mcp-server/src/keyword/keyword.types.ts b/apps/mcp-server/src/keyword/keyword.types.ts index 7c8ce333..f3130224 100644 --- a/apps/mcp-server/src/keyword/keyword.types.ts +++ b/apps/mcp-server/src/keyword/keyword.types.ts @@ -1,6 +1,8 @@ import type { DiffAnalysisResult } from './diff-analyzer'; import type { CouncilPreset } from '../agent/council-preset.types'; import type { CouncilSummary } from '../collaboration/council-summary.types'; +import type { ExecutionPlan } from '../agent/execution-plan.types'; +import type { TeamsCapabilityStatus } from '../agent/teams-capability.types'; export const KEYWORDS = ['PLAN', 'ACT', 'EVAL', 'AUTO'] as const; @@ -523,6 +525,18 @@ export interface ParseModeResult { * Clients should treat this as read-only diagnostic data. */ councilSummary?: CouncilSummary; + /** + * @apiProperty External API - do not rename. + * Describes the outer execution transport and optional inner coordination layer. + * Built from the composable execution model (#1309). Present when dispatch is active. + */ + executionPlan?: ExecutionPlan; + /** + * @apiProperty External API - do not rename. + * Runtime capability status for Teams coordination. + * Reflects environment, config, or default gating from TeamsCapabilityService (#1311). + */ + teamsCapability?: TeamsCapabilityStatus; } /** diff --git a/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts b/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts index 44da9660..099102bd 100644 --- a/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/abstract-handler.integration.spec.ts @@ -171,6 +171,14 @@ describe('Handler Security Integration', () => { executionHint: 'Use Task tool...', }), }; + const mockTeamsCapabilityServiceForMode = { + getStatus: vi.fn().mockResolvedValue({ + available: false, + reason: 'Teams coordination is experimental and disabled by default', + source: 'default', + }), + isAvailable: vi.fn().mockResolvedValue(false), + }; modeHandler = new ModeHandler( mockKeywordService, mockConfigService, @@ -181,6 +189,7 @@ describe('Handler Security Integration', () => { mockDiagnosticLogService, mockAgentServiceForMode as AgentService, new CouncilPresetService(), + mockTeamsCapabilityServiceForMode as unknown as import('../../agent/teams-capability.service').TeamsCapabilityService, mockImpactEventService, mockRuleEventCollector, ); diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts index 885ea774..2317868f 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.spec.ts @@ -9,6 +9,7 @@ import { ContextDocumentService } from '../../context/context-document.service'; import { DiagnosticLogService } from '../../diagnostic/diagnostic-log.service'; import type { AgentService } from '../../agent/agent.service'; import { CouncilPresetService } from '../../agent/council-preset.service'; +import type { TeamsCapabilityService } from '../../agent/teams-capability.service'; import type { ImpactEventService } from '../../impact'; import type { RuleEventCollector } from '../../rules/rule-event-collector'; @@ -23,6 +24,7 @@ describe('ModeHandler', () => { let mockDiagnosticLogService: DiagnosticLogService; let mockAgentService: Partial; let mockCouncilPresetService: CouncilPresetService; + let mockTeamsCapabilityService: Partial; let mockImpactEventService: Partial; let mockRuleEventCollector: Partial; @@ -133,6 +135,15 @@ describe('ModeHandler', () => { mockCouncilPresetService = new CouncilPresetService(); + mockTeamsCapabilityService = { + getStatus: vi.fn().mockResolvedValue({ + available: false, + reason: 'Teams coordination is experimental and disabled by default', + source: 'default', + }), + isAvailable: vi.fn().mockResolvedValue(false), + }; + mockImpactEventService = { logEvent: vi.fn(), }; @@ -149,6 +160,7 @@ describe('ModeHandler', () => { mockDiagnosticLogService, mockAgentService as AgentService, mockCouncilPresetService, + mockTeamsCapabilityService as TeamsCapabilityService, mockImpactEventService as ImpactEventService, mockRuleEventCollector as RuleEventCollector, ); @@ -1358,4 +1370,110 @@ describe('ModeHandler', () => { expect(JSON.parse(reserialized)).toEqual(parsed.councilPreset); }); }); + + describe('execution metadata in parse_mode response', () => { + it('should include teamsCapability when TeamsCapabilityService returns status', async () => { + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design auth feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.teamsCapability).toBeDefined(); + expect(parsed.teamsCapability.available).toBe(false); + expect(parsed.teamsCapability.source).toBe('default'); + }); + + it('should include executionPlan when dispatchReady has agents', async () => { + mockKeywordService.parseMode = vi.fn().mockResolvedValue({ + ...mockParseModeResult, + included_agent: { + name: 'Solution Architect', + systemPrompt: 'You are a solution architect...', + expertise: ['architecture'], + }, + }); + + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design auth feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.executionPlan).toBeDefined(); + expect(parsed.executionPlan.strategy).toBe('subagent'); + expect(parsed.executionPlan.isNested).toBe(false); + expect(parsed.executionPlan.outerExecution.type).toBe('subagent'); + }); + + it('should build nested plan when Teams is available', async () => { + (mockTeamsCapabilityService.getStatus as ReturnType).mockResolvedValue({ + available: true, + reason: 'Enabled via CODINGBUDDY_TEAMS_ENABLED environment variable', + source: 'environment', + }); + + mockKeywordService.parseMode = vi.fn().mockResolvedValue({ + ...mockParseModeResult, + included_agent: { + name: 'Solution Architect', + systemPrompt: 'You are a solution architect...', + expertise: ['architecture'], + }, + }); + + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design auth feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.executionPlan).toBeDefined(); + expect(parsed.executionPlan.isNested).toBe(true); + expect(parsed.executionPlan.strategy).toBe('subagent+teams'); + expect(parsed.executionPlan.innerCoordination).toBeDefined(); + expect(parsed.executionPlan.innerCoordination.type).toBe('teams'); + }); + + it('should not include executionPlan when no dispatchReady agents', async () => { + const result = await handler.handle('parse_mode', { + prompt: 'PLAN test task', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.executionPlan).toBeUndefined(); + }); + + it('should handle TeamsCapabilityService failure gracefully', async () => { + (mockTeamsCapabilityService.getStatus as ReturnType).mockRejectedValue( + new Error('Service unavailable'), + ); + + const result = await handler.handle('parse_mode', { + prompt: 'PLAN test task', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.teamsCapability).toBeUndefined(); + }); + + it('should reflect teamsCapability.available=true when enabled via config', async () => { + (mockTeamsCapabilityService.getStatus as ReturnType).mockResolvedValue({ + available: true, + reason: 'Enabled via experimental.teamsCoordination config', + source: 'config', + }); + + const result = await handler.handle('parse_mode', { + prompt: 'PLAN design feature', + }); + + expect(result?.isError).toBeFalsy(); + const parsed = JSON.parse(result!.content[0].text as string); + expect(parsed.teamsCapability.available).toBe(true); + expect(parsed.teamsCapability.source).toBe('config'); + }); + }); }); diff --git a/apps/mcp-server/src/mcp/handlers/mode.handler.ts b/apps/mcp-server/src/mcp/handlers/mode.handler.ts index 63e19feb..28ed8d22 100644 --- a/apps/mcp-server/src/mcp/handlers/mode.handler.ts +++ b/apps/mcp-server/src/mcp/handlers/mode.handler.ts @@ -30,6 +30,16 @@ import { buildVisualData, type AgentVisualInput } from '../../keyword/visual-dat import { isValidVerbosity } from '../../shared/verbosity.types'; import { AgentService } from '../../agent/agent.service'; import { CouncilPresetService } from '../../agent/council-preset.service'; +import { TeamsCapabilityService } from '../../agent/teams-capability.service'; +import type { TeamsCapabilityStatus } from '../../agent/teams-capability.types'; +import { + buildSimplePlan, + buildNestedPlan, + subagentLayer, + teamsLayer, + serializeExecutionPlan, +} from '../../agent/execution-plan'; +import type { ExecutionPlan } from '../../agent/execution-plan.types'; import { ImpactEventService } from '../../impact'; import { RuleEventCollector } from '../../rules/rule-event-collector'; @@ -106,6 +116,7 @@ export class ModeHandler extends AbstractHandler { private readonly diagnosticLogService: DiagnosticLogService, private readonly agentService: AgentService, private readonly councilPresetService: CouncilPresetService, + private readonly teamsCapabilityService: TeamsCapabilityService, private readonly impactEventService: ImpactEventService, private readonly ruleEventCollector: RuleEventCollector, ) { @@ -282,6 +293,10 @@ export class ModeHandler extends AbstractHandler { const councilPreset = this.councilPresetService.resolvePreset(result.mode as Mode) ?? undefined; + // Resolve Teams capability status and build execution plan + const teamsCapability = await this.resolveTeamsCapability(); + const executionPlan = this.buildExecutionPlan(dispatchReady, teamsCapability); + // Build visual data for agent visualization const visual = await this.buildVisual( result.mode as Mode, @@ -307,6 +322,10 @@ export class ModeHandler extends AbstractHandler { ...(visual && { visual }), // Include council preset for PLAN/EVAL modes ...(councilPreset && { councilPreset }), + // Include execution plan metadata when dispatch is active + ...(executionPlan && { executionPlan: serializeExecutionPlan(executionPlan) }), + // Include Teams capability status + ...(teamsCapability && { teamsCapability }), // Include context document info (mandatory) ...contextResult, // Include project root warning when auto-detected and config missing @@ -620,6 +639,92 @@ export class ModeHandler extends AbstractHandler { } } + /** + * Resolve Teams capability status from TeamsCapabilityService. + * Returns undefined on failure (graceful degradation). + */ + private async resolveTeamsCapability(): Promise { + try { + return await this.teamsCapabilityService.getStatus(); + } catch (error) { + this.logger.warn( + `Failed to resolve Teams capability: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + return undefined; + } + } + + /** + * Build an ExecutionPlan from dispatchReady data and Teams capability. + * Returns undefined when no dispatch-ready agents are present. + * + * Logic: + * - Outer layer is always 'subagent' (derived from dispatchReady agents). + * - If Teams is available, an inner 'teams' coordination layer is added. + */ + private buildExecutionPlan( + dispatchReady?: DispatchReady, + teamsCapability?: TeamsCapabilityStatus, + ): ExecutionPlan | undefined { + if (!dispatchReady) { + return undefined; + } + + const agents: { + name: string; + displayName: string; + description: string; + dispatchParams: { + subagent_type: 'general-purpose'; + prompt: string; + description: string; + run_in_background?: true; + }; + }[] = []; + + if (dispatchReady.primaryAgent) { + agents.push({ + name: dispatchReady.primaryAgent.name, + displayName: dispatchReady.primaryAgent.displayName, + description: dispatchReady.primaryAgent.description, + dispatchParams: dispatchReady.primaryAgent.dispatchParams, + }); + } + + if (dispatchReady.parallelAgents?.length) { + for (const agent of dispatchReady.parallelAgents) { + agents.push({ + name: agent.name, + displayName: agent.displayName, + description: agent.description, + dispatchParams: agent.dispatchParams, + }); + } + } + + if (agents.length === 0) { + return undefined; + } + + const outer = subagentLayer(agents); + + if (teamsCapability?.available) { + const inner = teamsLayer({ + team_name: 'auto', + description: 'Auto-generated Teams coordination layer', + teammates: agents.map(a => ({ + name: a.name, + subagent_type: 'general-purpose' as const, + team_name: 'auto', + prompt: a.description, + })), + }); + return buildNestedPlan(outer, inner); + } + + return buildSimplePlan(outer); + } + /** * Persist mode state for context recovery after compaction */ diff --git a/apps/mcp-server/src/mcp/mcp.service.spec.ts b/apps/mcp-server/src/mcp/mcp.service.spec.ts index e69754e5..c3fa003c 100644 --- a/apps/mcp-server/src/mcp/mcp.service.spec.ts +++ b/apps/mcp-server/src/mcp/mcp.service.spec.ts @@ -494,6 +494,12 @@ function createMcpServiceWithHandlers( services.diagnosticLogService as DiagnosticLogService, services.agentService as AgentService, new CouncilPresetService(), + { + getStatus: vi + .fn() + .mockResolvedValue({ available: false, reason: 'default', source: 'default' }), + isAvailable: vi.fn().mockResolvedValue(false), + } as never, services.impactEventService as ImpactEventService, services.ruleEventCollector as RuleEventCollector, ),