refactor(9/12): migrate debugging, swift-package, and remaining tools#327
refactor(9/12): migrate debugging, swift-package, and remaining tools#327cameroncooke merged 3 commits intomainfrom
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Duplicated
runLogictest helper across many files- Created shared test-helpers.ts module with runLogic helper, eliminating duplication across 46+ test files.
- ✅ Fixed: Removed
showAsciiLogoparameter breaks CLI doctor usage- Restored showAsciiLogo parameter to runDoctor and doctorLogic functions with ASCII logo rendering for CLI usage.
Or push these changes by commenting:
@cursor push 984868d074
Preview (984868d074)
diff --git a/manifests/tools/launch_app_logs_sim.yaml b/manifests/tools/launch_app_logs_sim.yaml
deleted file mode 100644
--- a/manifests/tools/launch_app_logs_sim.yaml
+++ /dev/null
@@ -1,17 +1,0 @@
-id: launch_app_logs_sim
-module: mcp/tools/simulator/launch_app_logs_sim
-names:
- mcp: launch_app_logs_sim
- cli: launch-app-with-logs
-description: Launch sim app with logs.
-routing:
- stateful: true
-annotations:
- title: Launch App Logs Simulator
- readOnlyHint: false
- destructiveHint: false
- openWorldHint: false
-nextSteps:
- - label: Stop capture and retrieve logs
- toolId: stop_sim_log_cap
- priority: 1
\ No newline at end of file
diff --git a/manifests/tools/start_device_log_cap.yaml b/manifests/tools/start_device_log_cap.yaml
deleted file mode 100644
--- a/manifests/tools/start_device_log_cap.yaml
+++ /dev/null
@@ -1,17 +1,0 @@
-id: start_device_log_cap
-module: mcp/tools/logging/start_device_log_cap
-names:
- mcp: start_device_log_cap
- cli: start-device-log-capture
-description: Start device log capture.
-routing:
- stateful: true
-annotations:
- title: Start Device Log Capture
- readOnlyHint: false
- destructiveHint: false
- openWorldHint: false
-nextSteps:
- - label: Stop capture and retrieve logs
- toolId: stop_device_log_cap
- priority: 1
\ No newline at end of file
diff --git a/manifests/tools/start_sim_log_cap.yaml b/manifests/tools/start_sim_log_cap.yaml
deleted file mode 100644
--- a/manifests/tools/start_sim_log_cap.yaml
+++ /dev/null
@@ -1,17 +1,0 @@
-id: start_sim_log_cap
-module: mcp/tools/logging/start_sim_log_cap
-names:
- mcp: start_sim_log_cap
- cli: start-simulator-log-capture
-description: Start sim log capture.
-routing:
- stateful: true
-annotations:
- title: Start Simulator Log Capture
- readOnlyHint: false
- destructiveHint: false
- openWorldHint: false
-nextSteps:
- - label: Stop capture and retrieve logs
- toolId: stop_sim_log_cap
- priority: 1
\ No newline at end of file
diff --git a/manifests/tools/stop_device_log_cap.yaml b/manifests/tools/stop_device_log_cap.yaml
deleted file mode 100644
--- a/manifests/tools/stop_device_log_cap.yaml
+++ /dev/null
@@ -1,13 +1,0 @@
-id: stop_device_log_cap
-module: mcp/tools/logging/stop_device_log_cap
-names:
- mcp: stop_device_log_cap
- cli: stop-device-log-capture
-description: Stop device app and return logs.
-routing:
- stateful: true
-annotations:
- title: Stop Device and Return Logs
- readOnlyHint: false
- destructiveHint: false
- openWorldHint: false
\ No newline at end of file
diff --git a/manifests/tools/stop_sim_log_cap.yaml b/manifests/tools/stop_sim_log_cap.yaml
deleted file mode 100644
--- a/manifests/tools/stop_sim_log_cap.yaml
+++ /dev/null
@@ -1,13 +1,0 @@
-id: stop_sim_log_cap
-module: mcp/tools/logging/stop_sim_log_cap
-names:
- mcp: stop_sim_log_cap
- cli: stop-simulator-log-capture
-description: Stop sim app and return logs.
-routing:
- stateful: true
-annotations:
- title: Stop Simulator and Return Logs
- readOnlyHint: false
- destructiveHint: false
- openWorldHint: false
\ No newline at end of file
diff --git a/manifests/workflows/device.yaml b/manifests/workflows/device.yaml
--- a/manifests/workflows/device.yaml
+++ b/manifests/workflows/device.yaml
@@ -15,7 +15,5 @@
- list_schemes
- show_build_settings
- get_app_bundle_id
- - start_device_log_cap
- - stop_device_log_cap
- get_coverage_report
- get_file_coverage
diff --git a/manifests/workflows/logging.yaml b/manifests/workflows/logging.yaml
deleted file mode 100644
--- a/manifests/workflows/logging.yaml
+++ /dev/null
@@ -1,8 +1,0 @@
-id: logging
-title: Log Capture
-description: Capture and retrieve logs from simulator and device apps.
-tools:
- - start_sim_log_cap
- - stop_sim_log_cap
- - start_device_log_cap
- - stop_device_log_cap
\ No newline at end of file
diff --git a/manifests/workflows/simulator.yaml b/manifests/workflows/simulator.yaml
--- a/manifests/workflows/simulator.yaml
+++ b/manifests/workflows/simulator.yaml
@@ -14,7 +14,6 @@
- get_sim_app_path
- install_app_sim
- launch_app_sim
- - launch_app_logs_sim
- stop_app_sim
- record_sim_video
- clean
@@ -24,7 +23,5 @@
- get_app_bundle_id
- screenshot
- snapshot_ui
- - stop_sim_log_cap
- - start_sim_log_cap
- get_coverage_report
- get_file_coverage
diff --git a/src/core/manifest/__tests__/load-manifest.test.ts b/src/core/manifest/__tests__/load-manifest.test.ts
deleted file mode 100644
--- a/src/core/manifest/__tests__/load-manifest.test.ts
+++ /dev/null
@@ -1,187 +1,0 @@
-import { describe, it, expect, beforeEach, afterEach } from 'vitest';
-import * as fs from 'node:fs';
-import * as path from 'node:path';
-import * as os from 'node:os';
-import {
- loadManifest,
- getWorkflowTools,
- getToolsForWorkflows,
- ManifestValidationError,
-} from '../load-manifest.ts';
-
-describe('load-manifest', () => {
- describe('loadManifest (integration with real manifests)', () => {
- it('should load all manifests from the manifests directory', () => {
- const manifest = loadManifest();
-
- // Check that we have tools and workflows
- expect(manifest.tools.size).toBeGreaterThan(0);
- expect(manifest.workflows.size).toBeGreaterThan(0);
- });
-
- it('should have required workflows', () => {
- const manifest = loadManifest();
-
- expect(manifest.workflows.has('simulator')).toBe(true);
- expect(manifest.workflows.has('device')).toBe(true);
- expect(manifest.workflows.has('session-management')).toBe(true);
- });
-
- it('should have required tools', () => {
- const manifest = loadManifest();
-
- expect(manifest.tools.has('build_sim')).toBe(true);
- expect(manifest.tools.has('discover_projs')).toBe(true);
- expect(manifest.tools.has('session_show_defaults')).toBe(true);
- expect(manifest.tools.has('session_use_defaults_profile')).toBe(true);
- });
-
- it('should validate tool references in workflows', () => {
- const manifest = loadManifest();
-
- // Every tool referenced in a workflow should exist
- for (const [workflowId, workflow] of manifest.workflows) {
- for (const toolId of workflow.tools) {
- expect(
- manifest.tools.has(toolId),
- `Workflow '${workflowId}' references unknown tool '${toolId}'`,
- ).toBe(true);
- }
- }
- });
-
- it('should have unique MCP names across all tools', () => {
- const manifest = loadManifest();
- const mcpNames = new Set<string>();
-
- for (const [, tool] of manifest.tools) {
- expect(mcpNames.has(tool.names.mcp), `Duplicate MCP name '${tool.names.mcp}'`).toBe(false);
- mcpNames.add(tool.names.mcp);
- }
- });
-
- it('should have session-management as auto-include workflow', () => {
- const manifest = loadManifest();
- const sessionMgmt = manifest.workflows.get('session-management');
-
- expect(sessionMgmt).toBeDefined();
- expect(sessionMgmt?.selection?.mcp?.autoInclude).toBe(true);
- });
-
- it('should have simulator as default-enabled workflow', () => {
- const manifest = loadManifest();
- const simulator = manifest.workflows.get('simulator');
-
- expect(simulator).toBeDefined();
- expect(simulator?.selection?.mcp?.defaultEnabled).toBe(true);
- });
-
- it('should have doctor workflow with debugEnabled predicate', () => {
- const manifest = loadManifest();
- const doctor = manifest.workflows.get('doctor');
-
- expect(doctor).toBeDefined();
- expect(doctor?.predicates).toContain('debugEnabled');
- expect(doctor?.selection?.mcp?.autoInclude).toBe(true);
- });
-
- it('should have xcode-ide workflow hidden in Xcode agent mode only', () => {
- const manifest = loadManifest();
- const xcodeIde = manifest.workflows.get('xcode-ide');
-
- expect(xcodeIde).toBeDefined();
- expect(xcodeIde?.predicates).toContain('hideWhenXcodeAgentMode');
- expect(xcodeIde?.predicates).not.toContain('debugEnabled');
- });
-
- it('should keep xcode bridge gateway tools daemon-routed and debug tools gated', () => {
- const manifest = loadManifest();
-
- expect(manifest.tools.get('xcode_ide_list_tools')?.routing?.stateful).toBe(true);
- expect(manifest.tools.get('xcode_ide_call_tool')?.routing?.stateful).toBe(true);
- expect(manifest.tools.get('xcode_tools_bridge_status')?.predicates).toContain('debugEnabled');
- expect(manifest.tools.get('xcode_tools_bridge_sync')?.predicates).toContain('debugEnabled');
- expect(manifest.tools.get('xcode_tools_bridge_disconnect')?.predicates).toContain(
- 'debugEnabled',
- );
- });
-
- it('should provide explicit approval annotations for every tool', () => {
- const manifest = loadManifest();
-
- for (const [toolId, tool] of manifest.tools) {
- expect(tool.annotations, `Tool '${toolId}' is missing annotations`).toBeDefined();
- expect(
- tool.annotations?.title,
- `Tool '${toolId}' is missing annotations.title`,
- ).toBeTruthy();
- expect(
- tool.annotations?.readOnlyHint,
- `Tool '${toolId}' is missing annotations.readOnlyHint`,
- ).not.toBeUndefined();
- expect(
- tool.annotations?.destructiveHint,
- `Tool '${toolId}' is missing annotations.destructiveHint`,
- ).not.toBeUndefined();
- expect(
- tool.annotations?.openWorldHint,
- `Tool '${toolId}' is missing annotations.openWorldHint`,
- ).not.toBeUndefined();
- }
- });
- });
-
- describe('getWorkflowTools', () => {
- it('should return tools for a workflow', () => {
- const manifest = loadManifest();
- const tools = getWorkflowTools(manifest, 'simulator');
-
- expect(tools.length).toBeGreaterThan(0);
- expect(tools.some((t) => t.id === 'build_sim')).toBe(true);
- });
-
- it('should return empty array for unknown workflow', () => {
- const manifest = loadManifest();
- const tools = getWorkflowTools(manifest, 'nonexistent-workflow');
-
- expect(tools).toEqual([]);
- });
- });
-
- describe('getToolsForWorkflows', () => {
- it('should return unique tools across multiple workflows', () => {
- const manifest = loadManifest();
- const tools = getToolsForWorkflows(manifest, ['simulator', 'device']);
-
- // Should have tools from both workflows
- expect(tools.some((t) => t.id === 'build_sim')).toBe(true);
- expect(tools.some((t) => t.id === 'build_device')).toBe(true);
-
- // Tools should be unique (discover_projs is in both)
- const toolIds = tools.map((t) => t.id);
- const uniqueIds = new Set(toolIds);
- expect(toolIds.length).toBe(uniqueIds.size);
- });
-
- it('should return empty array for empty workflow list', () => {
- const manifest = loadManifest();
- const tools = getToolsForWorkflows(manifest, []);
-
- expect(tools).toEqual([]);
- });
- });
-});
-
-describe('ManifestValidationError', () => {
- it('should include source file in message', () => {
- const error = new ManifestValidationError('Test error', 'test.yaml');
- expect(error.message).toBe('Test error (in test.yaml)');
- expect(error.sourceFile).toBe('test.yaml');
- });
-
- it('should work without source file', () => {
- const error = new ManifestValidationError('Test error');
- expect(error.message).toBe('Test error');
- expect(error.sourceFile).toBeUndefined();
- });
-});
\ No newline at end of file
diff --git a/src/core/manifest/__tests__/schema.test.ts b/src/core/manifest/__tests__/schema.test.ts
--- a/src/core/manifest/__tests__/schema.test.ts
+++ b/src/core/manifest/__tests__/schema.test.ts
@@ -2,223 +2,78 @@
import {
toolManifestEntrySchema,
workflowManifestEntrySchema,
- deriveCliName,
+ resourceManifestEntrySchema,
getEffectiveCliName,
- type ToolManifestEntry,
} from '../schema.ts';
describe('schema', () => {
- describe('toolManifestEntrySchema', () => {
- it('should parse valid tool manifest', () => {
- const input = {
- id: 'build_sim',
- module: 'mcp/tools/simulator/build_sim',
- names: { mcp: 'build_sim' },
- description: 'Build iOS app for simulator',
- availability: { mcp: true, cli: true },
- predicates: [],
- routing: { stateful: false },
- };
+ it('parses a representative manifest/tool naming pipeline', () => {
+ const toolInput = {
+ id: 'build_sim',
+ module: 'mcp/tools/simulator/build_sim',
+ names: { mcp: 'build_sim' },
+ };
+ const workflowInput = {
+ id: 'simulator',
+ title: 'iOS Simulator Development',
+ description: 'Build and test iOS apps on simulators',
+ tools: ['build_sim'],
+ };
- const result = toolManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.id).toBe('build_sim');
- expect(result.data.names.mcp).toBe('build_sim');
- }
- });
+ const toolResult = toolManifestEntrySchema.safeParse(toolInput);
+ const workflowResult = workflowManifestEntrySchema.safeParse(workflowInput);
- it('should apply default availability', () => {
- const input = {
- id: 'test_tool',
- module: 'mcp/tools/test/test_tool',
- names: { mcp: 'test_tool' },
- };
+ expect(toolResult.success).toBe(true);
+ expect(workflowResult.success).toBe(true);
- const result = toolManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.availability).toEqual({ mcp: true, cli: true });
- expect(result.data.predicates).toEqual([]);
- expect(result.data.nextSteps).toEqual([]);
- }
- });
+ if (!toolResult.success || !workflowResult.success) {
+ throw new Error('Expected representative manifest inputs to parse');
+ }
- it('should reject missing required fields', () => {
- const input = {
- id: 'test_tool',
- // missing module and names
- };
-
- const result = toolManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(false);
- });
-
- it('should accept optional CLI name', () => {
- const input = {
- id: 'build_sim',
- module: 'mcp/tools/simulator/build_sim',
- names: { mcp: 'build_sim', cli: 'build-simulator' },
- };
-
- const result = toolManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.names.cli).toBe('build-simulator');
- }
- });
-
- it('should reject availability.daemon', () => {
- const input = {
- id: 'tool1',
- module: 'mcp/tools/test/tool1',
- names: { mcp: 'tool1' },
- availability: { mcp: true, cli: true, daemon: true },
- };
-
- expect(toolManifestEntrySchema.safeParse(input).success).toBe(false);
- });
-
- it('should reject routing.daemonAffinity', () => {
- const input = {
- id: 'tool2',
- module: 'mcp/tools/test/tool2',
- names: { mcp: 'tool2' },
- routing: { stateful: true, daemonAffinity: 'required' },
- };
-
- expect(toolManifestEntrySchema.safeParse(input).success).toBe(false);
- });
+ expect(toolResult.data.availability).toEqual({ mcp: true, cli: true });
+ expect(toolResult.data.nextSteps).toEqual([]);
+ expect(toolResult.data.predicates).toEqual([]);
+ expect(workflowResult.data.availability).toEqual({ mcp: true, cli: true });
+ expect(workflowResult.data.predicates).toEqual([]);
+ expect(workflowResult.data.tools).toEqual(['build_sim']);
+ expect(getEffectiveCliName(toolResult.data)).toBe('build-sim');
});
- describe('workflowManifestEntrySchema', () => {
- it('should parse valid workflow manifest', () => {
- const input = {
- id: 'simulator',
- title: 'iOS Simulator Development',
- description: 'Build and test iOS apps on simulators',
- availability: { mcp: true, cli: true },
- selection: {
- mcp: {
- defaultEnabled: true,
- autoInclude: false,
- },
- },
- predicates: [],
- tools: ['build_sim', 'test_sim', 'boot_sim'],
- };
+ it('parses a resource manifest entry with defaults', () => {
+ const input = {
+ id: 'simulators',
+ module: 'mcp/resources/simulators',
+ name: 'simulators',
+ uri: 'xcodebuildmcp://simulators',
+ description: 'Available iOS simulators',
+ mimeType: 'text/plain',
+ };
- const result = workflowManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.id).toBe('simulator');
- expect(result.data.tools).toHaveLength(3);
- expect(result.data.selection?.mcp?.defaultEnabled).toBe(true);
- }
- });
+ const result = resourceManifestEntrySchema.safeParse(input);
- it('should apply default values', () => {
- const input = {
- id: 'test-workflow',
- title: 'Test Workflow',
- description: 'A test workflow',
- tools: ['tool1'],
- };
+ expect(result.success).toBe(true);
+ if (!result.success) throw new Error('Expected resource manifest input to parse');
- const result = workflowManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.availability).toEqual({ mcp: true, cli: true });
- expect(result.data.predicates).toEqual([]);
- }
- });
-
- it('should reject empty tools array', () => {
- const input = {
- id: 'empty-workflow',
- title: 'Empty Workflow',
- description: 'A workflow with no tools',
- tools: [],
- };
-
- // Empty tools array is technically valid per schema
- const result = workflowManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- });
-
- it('should parse autoInclude workflow', () => {
- const input = {
- id: 'session-management',
- title: 'Session Management',
- description: 'Manage session defaults',
- availability: { mcp: true, cli: false },
- selection: {
- mcp: {
- defaultEnabled: true,
- autoInclude: true,
- },
- },
- tools: ['session_show_defaults'],
- };
-
- const result = workflowManifestEntrySchema.safeParse(input);
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.selection?.mcp?.autoInclude).toBe(true);
- expect(result.data.availability.cli).toBe(false);
- }
- });
+ expect(result.data.availability).toEqual({ mcp: true });
+ expect(result.data.predicates).toEqual([]);
});
- describe('deriveCliName', () => {
- it('should convert underscores to hyphens', () => {
- expect(deriveCliName('build_sim')).toBe('build-sim');
- expect(deriveCliName('get_app_bundle_id')).toBe('get-app-bundle-id');
- });
+ it('parses a resource manifest entry with predicates', () => {
+ const input = {
+ id: 'xcode-ide-state',
+ module: 'mcp/resources/xcode-ide-state',
+ name: 'xcode-ide-state',
+ uri: 'xcodebuildmcp://xcode-ide-state',
+ description: 'Xcode IDE state',
+ mimeType: 'application/json',
+ predicates: ['runningUnderXcodeAgent'],
+ };
- it('should convert camelCase to kebab-case', () => {
- expect(deriveCliName('buildSim')).toBe('build-sim');
- expect(deriveCliName('getAppBundleId')).toBe('get-app-bundle-id');
- });
+ const result = resourceManifestEntrySchema.safeParse(input);
- it('should handle mixed underscores and camelCase', () => {
- expect(deriveCliName('build_simApp')).toBe('build-sim-app');
- });
+ expect(result.success).toBe(true);
+ if (!result.success) throw new Error('Expected resource manifest input to parse');
- it('should handle already kebab-case', () => {
- expect(deriveCliName('build-sim')).toBe('build-sim');
- });
-
- it('should lowercase the result', () => {
- expect(deriveCliName('BUILD_SIM')).toBe('build-sim');
- });
+ expect(result.data.predicates).toEqual(['runningUnderXcodeAgent']);
});
-
- describe('getEffectiveCliName', () => {
- it('should use explicit CLI name when provided', () => {
- const tool: ToolManifestEntry = {
- id: 'build_sim',
- module: 'mcp/tools/simulator/build_sim',
- names: { mcp: 'build_sim', cli: 'build-simulator' },
- availability: { mcp: true, cli: true },
- predicates: [],
- nextSteps: [],
- };
-
- expect(getEffectiveCliName(tool)).toBe('build-simulator');
- });
-
- it('should derive CLI name when not provided', () => {
- const tool: ToolManifestEntry = {
- id: 'build_sim',
- module: 'mcp/tools/simulator/build_sim',
- names: { mcp: 'build_sim' },
- availability: { mcp: true, cli: true },
- predicates: [],
- nextSteps: [],
- };
-
- expect(getEffectiveCliName(tool)).toBe('build-sim');
- });
- });
});
diff --git a/src/core/manifest/import-resource-module.ts b/src/core/manifest/import-resource-module.ts
new file mode 100644
--- /dev/null
+++ b/src/core/manifest/import-resource-module.ts
@@ -1,0 +1,61 @@
+/**
+ * Resource module importer.
+ * Dynamically imports resource modules using named exports only.
+ */
+
+import * as path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { getPackageRoot } from './load-manifest.ts';
+
+export interface ImportedResourceModule {
+ handler: (uri: URL) => Promise<{ contents: Array<{ text: string }> }>;
+}
+
+const moduleCache = new Map<string, ImportedResourceModule>();
+
+/**
+ * Import a resource module by its manifest module path.
+ *
+ * Accepts named export only: `export const handler = ...`
+ *
+ * @param moduleId - Extensionless module path (e.g., 'mcp/resources/simulators')
+ * @returns Imported resource module with handler
+ */
+export async function importResourceModule(moduleId: string): Promise<ImportedResourceModule> {
+ const cached = moduleCache.get(moduleId);
+ if (cached) {
+ return cached;
+ }
+
+ const packageRoot = getPackageRoot();
+ const modulePath = path.join(packageRoot, 'build', `${moduleId}.js`);
+ const moduleUrl = pathToFileURL(modulePath).href;
+
+ let mod: Record<string, unknown>;
+ try {
+ mod = (await import(moduleUrl)) as Record<string, unknown>;
+ } catch (err) {
+ throw new Error(`Failed to import resource module '${moduleId}': ${err}`);
+ }
+
+ if (typeof mod.handler !== 'function') {
+ throw new Error(
+ `Resource module '${moduleId}' does not export the required shape. ` +
+ `Expected a named export: export const handler = ...`,
+ );
+ }
+
+ const result: ImportedResourceModule = {
+ handler: mod.handler as ImportedResourceModule['handler'],
+ };
+
+ moduleCache.set(moduleId, result);
+ return result;
+}
+
+/**
+ * Reset module cache (for tests).
+ */
+export function __resetResourceModuleCacheForTests(): void {
+ moduleCache.clear();
+}
diff --git a/src/core/manifest/import-tool-module.ts b/src/core/manifest/import-tool-module.ts
--- a/src/core/manifest/import-tool-module.ts
+++ b/src/core/manifest/import-tool-module.ts
@@ -1,40 +1,30 @@
/**
- * Tool module importer with backward-compatible adapter.
- * Dynamically imports tool modules and adapts both old (PluginMeta default export)
- * and new (named exports) formats.
+ * Tool module importer.
+ * Dynamically imports tool modules using named exports only.
*/
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import type { ToolSchemaShape } from '../plugin-types.ts';
+import type { ToolHandlerContext } from '../../rendering/types.ts';
import { getPackageRoot } from './load-manifest.ts';
-/**
- * Imported tool module interface.
- * This is what we extract from each tool module for runtime use.
- */
export interface ImportedToolModule {
schema: ToolSchemaShape;
- handler: (params: Record<string, unknown>) => Promise<unknown>;
+ handler: (params: Record<string, unknown>, ctx?: ToolHandlerContext) => Promise<unknown>;
}
-/**
- * Cache for imported modules.
- */
const moduleCache = new Map<string, ImportedToolModule>();
/**
* Import a tool module by its manifest module path.
*
- * Supports two module formats:
- * 1. Legacy: `export default { name, schema, handler, ... }`
- * 2. New: Named exports `{ schema, handler }`
+ * Accepts named exports only: `export const schema = ...` and `export const handler = ...`
*
* @param moduleId - Extensionless module path (e.g., 'mcp/tools/simulator/build_sim')
* @returns Imported tool module with schema and handler
*/
export async function importToolModule(moduleId: string): Promise<ImportedToolModule> {
- // Check cache first
const cached = moduleCache.get(moduleId);
if (cached) {
return cached;
@@ -51,56 +41,28 @@
throw new Error(`Failed to import tool module '${moduleId}': ${err}`);
}
- const result = extractToolExports(mod, moduleId);
+ if (!mod.schema || typeof mod.handler !== 'function') {
+ throw new Error(
+ `Tool module '${moduleId}' does not export the required shape. ` +
+ `Expected named exports: export const schema = ... and export const handler = ...`,
+ );
+ }
- // Cache the result
+ const result: ImportedToolModule = {
+ schema: mod.schema as ToolSchemaShape,
+ handler: mod.handler as (
+ params: Record<string, unknown>,
+ ctx?: ToolHandlerContext,
+ ) => Promise<unknown>,
+ };
+
moduleCache.set(moduleId, result);
-
return result;
}
/**
- * Extract tool exports from a module, supporting both legacy and new formats.
+ * Reset module cache (for tests).
*/
-function extractToolExports(mod: Record<string, unknown>, moduleId: string): ImportedToolModule {
- // Try legacy format first: default export with PluginMeta shape
- if (mod.default && typeof mod.default === 'object') {
- const defaultExport = mod.default as Record<string, unknown>;
-
- // Check if it looks like a PluginMeta (has schema and handler)
- if (defaultExport.schema && typeof defaultExport.handler === 'function') {
- return {
- schema: defaultExport.schema as ToolSchemaShape,
- handler: defaultExport.handler as (params: Record<string, unknown>) => Promise<unknown>,
- };
- }
- }
-
- // Try new format: named exports
- if (mod.schema && typeof mod.handler === 'function') {
- return {
- schema: mod.schema as ToolSchemaShape,
... diff truncated: showing 800 of 55013 linesThis Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
46fdf65 to
d2e93fa
Compare
f8a5827 to
23575fb
Compare
d2e93fa to
56e6ae9
Compare
23575fb to
0561a67
Compare
56e6ae9 to
d79dae4
Compare
0561a67 to
01e6478
Compare
d79dae4 to
9af3129
Compare
01e6478 to
8390e80
Compare
| statusLine('error', `Failed to pause debugger after attach: ${message}`), | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
New process interrupt behavior beyond mechanical migration
Medium Severity
When continueOnAttach is false, the old code simply skipped resuming and returned success. The new code actively sends a process interrupt LLDB command and then queries getExecutionState to determine the output message. This is a functional behavior change — not the "identical mechanical transformation" described in the PR. Sending process interrupt to a freshly-attached process that may already be stopped could produce unexpected side effects depending on the backend and debugger state.
Reviewed by Cursor Bugbot for commit 8390e80. Configure here.
There was a problem hiding this comment.
Intentional behavior improvement. The old code assumed the process was already stopped after attach, but that's not guaranteed. The new code explicitly pauses via process interrupt to ensure the advertised 'execution is paused' state is actually true. The DAP backend gracefully ignores 'already stopped' errors.
8390e80 to
fd818c3
Compare
9af3129 to
08d16bd
Compare
fd818c3 to
dad2143
Compare
08d16bd to
af53a77
Compare
c541317 to
f62e76a
Compare
fb11021 to
e31bf00
Compare
f62e76a to
aaa305d
Compare
e31bf00 to
57874da
Compare
57874da to
f7944fd
Compare
aaa305d to
9c7a22a
Compare
9c7a22a to
5c4723f
Compare
f7944fd to
6249b37
Compare
5c4723f to
5574554
Compare
6249b37 to
0ae8ed2
Compare
9886ffc to
9552f12
Compare
8f6196a to
e1fde1e
Compare
Merge activity
|
…-discovery, scaffolding, session-management, utilities, workflow-discovery, and xcode-ide tools to event-based handler contract
Replace 18 identical local runLogic definitions with the shared import from test-helpers.ts.
- Restore dropped coverage calculation assertions in tests - Wrap runCommand 'process interrupt' in enqueue for thread safety - Add mapError to swift_package_test withErrorHandling to prevent double header emission on error - Wrap doctor runDoctor body in try/finally to restore env variable
9552f12 to
6ba09ca
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6ba09ca. Configure here.
| expect(text.includes('AppDelegate.swift')).toBe(true); | ||
| expect(text.includes('ViewModel.swift')).toBe(true); | ||
| expect(text.includes('Service.swift')).toBe(true); | ||
| expect(text.includes('Model.swift')).toBe(true); |
There was a problem hiding this comment.
Coverage test drops per-file percentage assertions
Low Severity
The showFiles per-file breakdown test dropped all four percentage assertions (e.g., AppDelegate.swift: 20.0%, ViewModel.swift: 60.0%, Service.swift: 0.0%, Model.swift: 25.0%) and now only verifies filename presence via text.includes('AppDelegate.swift'). The underlying rendering still produces filename: pct% strings, so percentage assertions remain valid. Without them, a regression in per-file coverage calculation would go undetected. The PR already restored similar percentage assertions for the happy-path and nested-targets tests per reviewer feedback, but this test was missed.
Reviewed by Cursor Bugbot for commit 6ba09ca. Configure here.




Summary
This is PR 9 of 12 in a stacked PR series that decouples the rendering pipeline from MCP transport. Depends on PR 8 (UI automation migrations). This completes the tool migration -- after this PR, every tool handler in the codebase uses the new event-based contract.
Tools migrated (68 files)
Debugging tools:
debug_attach_sim,debug_breakpoint_add,debug_breakpoint_remove,debug_continue,debug_detach,debug_lldb_command,debug_stack,debug_variablesSwift package tools:
swift_package_build,swift_package_clean,swift_package_list,swift_package_run,swift_package_stop,swift_package_testCoverage tools:
get_coverage_report,get_file_coverageProject discovery tools:
discover_projs,get_app_bundle_id,get_mac_bundle_id,list_schemes,show_build_settingsProject scaffolding tools:
scaffold_ios_project,scaffold_macos_projectSession management tools:
session_clear_defaults,session_set_defaults,session_show_defaults,session_use_defaults_profileOther tools:
doctor,clean,manage_workflows,sync_xcode_defaults,xcode_ide_call_tool,xcode_ide_list_tools,xcode_tools_bridge_disconnect,xcode_tools_bridge_status,xcode_tools_bridge_syncNotable changes
session-format-helpers.ts): Extracted formatting logic for session default display that was duplicated across the 4 session management tools.src/utils/debugger/): Updated DAP backend, transport, and UI automation guard to work with the event-based context. The debugger's state machine continues to work the same way -- events are emitted instead of constructing response content.test-preflight.tsand xcodebuild pipeline modules as the platform-specific test tools.Why this is one PR despite the file count
All 68 files follow the identical mechanical transformation (
return toolResponse([...])toctx.emit(...)). Splitting by tool category would create 5+ PRs with no architectural difference between them. Reviewers can verify the pattern in a few files and trust it applies uniformly. The file count is high but the conceptual complexity is low.Stack navigation
Test plan
npx vitest runpasses -- all tool tests updatedctx.emit(no remainingtoolResponsecalls)