Skip to content

Commit 2c62f41

Browse files
committed
feat: Enhance runtime tool registration and doctor tool output
Changes: - Updated the `doctor` tool to conditionally display a note based on the runtime mode. - Modified the `getRuntimeToolInfo` method to support both 'static' and 'runtime' modes, including a note for static mode. - Introduced a new `runtime-registry.ts` file to manage runtime tool information. - Updated the `tool-registry.ts` to record runtime registrations and improve workflow selection logic. - Added utility functions in `workflow-selection.ts` for better workflow management. This update improves the flexibility of the MCP server's runtime behavior and enhances the information provided by the doctor tool.
1 parent e85a4ce commit 2c62f41

7 files changed

Lines changed: 226 additions & 28 deletions

File tree

src/mcp/tools/doctor/__tests__/doctor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies
8686
runtime: {
8787
async getRuntimeToolInfo() {
8888
return {
89-
mode: 'static' as const,
89+
mode: 'runtime' as const,
9090
enabledWorkflows: ['doctor'],
9191
enabledTools: ['doctor'],
9292
totalRegistered: 1,

src/mcp/tools/doctor/doctor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export async function runDoctor(
210210
`- Mode: ${runtimeInfo.mode}`,
211211
`- Enabled Workflows: ${runtimeInfo.enabledWorkflows.length}`,
212212
`- Registered Tools: ${runtimeInfo.totalRegistered}`,
213+
...(runtimeInfo.mode === 'static' ? [`- Note: ${runtimeInfo.note}`] : []),
213214
...(runtimeInfo.enabledWorkflows.length > 0
214215
? [`- Workflows: ${runtimeInfo.enabledWorkflows.join(', ')}`]
215216
: []),

src/mcp/tools/doctor/lib/doctor.deps.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as os from 'os';
22
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
3-
import { loadWorkflowGroups, loadPlugins } from '../../../../utils/plugin-registry/index.ts';
3+
import { loadWorkflowGroups } from '../../../../utils/plugin-registry/index.ts';
4+
import { getRuntimeRegistration } from '../../../../utils/runtime-registry.ts';
5+
import {
6+
collectToolNames,
7+
resolveSelectedWorkflows,
8+
} from '../../../../utils/workflow-selection.ts';
49
import { areAxeToolsAvailable } from '../../../../utils/axe/index.ts';
510
import {
611
isXcodemakeEnabled,
@@ -57,12 +62,21 @@ export interface PluginInfoProvider {
5762
}
5863

5964
export interface RuntimeInfoProvider {
60-
getRuntimeToolInfo(): Promise<{
61-
mode: 'static';
62-
enabledWorkflows: string[];
63-
enabledTools: string[];
64-
totalRegistered: number;
65-
}>;
65+
getRuntimeToolInfo(): Promise<
66+
| {
67+
mode: 'runtime';
68+
enabledWorkflows: string[];
69+
enabledTools: string[];
70+
totalRegistered: number;
71+
}
72+
| {
73+
mode: 'static';
74+
enabledWorkflows: string[];
75+
enabledTools: string[];
76+
totalRegistered: number;
77+
note: string;
78+
}
79+
>;
6680
}
6781

6882
export interface FeatureDetector {
@@ -229,15 +243,28 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen
229243

230244
const runtime: RuntimeInfoProvider = {
231245
async getRuntimeToolInfo() {
246+
const runtimeInfo = getRuntimeRegistration();
247+
if (runtimeInfo) {
248+
return runtimeInfo;
249+
}
250+
232251
const workflows = await loadWorkflowGroups();
233-
const enabledWorkflows = Array.from(workflows.keys());
234-
const plugins = await loadPlugins();
235-
const enabledTools = Array.from(plugins.keys());
252+
const enabledWorkflowEnv = process.env.XCODEBUILDMCP_ENABLED_WORKFLOWS ?? '';
253+
const workflowNames = enabledWorkflowEnv
254+
.split(',')
255+
.map((workflow) => workflow.trim())
256+
.filter(Boolean);
257+
const selection = resolveSelectedWorkflows(workflows, workflowNames);
258+
const enabledWorkflows = selection.selectedWorkflows.map(
259+
(workflow) => workflow.directoryName,
260+
);
261+
const enabledTools = collectToolNames(selection.selectedWorkflows);
236262
return {
237263
mode: 'static',
238264
enabledWorkflows,
239265
enabledTools,
240266
totalRegistered: enabledTools.length,
267+
note: 'Runtime registry unavailable; showing expected tools from selection rules.',
241268
};
242269
},
243270
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { z } from 'zod';
3+
import { resolveSelectedWorkflows } from '../workflow-selection.ts';
4+
import type { WorkflowGroup } from '../../core/plugin-types.ts';
5+
6+
function makeWorkflow(name: string): WorkflowGroup {
7+
return {
8+
directoryName: name,
9+
workflow: {
10+
name,
11+
description: `${name} workflow`,
12+
},
13+
tools: [
14+
{
15+
name: `${name}-tool`,
16+
description: `${name} tool`,
17+
schema: { enabled: z.boolean().optional() },
18+
async handler() {
19+
return { content: [] };
20+
},
21+
},
22+
],
23+
};
24+
}
25+
26+
function makeWorkflowMap(names: string[]): Map<string, WorkflowGroup> {
27+
const map = new Map<string, WorkflowGroup>();
28+
for (const name of names) {
29+
map.set(name, makeWorkflow(name));
30+
}
31+
return map;
32+
}
33+
34+
describe('resolveSelectedWorkflows', () => {
35+
let originalDebug: string | undefined;
36+
37+
beforeEach(() => {
38+
originalDebug = process.env.XCODEBUILDMCP_DEBUG;
39+
});
40+
41+
afterEach(() => {
42+
if (typeof originalDebug === 'undefined') {
43+
delete process.env.XCODEBUILDMCP_DEBUG;
44+
} else {
45+
process.env.XCODEBUILDMCP_DEBUG = originalDebug;
46+
}
47+
});
48+
49+
it('adds doctor when debug is enabled and selection list is provided', () => {
50+
process.env.XCODEBUILDMCP_DEBUG = 'true';
51+
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
52+
53+
const result = resolveSelectedWorkflows(workflows, ['simulator']);
54+
55+
expect(result.selectedNames).toEqual(['session-management', 'doctor', 'simulator']);
56+
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
57+
'session-management',
58+
'doctor',
59+
'simulator',
60+
]);
61+
});
62+
63+
it('does not add doctor when debug is disabled', () => {
64+
process.env.XCODEBUILDMCP_DEBUG = 'false';
65+
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
66+
67+
const result = resolveSelectedWorkflows(workflows, ['simulator']);
68+
69+
expect(result.selectedNames).toEqual(['session-management', 'simulator']);
70+
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
71+
'session-management',
72+
'simulator',
73+
]);
74+
});
75+
76+
it('returns all workflows when no selection list is provided', () => {
77+
process.env.XCODEBUILDMCP_DEBUG = 'true';
78+
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
79+
80+
const result = resolveSelectedWorkflows(workflows, []);
81+
82+
expect(result.selectedNames).toBeNull();
83+
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
84+
'session-management',
85+
'doctor',
86+
'simulator',
87+
]);
88+
});
89+
});

src/utils/runtime-registry.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type RuntimeToolInfo =
2+
| {
3+
mode: 'runtime';
4+
enabledWorkflows: string[];
5+
enabledTools: string[];
6+
totalRegistered: number;
7+
}
8+
| {
9+
mode: 'static';
10+
enabledWorkflows: string[];
11+
enabledTools: string[];
12+
totalRegistered: number;
13+
note: string;
14+
};
15+
16+
let runtimeToolInfo: RuntimeToolInfo | null = null;
17+
18+
export function recordRuntimeRegistration(info: {
19+
enabledWorkflows: string[];
20+
enabledTools: string[];
21+
}): void {
22+
const enabledWorkflows = [...new Set(info.enabledWorkflows)];
23+
const enabledTools = [...new Set(info.enabledTools)];
24+
25+
runtimeToolInfo = {
26+
mode: 'runtime',
27+
enabledWorkflows,
28+
enabledTools,
29+
totalRegistered: enabledTools.length,
30+
};
31+
}
32+
33+
export function getRuntimeRegistration(): RuntimeToolInfo | null {
34+
return runtimeToolInfo;
35+
}

src/utils/tool-registry.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
import { loadWorkflowGroups } from '../core/plugin-registry.ts';
33
import { ToolResponse } from '../types/common.ts';
44
import { log } from './logger.ts';
5-
6-
// Workflow that must always be included as other tools depend on it
7-
const REQUIRED_WORKFLOW = 'session-management';
5+
import { recordRuntimeRegistration } from './runtime-registry.ts';
6+
import { resolveSelectedWorkflows } from './workflow-selection.ts';
87

98
/**
109
* Register workflows (selected list or all when omitted)
@@ -14,21 +13,13 @@ export async function registerWorkflows(
1413
workflowNames: string[] = [],
1514
): Promise<void> {
1615
const workflowGroups = await loadWorkflowGroups();
16+
const selection = resolveSelectedWorkflows(workflowGroups, workflowNames);
1717
let registeredCount = 0;
1818
const registeredTools = new Set<string>();
19+
const registeredWorkflows = new Set<string>();
1920

20-
const normalizedNames = workflowNames.map((name) => name.trim().toLowerCase());
21-
const selectedNames =
22-
normalizedNames.length > 0 ? [...new Set([REQUIRED_WORKFLOW, ...normalizedNames])] : null;
23-
24-
const workflows = selectedNames
25-
? selectedNames.map((workflowName) => workflowGroups.get(workflowName))
26-
: [...workflowGroups.values()];
27-
28-
for (const workflow of workflows) {
29-
if (!workflow) {
30-
continue;
31-
}
21+
for (const workflow of selection.selectedWorkflows) {
22+
registeredWorkflows.add(workflow.directoryName);
3223
for (const tool of workflow.tools) {
3324
if (registeredTools.has(tool.name)) {
3425
continue;
@@ -47,10 +38,15 @@ export async function registerWorkflows(
4738
}
4839
}
4940

50-
if (selectedNames) {
41+
recordRuntimeRegistration({
42+
enabledWorkflows: [...registeredWorkflows],
43+
enabledTools: [...registeredTools],
44+
});
45+
46+
if (selection.selectedNames) {
5147
log(
5248
'info',
53-
`✅ Registered ${registeredCount} tools from workflows: ${selectedNames.join(', ')}`,
49+
`✅ Registered ${registeredCount} tools from workflows: ${selection.selectedNames.join(', ')}`,
5450
);
5551
} else {
5652
log('info', `✅ Registered ${registeredCount} tools in static mode.`);

src/utils/workflow-selection.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { WorkflowGroup } from '../core/plugin-types.ts';
2+
3+
const REQUIRED_WORKFLOW = 'session-management';
4+
const DEBUG_WORKFLOW = 'doctor';
5+
6+
function normalizeWorkflowNames(workflowNames: string[]): string[] {
7+
return workflowNames.map((name) => name.trim().toLowerCase()).filter(Boolean);
8+
}
9+
10+
function isWorkflowGroup(value: WorkflowGroup | undefined): value is WorkflowGroup {
11+
return Boolean(value);
12+
}
13+
14+
function isDebugEnabled(): boolean {
15+
const value = process.env.XCODEBUILDMCP_DEBUG ?? '';
16+
return value.toLowerCase() === 'true' || value === '1';
17+
}
18+
19+
export function resolveSelectedWorkflows(
20+
workflowGroups: Map<string, WorkflowGroup>,
21+
workflowNames: string[] = [],
22+
): {
23+
selectedWorkflows: WorkflowGroup[];
24+
selectedNames: string[] | null;
25+
} {
26+
const normalizedNames = normalizeWorkflowNames(workflowNames);
27+
const autoSelected = isDebugEnabled() ? [REQUIRED_WORKFLOW, DEBUG_WORKFLOW] : [REQUIRED_WORKFLOW];
28+
const selectedNames =
29+
normalizedNames.length > 0 ? [...new Set([...autoSelected, ...normalizedNames])] : null;
30+
31+
const selectedWorkflows = selectedNames
32+
? selectedNames.map((workflowName) => workflowGroups.get(workflowName)).filter(isWorkflowGroup)
33+
: [...workflowGroups.values()];
34+
35+
return { selectedWorkflows, selectedNames };
36+
}
37+
38+
export function collectToolNames(workflows: WorkflowGroup[]): string[] {
39+
const toolNames = new Set<string>();
40+
41+
for (const workflow of workflows) {
42+
for (const tool of workflow.tools) {
43+
if (tool?.name) {
44+
toolNames.add(tool.name);
45+
}
46+
}
47+
}
48+
49+
return [...toolNames];
50+
}

0 commit comments

Comments
 (0)