diff --git a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts index ac2fec8f..0a8e4bff 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -5,109 +5,50 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; -import { - createMockExecutor, - createMockFileSystemExecutor, -} from '../../../../test-utils/mock-executors.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { sessionStore } from '../../../../utils/session-store.ts'; import buildRunSim, { build_run_simLogic } from '../build_run_sim.ts'; describe('build_run_sim tool', () => { + beforeEach(() => { + sessionStore.clear(); + }); + describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(buildRunSim.name).toBe('build_run_sim'); }); it('should have correct description', () => { - expect(buildRunSim.description).toBe( - "Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName. IMPORTANT: Requires either projectPath or workspacePath, plus scheme and either simulatorId or simulatorName. Example: build_run_sim({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', simulatorName: 'iPhone 16' })", - ); + expect(buildRunSim.description).toBe('Builds and runs an app on an iOS simulator.'); }); it('should have handler function', () => { expect(typeof buildRunSim.handler).toBe('function'); }); - it('should have correct schema with required and optional fields', () => { + it('should expose only non-session fields in public schema', () => { const schema = z.object(buildRunSim.schema); - // Valid inputs - workspace - expect( - schema.safeParse({ - workspacePath: '/path/to/workspace', - scheme: 'MyScheme', - simulatorName: 'iPhone 16', - }).success, - ).toBe(true); + expect(schema.safeParse({}).success).toBe(true); - // Valid inputs - project expect( schema.safeParse({ - projectPath: '/path/to/project.xcodeproj', - scheme: 'MyScheme', - simulatorName: 'iPhone 16', - }).success, - ).toBe(true); - - expect( - schema.safeParse({ - workspacePath: '/path/to/workspace', - scheme: 'MyScheme', - simulatorName: 'iPhone 16', - configuration: 'Release', derivedDataPath: '/path/to/derived', extraArgs: ['--verbose'], - useLatestOS: true, preferXcodebuild: false, }).success, ).toBe(true); - // Invalid inputs - missing required fields - // Note: simulatorId/simulatorName are optional at schema level, XOR validation at runtime - expect( - schema.safeParse({ - workspacePath: '/path/to/workspace', - scheme: 'MyScheme', - }).success, - ).toBe(true); // Schema validation passes, runtime XOR validation would catch missing simulator fields - - expect( - schema.safeParse({ - projectPath: '/path/to/project.xcodeproj', - simulatorName: 'iPhone 16', - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - scheme: 'MyScheme', - simulatorName: 'iPhone 16', - }).success, - ).toBe(true); // Base schema allows this, XOR validation happens in handler + expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); + expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); - // Invalid types - expect( - schema.safeParse({ - workspacePath: 123, - scheme: 'MyScheme', - simulatorName: 'iPhone 16', - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - workspacePath: '/path/to/workspace', - scheme: 123, - simulatorName: 'iPhone 16', - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - workspacePath: '/path/to/workspace', - scheme: 'MyScheme', - simulatorName: 123, - }).success, - ).toBe(false); + const schemaKeys = Object.keys(buildRunSim.schema).sort(); + expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); + expect(schemaKeys).not.toContain('scheme'); + expect(schemaKeys).not.toContain('simulatorName'); + expect(schemaKeys).not.toContain('projectPath'); }); }); @@ -600,7 +541,8 @@ describe('build_run_sim tool', () => { simulatorName: 'iPhone 16', }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Either projectPath or workspacePath is required'); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should error when both projectPath and workspacePath provided', async () => { @@ -611,7 +553,10 @@ describe('build_run_sim tool', () => { simulatorName: 'iPhone 16', }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('mutually exclusive'); + expect(result.content[0].text).toContain('Parameter validation failed'); + expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); + expect(result.content[0].text).toContain('projectPath'); + expect(result.content[0].text).toContain('workspacePath'); }); it('should succeed with only projectPath', async () => { diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 3241d592..81642d25 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -10,6 +10,7 @@ import { z } from 'zod'; import { ToolResponse, SharedBuildParams, XcodePlatform } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; import { createTextResponse } from '../../../utils/responses/index.ts'; import { executeXcodeBuildCommand } from '../../../utils/build/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; @@ -489,37 +490,32 @@ When done with any option, use: stop_sim_log_cap({ logSessionId: 'SESSION_ID' }) } } +const publicSchemaObject = baseSchemaObject.omit({ + projectPath: true, + workspacePath: true, + scheme: true, + configuration: true, + simulatorId: true, + simulatorName: true, + useLatestOS: true, +} as const); + export default { name: 'build_run_sim', - description: - "Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName. IMPORTANT: Requires either projectPath or workspacePath, plus scheme and either simulatorId or simulatorName. Example: build_run_sim({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', simulatorName: 'iPhone 16' })", - schema: baseSchemaObject.shape, // MCP SDK compatibility - handler: async (args: Record): Promise => { - try { - // Runtime validation with XOR constraints - const validatedParams = buildRunSimulatorSchema.parse(args); - return await build_run_simLogic(validatedParams, getDefaultCommandExecutor()); - } catch (error) { - if (error instanceof z.ZodError) { - // Format validation errors in a user-friendly way - const errorMessages = error.errors.map((e) => { - const path = e.path.length > 0 ? `${e.path.join('.')}` : 'root'; - return `${path}: ${e.message}`; - }); - - return { - content: [ - { - type: 'text', - text: `Parameter validation failed. Invalid parameters:\n${errorMessages.join('\n')}`, - }, - ], - isError: true, - }; - } - - // Re-throw unexpected errors - throw error; - } - }, + description: 'Builds and runs an app on an iOS simulator.', + schema: publicSchemaObject.shape, + handler: createSessionAwareTool({ + internalSchema: buildRunSimulatorSchema as unknown as z.ZodType, + logicFunction: build_run_simLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [ + { allOf: ['scheme'], message: 'scheme is required' }, + { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, + { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, + ], + exclusivePairs: [ + ['projectPath', 'workspacePath'], + ['simulatorId', 'simulatorName'], + ], + }), };