Skip to content

Commit de72b23

Browse files
committed
fix(session-aware): reject multiple explicit args in exclusivePairs at factory level; add test; update build_sim tests to expect factory message
1 parent cb6156a commit de72b23

3 files changed

Lines changed: 52 additions & 6 deletions

File tree

src/mcp/tools/simulator/__tests__/build_sim.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ describe('build_sim tool', () => {
6868

6969
expect(result.isError).toBe(true);
7070
expect(result.content[0].text).toContain('Parameter validation failed');
71-
expect(result.content[0].text).toContain(
72-
'projectPath and workspacePath are mutually exclusive',
73-
);
71+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
72+
expect(result.content[0].text).toContain('projectPath');
73+
expect(result.content[0].text).toContain('workspacePath');
7474
});
7575

7676
it('should handle empty workspacePath parameter', async () => {
@@ -158,9 +158,9 @@ describe('build_sim tool', () => {
158158

159159
expect(result.isError).toBe(true);
160160
expect(result.content[0].text).toContain('Parameter validation failed');
161-
expect(result.content[0].text).toContain(
162-
'simulatorId and simulatorName are mutually exclusive',
163-
);
161+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
162+
expect(result.content[0].text).toContain('simulatorId');
163+
expect(result.content[0].text).toContain('simulatorName');
164164
});
165165

166166
it('should handle empty simulatorName parameter', async () => {

src/utils/__tests__/session-aware-tool-factory.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,36 @@ describe('createSessionAwareTool', () => {
155155
expect(res.isError).toBe(false);
156156
expect(res.content[0].text).toBe('OK');
157157
});
158+
159+
it('rejects when multiple explicit args in an exclusive pair are provided (factory-level)', async () => {
160+
const internalSchemaNoXor = z.object({
161+
scheme: z.string(),
162+
projectPath: z.string().optional(),
163+
workspacePath: z.string().optional(),
164+
});
165+
166+
const handlerNoXor = createSessionAwareTool<z.infer<typeof internalSchemaNoXor>>({
167+
internalSchema: internalSchemaNoXor,
168+
logicFunction: (async () => ({
169+
content: [{ type: 'text', text: 'OK' }],
170+
isError: false,
171+
})) as any,
172+
getExecutor: () => createMockExecutor({ success: true }),
173+
requirements: [{ allOf: ['scheme'], message: 'scheme is required' }],
174+
exclusivePairs: [['projectPath', 'workspacePath']],
175+
});
176+
177+
const res = await handlerNoXor({
178+
scheme: 'App',
179+
projectPath: '/path/a.xcodeproj',
180+
workspacePath: '/path/b.xcworkspace',
181+
});
182+
183+
expect(res.isError).toBe(true);
184+
const msg = res.content[0].text;
185+
expect(msg).toContain('Parameter validation failed');
186+
expect(msg).toContain('Mutually exclusive parameters provided');
187+
expect(msg).toContain('projectPath');
188+
expect(msg).toContain('workspacePath');
189+
});
158190
});

src/utils/typed-tool-factory.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ export function createSessionAwareTool<TParams>(opts: {
9595
if (v !== null && v !== undefined) sanitizedArgs[k] = v;
9696
}
9797

98+
// Factory-level mutual exclusivity check: if user provides multiple explicit values
99+
// within an exclusive group, reject early even if tool schema doesn't enforce XOR.
100+
for (const pair of exclusivePairs) {
101+
const provided = pair.filter((k) => Object.prototype.hasOwnProperty.call(sanitizedArgs, k));
102+
if (provided.length >= 2) {
103+
return createErrorResponse(
104+
'Parameter validation failed',
105+
`Invalid parameters:\nMutually exclusive parameters provided: ${provided.join(
106+
', ',
107+
)}. Provide only one.`,
108+
);
109+
}
110+
}
111+
98112
// Start with session defaults merged with explicit args (args override session)
99113
const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...sanitizedArgs };
100114

0 commit comments

Comments
 (0)