Skip to content

Commit fb6ec1e

Browse files
committed
Add tool discovery mode!
Only works with clients that support Sampling currently on VSCode.
1 parent 864acf4 commit fb6ec1e

9 files changed

Lines changed: 235 additions & 143 deletions

File tree

.vscode/launch.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
{
55
"type": "node",
66
"request": "attach",
7-
"name": "Attach to MCP Server Dev",
7+
"name": "Wait for MCP Server to Start",
88
"port": 9999,
9+
"address": "localhost",
910
"restart": true,
1011
"skipFiles": [
1112
"<node_internals>/**"
@@ -17,7 +18,10 @@
1718
"cwd": "${workspaceFolder}",
1819
"sourceMapPathOverrides": {
1920
"/*": "${workspaceFolder}/src/*"
20-
}
21+
},
22+
"timeout": 60000,
23+
"localRoot": "${workspaceFolder}",
24+
"remoteRoot": "${workspaceFolder}"
2125
},
2226
{
2327
"type": "node",

docs/TOOLS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,15 @@ XcodeBuildMCP supports two operating modes:
198198
#### Static Mode (Default)
199199
All tools are loaded and available immediately at startup. Provides complete access to the full toolset without restrictions. Set `XCODEBUILDMCP_DYNAMIC_TOOLS=false` or leave unset.
200200

201-
#### Dynamic Mode
202-
Only the `discover_tools` tool is available initially. AI agents can use `discover_tools` to analyze task descriptions and intelligently enable relevant workflow based tool-groups on-demand. Set `XCODEBUILDMCP_DYNAMIC_TOOLS=true` to enable.
201+
#### Dynamic Mode (Experimental)
202+
Only the `discover_tools` and `discover_projs` tools are available initially. AI agents can use `discover_tools` tool to provide a task description that the server will analyze and intelligently enable relevant workflow based tool-groups on-demand. Set `XCODEBUILDMCP_DYNAMIC_TOOLS=true` to enable.
203203

204204
## MCP Resources
205205

206206
For clients that support MCP resources, XcodeBuildMCP provides efficient URI-based data access:
207207

208208
| Resource URI | Description | Mirrors Tool |
209209
|--------------|-------------|---------------|
210-
| `xcodebuildmcp://simulators` | Available iOS simulators with UUIDs and states | `list_sims` |
210+
| `xcodebuildmcp://simulators` | Available iOS simulators with UUIDs and states | `list_sims` |
211+
| `xcodebuildmcp://devices` | Available physical Apple devices with UUIDs, names, and connection status | `list_devices` |
212+
| `xcodebuildmcp://environment` | System diagnostics and environment validation | `diagnostic` |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"url": "https://github.com/cameroncooke/XcodeBuildMCP/issues"
5959
},
6060
"dependencies": {
61-
"@modelcontextprotocol/sdk": "^1.6.1",
61+
"@modelcontextprotocol/sdk": "github:cameroncooke/typescript-sdk#main",
6262
"@sentry/cli": "^2.43.1",
6363
"@sentry/node": "^9.15.0",
6464
"reloaderoo": "^1.0.1",

src/core/dynamic-tools.ts

Lines changed: 67 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { getDefaultCommandExecutor, CommandExecutor } from '../utils/command.js'
33
import { WORKFLOW_LOADERS, WorkflowName, WORKFLOW_METADATA } from './generated-plugins.js';
44
import { ToolResponse } from '../types/common.js';
55
import { PluginMeta } from './plugin-types.js';
6+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7+
import { registerAndTrackTools } from '../utils/tool-registry.js';
8+
import { ZodRawShape } from 'zod';
69

710
// Track enabled workflows and their tools for replacement functionality
811
const enabledWorkflows = new Set<string>();
@@ -14,40 +17,7 @@ type ToolHandler = (
1417
executor: CommandExecutor,
1518
) => Promise<ToolResponse>;
1619

17-
// Interface for the MCP server with the methods we need
18-
interface MCPServerInterface {
19-
tool(
20-
name: string,
21-
description: string,
22-
schema: unknown,
23-
handler: (args: unknown) => Promise<unknown>,
24-
): void;
25-
registerTool?(
26-
name: string,
27-
config: {
28-
title?: string;
29-
description: string;
30-
inputSchema?: unknown;
31-
outputSchema?: unknown;
32-
annotations?: unknown;
33-
},
34-
callback: (args: unknown) => Promise<unknown>,
35-
): unknown;
36-
registerTools?(
37-
tools: Array<{
38-
name: string;
39-
config: {
40-
title?: string;
41-
description: string;
42-
inputSchema?: unknown;
43-
outputSchema?: unknown;
44-
annotations?: unknown;
45-
};
46-
callback: (args: unknown) => Promise<unknown>;
47-
}>,
48-
): unknown[];
49-
notifyToolsChanged?: () => Promise<void>;
50-
}
20+
// Use the actual McpServer type from the SDK instead of a custom interface
5121

5222
/**
5323
* Wrapper function to adapt MCP SDK handler calling convention to our dependency injection pattern
@@ -99,7 +69,7 @@ export function getEnabledWorkflows(): string[] {
9969
* @param additive - If true, add to existing workflows. If false (default), replace existing workflows
10070
*/
10171
export async function enableWorkflows(
102-
server: MCPServerInterface,
72+
server: McpServer,
10373
workflowNames: string[],
10474
additive: boolean = false,
10575
): Promise<void> {
@@ -134,17 +104,16 @@ export async function enableWorkflows(
134104

135105
log('info', `Enabling ${toolKeys.length} tools from '${workflowName}' workflow`);
136106

137-
// Prepare tools for bulk registration
138107
const toolsToRegister: Array<{
139108
name: string;
140109
config: {
141110
title?: string;
142-
description: string;
143-
inputSchema?: unknown;
144-
outputSchema?: unknown;
145-
annotations?: unknown;
111+
description?: string;
112+
inputSchema?: ZodRawShape;
113+
outputSchema?: ZodRawShape;
114+
annotations?: Record<string, unknown>;
146115
};
147-
callback: (args: unknown) => Promise<unknown>;
116+
callback: (args: Record<string, unknown>) => Promise<ToolResponse>;
148117
}> = [];
149118

150119
// Collect all tools from this workflow
@@ -156,7 +125,7 @@ export async function enableWorkflows(
156125
name: tool.name,
157126
config: {
158127
description: tool.description ?? '',
159-
inputSchema: tool.schema,
128+
inputSchema: tool.schema, // MCP SDK now handles complex types properly
160129
},
161130
callback: wrapHandlerWithExecutor(tool.handler as ToolHandler),
162131
});
@@ -169,46 +138,66 @@ export async function enableWorkflows(
169138
}
170139
}
171140

172-
// Use bulk registration if available, otherwise fall back to individual registration
173-
if (typeof (server as any).registerTools === 'function') {
174-
try {
175-
log('info', `🚀 Enabling ${toolsToRegister.length} tools from '${workflowName}' workflow`);
176-
const registeredTools = (server as any).registerTools(toolsToRegister);
141+
// Use bulk registration with proper types - no runtime checking needed
142+
try {
143+
const availableTools = toolsToRegister.filter((tool) => {
144+
// In testing/development, check for duplicate registrations
145+
// The MCP SDK handles this internally, so this is just for logging
146+
log('debug', `Preparing to register tool: ${tool.name}`);
147+
return true;
148+
});
149+
150+
if (availableTools.length > 0) {
151+
log('info', `🚀 Enabling ${availableTools.length} tools from '${workflowName}' workflow`);
152+
153+
// Convert to proper tool registration format, adapting callback signature
154+
const toolRegistrations = availableTools.map((tool) => ({
155+
name: tool.name,
156+
config: {
157+
description: tool.config.description,
158+
inputSchema: tool.config.inputSchema as unknown, // Cast to unknown for SDK interface
159+
},
160+
// Adapt callback to match SDK's expected signature (args, extra) => result
161+
callback: (args: unknown): Promise<ToolResponse> =>
162+
tool.callback(args as Record<string, unknown>),
163+
}));
164+
165+
// Use registerTools with proper types and tracking
166+
const registeredTools = registerAndTrackTools(server, toolRegistrations);
177167
log('info', `✅ Registered ${registeredTools.length} tools from '${workflowName}'`);
178168
// registerTools() automatically sends tool list change notification internally
179-
} catch (error) {
180-
log('error', `Failed to register tools from '${workflowName}': ${error}`);
181-
}
182-
} else if (typeof (server as any).registerTool === 'function') {
183-
// Use registerTool (fewer notifications than tool())
184-
log('info', `🚀 Enabling ${toolsToRegister.length} tools from '${workflowName}' workflow`);
185-
for (const toolToRegister of toolsToRegister) {
186-
try {
187-
(server as any).registerTool(
188-
toolToRegister.name,
189-
toolToRegister.config,
190-
toolToRegister.callback,
191-
);
192-
log('debug', `Registered tool: ${toolToRegister.name}`);
193-
} catch (error) {
194-
log('error', `Failed to register tool '${toolToRegister.name}': ${error}`);
195-
}
169+
} else {
170+
log(
171+
'info',
172+
`All ${toolsToRegister.length} tools from '${workflowName}' were already registered`,
173+
);
196174
}
197-
log('info', `✅ Registered ${toolsToRegister.length} tools from '${workflowName}'`);
198-
} else {
199-
// Final fallback to tool() method (most notifications)
200-
log('info', `🚀 Enabling ${toolsToRegister.length} tools from '${workflowName}' workflow`);
175+
} catch (error) {
176+
log('error', `Failed to register tools from '${workflowName}': ${error}`);
177+
// Fallback to simplified tool registration one at a time
178+
log(
179+
'info',
180+
`🚀 Fallback: Enabling ${toolsToRegister.length} tools individually from '${workflowName}' workflow`,
181+
);
201182
for (const toolToRegister of toolsToRegister) {
202183
try {
203-
server.tool(
204-
toolToRegister.name,
205-
toolToRegister.config.description,
206-
toolToRegister.config.inputSchema,
207-
toolToRegister.callback,
208-
);
184+
// Use the simplified registerTools method with single tool to avoid type complexity
185+
const singleToolRegistration = [
186+
{
187+
name: toolToRegister.name,
188+
config: {
189+
description: toolToRegister.config.description,
190+
inputSchema: toolToRegister.config.inputSchema as unknown, // Cast to unknown for SDK interface
191+
},
192+
// Adapt callback to match SDK's expected signature
193+
callback: (args: unknown): Promise<ToolResponse> =>
194+
toolToRegister.callback(args as Record<string, unknown>),
195+
},
196+
];
197+
registerAndTrackTools(server, singleToolRegistration);
209198
log('debug', `Registered tool: ${toolToRegister.name}`);
210-
} catch (error) {
211-
log('error', `Failed to register tool '${toolToRegister.name}': ${error}`);
199+
} catch (toolError) {
200+
log('error', `Failed to register tool '${toolToRegister.name}': ${toolError}`);
212201
}
213202
}
214203
log('info', `✅ Registered ${toolsToRegister.length} tools from '${workflowName}'`);
@@ -224,32 +213,8 @@ export async function enableWorkflows(
224213
}
225214
}
226215

227-
// Notify the client about the tool list change
228-
// Only send manual notifications if we're not using registerTools (which sends automatically)
229-
let needsManualNotification = true;
230-
231-
for (const workflowName of workflowNames) {
232-
if (typeof (server as any).registerTools === 'function') {
233-
needsManualNotification = false;
234-
break;
235-
}
236-
}
237-
238-
if (needsManualNotification) {
239-
try {
240-
if (typeof (server as any).sendToolListChanged === 'function') {
241-
(server as any).sendToolListChanged();
242-
log('debug', 'Sent tool list changed notification');
243-
} else if (server.notifyToolsChanged) {
244-
await server.notifyToolsChanged();
245-
log('debug', 'Notified client of tool list changes (fallback)');
246-
}
247-
} catch (error) {
248-
log('warn', `Failed to notify client of tool changes: ${error}`);
249-
}
250-
} else {
251-
log('debug', 'Skipping manual notification - registerTools() handles notifications automatically');
252-
}
216+
// No manual notification needed - registerTools() handles notifications automatically
217+
log('debug', 'Tool list change notifications handled automatically by registerTools()');
253218

254219
log(
255220
'info',

src/index.ts

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { log } from './utils/logger.js';
2525

2626
// Import version
2727
import { version } from './version.js';
28-
import { loadPlugins } from './core/plugin-registry.js';
2928

3029
// Import xcodemake utilities
3130
import { isXcodemakeEnabled, isXcodemakeAvailable } from './utils/xcodemake.js';
@@ -35,16 +34,13 @@ import process from 'node:process';
3534

3635
// Import resource management
3736
import { registerResources } from './core/resources.js';
37+
import { registerDiscoveryTools, registerAllToolsStatic } from './utils/tool-registry.js';
3838

3939
/**
4040
* Main function to start the server
4141
*/
4242
async function main(): Promise<void> {
4343
try {
44-
// Increase stdout maxListeners to prevent EventEmitter memory leak warnings
45-
// during bulk tool registration in dynamic mode (80+ tools)
46-
process.stdout.setMaxListeners(100);
47-
4844
// Check if xcodemake is enabled and available
4945
if (isXcodemakeEnabled()) {
5046
log('info', 'xcodemake is enabled, checking if available...');
@@ -67,37 +63,20 @@ async function main(): Promise<void> {
6763
// Make server available globally for dynamic tools
6864
(globalThis as { mcpServer?: McpServer }).mcpServer = server;
6965

70-
// Check if dynamic tools mode is enabled
71-
const isDynamicMode = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true';
72-
73-
if (isDynamicMode) {
74-
// DYNAMIC MODE: Only load discovery tools initially
75-
log('info', '🚀 Initializing server in dynamic mode...');
76-
const plugins = await loadPlugins();
77-
let registeredCount = 0;
78-
79-
// Only register discovery tools initially
80-
for (const plugin of plugins.values()) {
81-
// Only load discover_tools and discovery-related tools initially
82-
if (plugin.name === 'discover_tools' || plugin.name === 'discover_projs') {
83-
server.tool(plugin.name, plugin.description ?? '', plugin.schema, plugin.handler);
84-
registeredCount++;
85-
}
86-
}
87-
log('info', `✅ Registered ${registeredCount} discovery tools in dynamic mode.`);
88-
log('info', 'Use discover_tools to enable additional workflows based on your task.');
66+
// Check if dynamic tools mode is explicitly disabled
67+
const isDynamicModeEnabled = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true';
68+
69+
if (isDynamicModeEnabled) {
70+
// DYNAMIC MODE: Start with discovery tools only
71+
log('info', '🚀 Initializing server in dynamic tools mode...');
72+
await registerDiscoveryTools(server);
73+
log('info', '💡 Use discover_tools to enable additional workflows based on your task.');
8974
} else {
90-
// STATIC MODE: Load all tools immediately
91-
log('info', '🚀 Initializing server in static mode...');
92-
const plugins = await loadPlugins();
93-
let registeredCount = 0;
94-
for (const plugin of plugins.values()) {
95-
server.tool(plugin.name, plugin.description ?? '', plugin.schema, plugin.handler);
96-
registeredCount++;
97-
}
98-
log('info', `✅ Registered ${registeredCount} tools in static mode.`);
75+
// EXPLICIT STATIC MODE: Load all tools immediately
76+
log('info', '🚀 Initializing server in static tools mode...');
77+
await registerAllToolsStatic(server);
9978
}
100-
79+
10180
await registerResources(server);
10281

10382
// Start the server

src/mcp/tools/discovery/__tests__/discover_tools.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe('discover_tools', () => {
116116

117117
it('should have correct description', () => {
118118
expect(discoverTools.description).toBe(
119-
'Analyzes a natural language task description to enable a relevant set of Xcode and Apple development tools for the current session.',
119+
'Analyzes a natural language task description to enable a relevant set of Xcode and Apple development tools. For best results, specify the target platform (iOS, macOS, watchOS, tvOS, visionOS) and project type (.xcworkspace or .xcodeproj).',
120120
);
121121
});
122122

src/utils/capabilities.ts

Whitespace-only changes.

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './template-manager.js';
1111
export * from './test-common.js';
1212
export * from './xcodemake.js';
1313
export * from './environment.js';
14+
export * from './tool-registry.js';
1415
export * from '../version.js';
1516
export * from '../core/dynamic-tools.js';
1617
export * from '../core/plugin-registry.js';

0 commit comments

Comments
 (0)