Skip to content

Commit 4bbfe5d

Browse files
authored
feat(project-discovery): migrate clean/list/show to session defaults (getsentry#125)
1 parent 79736a3 commit 4bbfe5d

File tree

8 files changed

+154
-74
lines changed

8 files changed

+154
-74
lines changed

docs/session-aware-migration-todo.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ _Audit date: October 6, 2025_
55
Reference: `docs/session_management_plan.md`
66

77
## Utilities
8-
- [ ] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
8+
- [x] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
99

1010
## Project Discovery
11-
- [ ] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`.
12-
- [ ] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
11+
- [x] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`.
12+
- [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
1313

1414
## Device Workflows
1515
- [ ] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.

src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,35 @@
44
* Using dependency injection for deterministic testing
55
*/
66

7-
import { describe, it, expect } from 'vitest';
7+
import { describe, it, expect, beforeEach } from 'vitest';
88
import { z } from 'zod';
99
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
1010
import plugin, { listSchemesLogic } from '../list_schemes.ts';
11+
import { sessionStore } from '../../../../utils/session-store.ts';
1112

1213
describe('list_schemes plugin', () => {
14+
beforeEach(() => {
15+
sessionStore.clear();
16+
});
17+
1318
describe('Export Field Validation (Literal)', () => {
1419
it('should have correct name', () => {
1520
expect(plugin.name).toBe('list_schemes');
1621
});
1722

1823
it('should have correct description', () => {
19-
expect(plugin.description).toBe(
20-
"Lists available schemes for either a project or a workspace. Provide exactly one of projectPath or workspacePath. Example: list_schemes({ projectPath: '/path/to/MyProject.xcodeproj' })",
21-
);
24+
expect(plugin.description).toBe('Lists schemes for a project or workspace.');
2225
});
2326

2427
it('should have handler function', () => {
2528
expect(typeof plugin.handler).toBe('function');
2629
});
2730

28-
it('should validate schema with valid inputs', () => {
29-
const schema = z.object(plugin.schema);
30-
expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(true);
31-
expect(schema.safeParse({ projectPath: '/Users/dev/App.xcodeproj' }).success).toBe(true);
32-
});
33-
34-
it('should validate schema with invalid inputs', () => {
35-
const schema = z.object(plugin.schema);
36-
// Base schema allows empty object - XOR validation is in refinements
31+
it('should expose an empty public schema', () => {
32+
const schema = z.object(plugin.schema).strict();
3733
expect(schema.safeParse({}).success).toBe(true);
38-
expect(schema.safeParse({ projectPath: 123 }).success).toBe(false);
39-
expect(schema.safeParse({ projectPath: null }).success).toBe(false);
40-
expect(schema.safeParse({ workspacePath: 123 }).success).toBe(false);
34+
expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false);
35+
expect(Object.keys(plugin.schema)).toEqual([]);
4136
});
4237
});
4338

@@ -235,16 +230,17 @@ describe('list_schemes plugin', () => {
235230
// to verify Zod validation works properly. The createTypedTool wrapper handles validation.
236231
const result = await plugin.handler({});
237232
expect(result.isError).toBe(true);
238-
expect(result.content[0].text).toContain('Parameter validation failed');
239-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
233+
expect(result.content[0].text).toContain('Missing required session defaults');
234+
expect(result.content[0].text).toContain('Provide a project or workspace');
240235
});
241236
});
242237

243238
describe('XOR Validation', () => {
244239
it('should error when neither projectPath nor workspacePath provided', async () => {
245240
const result = await plugin.handler({});
246241
expect(result.isError).toBe(true);
247-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
242+
expect(result.content[0].text).toContain('Missing required session defaults');
243+
expect(result.content[0].text).toContain('Provide a project or workspace');
248244
});
249245

250246
it('should error when both projectPath and workspacePath provided', async () => {
@@ -253,7 +249,7 @@ describe('list_schemes plugin', () => {
253249
workspacePath: '/path/to/workspace.xcworkspace',
254250
});
255251
expect(result.isError).toBe(true);
256-
expect(result.content[0].text).toContain('mutually exclusive');
252+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
257253
});
258254

259255
it('should handle empty strings as undefined', async () => {
@@ -262,7 +258,8 @@ describe('list_schemes plugin', () => {
262258
workspacePath: '',
263259
});
264260
expect(result.isError).toBe(true);
265-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
261+
expect(result.content[0].text).toContain('Missing required session defaults');
262+
expect(result.content[0].text).toContain('Provide a project or workspace');
266263
});
267264
});
268265

src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, beforeEach } from 'vitest';
22
import { z } from 'zod';
33
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
44
import plugin, { showBuildSettingsLogic } from '../show_build_settings.ts';
5+
import { sessionStore } from '../../../../utils/session-store.ts';
56

67
describe('show_build_settings plugin', () => {
8+
beforeEach(() => {
9+
sessionStore.clear();
10+
});
711
describe('Export Field Validation (Literal)', () => {
812
it('should have correct name', () => {
913
expect(plugin.name).toBe('show_build_settings');
1014
});
1115

1216
it('should have correct description', () => {
13-
expect(plugin.description).toBe(
14-
"Shows build settings from either a project or workspace using xcodebuild. Provide exactly one of projectPath or workspacePath, plus scheme. Example: show_build_settings({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
15-
);
17+
expect(plugin.description).toBe('Shows xcodebuild build settings.');
1618
});
1719

1820
it('should have handler function', () => {
1921
expect(typeof plugin.handler).toBe('function');
2022
});
2123

22-
it('should have schema object', () => {
23-
expect(plugin.schema).toBeDefined();
24-
expect(typeof plugin.schema).toBe('object');
24+
it('should expose an empty public schema', () => {
25+
const schema = z.object(plugin.schema).strict();
26+
expect(schema.safeParse({}).success).toBe(true);
27+
expect(schema.safeParse({ projectPath: '/path.xcodeproj' }).success).toBe(false);
28+
expect(schema.safeParse({ scheme: 'App' }).success).toBe(false);
29+
expect(Object.keys(plugin.schema)).toEqual([]);
2530
});
2631
});
2732

@@ -50,8 +55,8 @@ describe('show_build_settings plugin', () => {
5055
});
5156

5257
expect(result.isError).toBe(true);
53-
expect(result.content[0].text).toContain('Parameter validation failed');
54-
expect(result.content[0].text).toContain('projectPath');
58+
expect(result.content[0].text).toContain('Missing required session defaults');
59+
expect(result.content[0].text).toContain('Provide a project or workspace');
5560
});
5661

5762
it('should return success with build settings', async () => {
@@ -169,7 +174,8 @@ describe('show_build_settings plugin', () => {
169174
});
170175

171176
expect(result.isError).toBe(true);
172-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
177+
expect(result.content[0].text).toContain('Missing required session defaults');
178+
expect(result.content[0].text).toContain('Provide a project or workspace');
173179
});
174180

175181
it('should error when both projectPath and workspacePath provided', async () => {
@@ -180,7 +186,7 @@ describe('show_build_settings plugin', () => {
180186
});
181187

182188
expect(result.isError).toBe(true);
183-
expect(result.content[0].text).toContain('mutually exclusive');
189+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
184190
});
185191

186192
it('should work with projectPath only', async () => {
@@ -214,6 +220,28 @@ describe('show_build_settings plugin', () => {
214220
});
215221
});
216222

223+
describe('Session requirement handling', () => {
224+
it('should require scheme when not provided', async () => {
225+
const result = await plugin.handler({
226+
projectPath: '/path/to/MyProject.xcodeproj',
227+
} as any);
228+
229+
expect(result.isError).toBe(true);
230+
expect(result.content[0].text).toContain('Missing required session defaults');
231+
expect(result.content[0].text).toContain('scheme is required');
232+
});
233+
234+
it('should surface project/workspace requirement even with scheme default', async () => {
235+
sessionStore.setDefaults({ scheme: 'MyScheme' });
236+
237+
const result = await plugin.handler({});
238+
239+
expect(result.isError).toBe(true);
240+
expect(result.content[0].text).toContain('Missing required session defaults');
241+
expect(result.content[0].text).toContain('Provide a project or workspace');
242+
});
243+
});
244+
217245
describe('showBuildSettingsLogic function', () => {
218246
it('should return success with build settings', async () => {
219247
const calls: any[] = [];

src/mcp/tools/project-discovery/list_schemes.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { CommandExecutor } from '../../../utils/execution/index.ts';
1111
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
1212
import { createTextResponse } from '../../../utils/responses/index.ts';
1313
import { ToolResponse } from '../../../types/common.ts';
14-
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
14+
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
1515
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
1616

1717
// Unified schema: XOR between projectPath and workspacePath
@@ -109,14 +109,22 @@ export async function listSchemesLogic(
109109
}
110110
}
111111

112+
const publicSchemaObject = baseSchemaObject.omit({
113+
projectPath: true,
114+
workspacePath: true,
115+
} as const);
116+
112117
export default {
113118
name: 'list_schemes',
114-
description:
115-
"Lists available schemes for either a project or a workspace. Provide exactly one of projectPath or workspacePath. Example: list_schemes({ projectPath: '/path/to/MyProject.xcodeproj' })",
116-
schema: baseSchemaObject.shape,
117-
handler: createTypedTool<ListSchemesParams>(
118-
listSchemesSchema as z.ZodType<ListSchemesParams>,
119-
listSchemesLogic,
120-
getDefaultCommandExecutor,
121-
),
119+
description: 'Lists schemes for a project or workspace.',
120+
schema: publicSchemaObject.shape,
121+
handler: createSessionAwareTool<ListSchemesParams>({
122+
internalSchema: listSchemesSchema as unknown as z.ZodType<ListSchemesParams>,
123+
logicFunction: listSchemesLogic,
124+
getExecutor: getDefaultCommandExecutor,
125+
requirements: [
126+
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
127+
],
128+
exclusivePairs: [['projectPath', 'workspacePath']],
129+
}),
122130
};

src/mcp/tools/project-discovery/show_build_settings.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { CommandExecutor } from '../../../utils/execution/index.ts';
1111
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
1212
import { createTextResponse } from '../../../utils/responses/index.ts';
1313
import { ToolResponse } from '../../../types/common.ts';
14-
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
14+
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
1515
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
1616

1717
// Unified schema: XOR between projectPath and workspacePath
@@ -102,14 +102,24 @@ export async function showBuildSettingsLogic(
102102
}
103103
}
104104

105+
const publicSchemaObject = baseSchemaObject.omit({
106+
projectPath: true,
107+
workspacePath: true,
108+
scheme: true,
109+
} as const);
110+
105111
export default {
106112
name: 'show_build_settings',
107-
description:
108-
"Shows build settings from either a project or workspace using xcodebuild. Provide exactly one of projectPath or workspacePath, plus scheme. Example: show_build_settings({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
109-
schema: baseSchemaObject.shape,
110-
handler: createTypedTool<ShowBuildSettingsParams>(
111-
showBuildSettingsSchema as z.ZodType<ShowBuildSettingsParams>,
112-
showBuildSettingsLogic,
113-
getDefaultCommandExecutor,
114-
),
113+
description: 'Shows xcodebuild build settings.',
114+
schema: publicSchemaObject.shape,
115+
handler: createSessionAwareTool<ShowBuildSettingsParams>({
116+
internalSchema: showBuildSettingsSchema as unknown as z.ZodType<ShowBuildSettingsParams>,
117+
logicFunction: showBuildSettingsLogic,
118+
getExecutor: getDefaultCommandExecutor,
119+
requirements: [
120+
{ allOf: ['scheme'], message: 'scheme is required' },
121+
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
122+
],
123+
exclusivePairs: [['projectPath', 'workspacePath']],
124+
}),
115125
};

src/mcp/tools/utilities/__tests__/clean.test.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { z } from 'zod';
23
import tool, { cleanLogic } from '../clean.ts';
34
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
5+
import { sessionStore } from '../../../../utils/session-store.ts';
46

57
describe('clean (unified) tool', () => {
8+
beforeEach(() => {
9+
sessionStore.clear();
10+
});
11+
612
it('exports correct name/description/schema/handler', () => {
713
expect(tool.name).toBe('clean');
8-
expect(typeof tool.description).toBe('string');
9-
expect(tool.schema).toBeDefined();
14+
expect(tool.description).toBe('Cleans build products with xcodebuild.');
1015
expect(typeof tool.handler).toBe('function');
16+
17+
const schema = z.object(tool.schema).strict();
18+
expect(schema.safeParse({}).success).toBe(true);
19+
expect(
20+
schema.safeParse({
21+
derivedDataPath: '/tmp/Derived',
22+
extraArgs: ['--quiet'],
23+
preferXcodebuild: true,
24+
platform: 'iOS Simulator',
25+
}).success,
26+
).toBe(true);
27+
expect(schema.safeParse({ configuration: 'Debug' }).success).toBe(false);
28+
29+
const schemaKeys = Object.keys(tool.schema).sort();
30+
expect(schemaKeys).toEqual(
31+
['derivedDataPath', 'extraArgs', 'platform', 'preferXcodebuild'].sort(),
32+
);
1133
});
1234

1335
it('handler validation: error when neither projectPath nor workspacePath provided', async () => {
1436
const result = await (tool as any).handler({});
1537
expect(result.isError).toBe(true);
16-
const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? '');
17-
expect(text).toContain('Invalid parameters');
38+
const text = String(result.content?.[0]?.text ?? '');
39+
expect(text).toContain('Missing required session defaults');
40+
expect(text).toContain('Provide a project or workspace');
1841
});
1942

2043
it('handler validation: error when both projectPath and workspacePath provided', async () => {
@@ -23,8 +46,8 @@ describe('clean (unified) tool', () => {
2346
workspacePath: '/w.xcworkspace',
2447
});
2548
expect(result.isError).toBe(true);
26-
const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? '');
27-
expect(text).toContain('Invalid parameters');
49+
const text = String(result.content?.[0]?.text ?? '');
50+
expect(text).toContain('Mutually exclusive parameters provided');
2851
});
2952

3053
it('runs project-path flow via logic', async () => {
@@ -45,8 +68,9 @@ describe('clean (unified) tool', () => {
4568
it('handler validation: requires scheme when workspacePath is provided', async () => {
4669
const result = await (tool as any).handler({ workspacePath: '/w.xcworkspace' });
4770
expect(result.isError).toBe(true);
48-
const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? '');
49-
expect(text).toContain('Invalid parameters');
71+
const text = String(result.content?.[0]?.text ?? '');
72+
expect(text).toContain('Parameter validation failed');
73+
expect(text).toContain('scheme is required when workspacePath is provided');
5074
});
5175

5276
it('uses iOS platform by default', async () => {
@@ -121,7 +145,8 @@ describe('clean (unified) tool', () => {
121145
platform: 'InvalidPlatform',
122146
});
123147
expect(result.isError).toBe(true);
124-
const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? '');
125-
expect(text).toContain('Invalid parameters');
148+
const text = String(result.content?.[0]?.text ?? '');
149+
expect(text).toContain('Parameter validation failed');
150+
expect(text).toContain('platform');
126151
});
127152
});

0 commit comments

Comments
 (0)