Skip to content

Commit 442796b

Browse files
cameroncookeclaude
andcommitted
fix(next-steps): Use tool-provided params over arg placeholders
Stop resolving manifest next-step params from raw invocation args and rely on tool-provided nextStepParams for canonical runtime values. Replace YAML arg placeholders with static defaults and update snapshot_ui to return dynamic params for all templated follow-up actions. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8cd48d5 commit 442796b

8 files changed

Lines changed: 35 additions & 60 deletions

File tree

manifests/tools/boot_sim.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ nextSteps:
1414
- label: Install an app
1515
toolId: install_app_sim
1616
params:
17-
simulatorId: ${simulatorId}
17+
simulatorId: SIMULATOR_UUID
1818
appPath: PATH_TO_YOUR_APP
1919
priority: 2
2020
- label: Launch an app
2121
toolId: launch_app_sim
2222
params:
23-
simulatorId: ${simulatorId}
23+
simulatorId: SIMULATOR_UUID
2424
bundleId: YOUR_APP_BUNDLE_ID
2525
priority: 3

manifests/tools/launch_app_sim.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ nextSteps:
1414
- label: Capture structured logs (app continues running)
1515
toolId: start_sim_log_cap
1616
params:
17-
simulatorId: ${simulatorId}
18-
bundleId: ${bundleId}
17+
simulatorId: SIMULATOR_UUID
18+
bundleId: BUNDLE_ID
1919
priority: 2
2020
- label: Capture console + structured logs (app restarts)
2121
toolId: start_sim_log_cap
2222
params:
23-
simulatorId: ${simulatorId}
24-
bundleId: ${bundleId}
23+
simulatorId: SIMULATOR_UUID
24+
bundleId: BUNDLE_ID
2525
captureConsole: true
2626
priority: 3

manifests/tools/snapshot_ui.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ nextSteps:
88
- label: Refresh after layout changes
99
toolId: snapshot_ui
1010
params:
11-
simulatorId: ${simulatorId}
11+
simulatorId: SIMULATOR_UUID
1212
- label: Tap on element
1313
toolId: tap
1414
params:
15-
simulatorId: ${simulatorId}
15+
simulatorId: SIMULATOR_UUID
1616
x: 0
1717
y: 0
1818
- label: Take screenshot for verification
1919
toolId: screenshot
2020
params:
21-
simulatorId: ${simulatorId}
21+
simulatorId: SIMULATOR_UUID
2222
annotations:
2323
title: Snapshot UI
2424
readOnlyHint: true

src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ describe('Snapshot UI Plugin', () => {
102102
text: 'Tips:\n- Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)\n- If a debugger is attached, ensure the app is running (not stopped on breakpoints)\n- Screenshots are for visual verification only',
103103
},
104104
],
105+
nextStepParams: {
106+
snapshot_ui: { simulatorId: '12345678-1234-4234-8234-123456789012' },
107+
tap: { simulatorId: '12345678-1234-4234-8234-123456789012', x: 0, y: 0 },
108+
screenshot: { simulatorId: '12345678-1234-4234-8234-123456789012' },
109+
},
105110
});
106111
});
107112

src/mcp/tools/ui-automation/snapshot_ui.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ export async function snapshot_uiLogic(
8686
text: `Tips:\n- Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)\n- If a debugger is attached, ensure the app is running (not stopped on breakpoints)\n- Screenshots are for visual verification only`,
8787
},
8888
],
89+
nextStepParams: {
90+
snapshot_ui: { simulatorId },
91+
tap: { simulatorId, x: 0, y: 0 },
92+
screenshot: { simulatorId },
93+
},
8994
};
9095
if (guard.warningText) {
9196
response.content.push({ type: 'text', text: guard.warningText });

src/runtime/__tests__/tool-invoker.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,14 @@ describe('DefaultToolInvoker next steps post-processing', () => {
252252
]);
253253
});
254254

255-
it('injects manifest template next steps when a response omits nextSteps', async () => {
256-
const directHandler = vi.fn().mockResolvedValue(textResponse('ok'));
255+
it('injects manifest template next steps from dynamic nextStepParams when response omits nextSteps', async () => {
256+
const directHandler = vi.fn().mockResolvedValue({
257+
content: [{ type: 'text', text: 'ok' }],
258+
nextStepParams: {
259+
snapshot_ui: { simulatorId: '12345678-1234-4234-8234-123456789012' },
260+
tap: { simulatorId: '12345678-1234-4234-8234-123456789012', x: 0, y: 0 },
261+
},
262+
} satisfies ToolResponse);
257263
const catalog = createToolCatalog([
258264
makeTool({
259265
id: 'snapshot_ui',
@@ -265,15 +271,15 @@ describe('DefaultToolInvoker next steps post-processing', () => {
265271
{
266272
label: 'Refresh',
267273
toolId: 'snapshot_ui',
268-
params: { simulatorId: '${simulatorId}' },
274+
params: { simulatorId: 'SIMULATOR_UUID' },
269275
},
270276
{
271277
label: 'Visually verify hierarchy output',
272278
},
273279
{
274280
label: 'Tap on element',
275281
toolId: 'tap',
276-
params: { simulatorId: '${simulatorId}', x: 0, y: 0 },
282+
params: { simulatorId: 'SIMULATOR_UUID', x: 0, y: 0 },
277283
},
278284
],
279285
handler: directHandler,
@@ -289,11 +295,7 @@ describe('DefaultToolInvoker next steps post-processing', () => {
289295
]);
290296

291297
const invoker = new DefaultToolInvoker(catalog);
292-
const response = await invoker.invoke(
293-
'snapshot-ui',
294-
{ simulatorId: '12345678-1234-4234-8234-123456789012' },
295-
{ runtime: 'cli' },
296-
);
298+
const response = await invoker.invoke('snapshot-ui', {}, { runtime: 'cli' });
297299

298300
expect(response.nextSteps).toEqual([
299301
{

src/runtime/tool-invoker.ts

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,13 @@ import {
1212
type SentryToolTransport,
1313
} from '../utils/sentry.ts';
1414

15-
/**
16-
* Resolve template params using input args.
17-
* Supports primitive passthrough and ${argName} substitution.
18-
*/
19-
function resolveTemplateParams(
20-
params: Record<string, string | number | boolean>,
21-
args: Record<string, unknown>,
22-
): Record<string, string | number | boolean> {
23-
const resolved: Record<string, string | number | boolean> = {};
24-
25-
for (const [key, value] of Object.entries(params)) {
26-
if (typeof value === 'string') {
27-
const match = value.match(/^\$\{([^}]+)\}$/);
28-
if (match) {
29-
const argValue = args[match[1]];
30-
if (
31-
typeof argValue === 'string' ||
32-
typeof argValue === 'number' ||
33-
typeof argValue === 'boolean'
34-
) {
35-
resolved[key] = argValue;
36-
continue;
37-
}
38-
}
39-
}
40-
resolved[key] = value;
41-
}
42-
43-
return resolved;
44-
}
45-
4615
type BuiltTemplateNextStep = {
4716
step: NextStep;
4817
templateToolId?: string;
4918
};
5019

5120
function buildTemplateNextSteps(
5221
tool: ToolDefinition,
53-
args: Record<string, unknown>,
5422
catalog: ToolCatalog,
5523
): BuiltTemplateNextStep[] {
5624
if (!tool.nextStepTemplates || tool.nextStepTemplates.length === 0) {
@@ -78,7 +46,7 @@ function buildTemplateNextSteps(
7846
step: {
7947
tool: target.mcpName,
8048
label: template.label,
81-
params: resolveTemplateParams(template.params ?? {}, args),
49+
params: template.params ?? {},
8250
priority: template.priority,
8351
},
8452
templateToolId: template.toolId,
@@ -194,13 +162,12 @@ function normalizeNextSteps(
194162
export function postProcessToolResponse(params: {
195163
tool: ToolDefinition;
196164
response: ToolResponse;
197-
args: Record<string, unknown>;
198165
catalog: ToolCatalog;
199166
runtime: InvokeOptions['runtime'];
200167
}): ToolResponse {
201-
const { tool, response, args, catalog, runtime } = params;
168+
const { tool, response, catalog, runtime } = params;
202169

203-
const templateSteps = buildTemplateNextSteps(tool, args, catalog);
170+
const templateSteps = buildTemplateNextSteps(tool, catalog);
204171
const canApplyTemplates =
205172
templateSteps.length > 0 &&
206173
(!response.nextSteps ||
@@ -292,15 +259,13 @@ export class DefaultToolInvoker implements ToolInvoker {
292259

293260
private buildPostProcessParams(
294261
tool: ToolDefinition,
295-
args: Record<string, unknown>,
296262
runtime: InvokeOptions['runtime'],
297263
): {
298264
tool: ToolDefinition;
299-
args: Record<string, unknown>;
300265
catalog: ToolCatalog;
301266
runtime: InvokeOptions['runtime'];
302267
} {
303-
return { tool, args, catalog: this.catalog, runtime };
268+
return { tool, catalog: this.catalog, runtime };
304269
}
305270

306271
private async invokeViaDaemon(
@@ -313,7 +278,6 @@ export class DefaultToolInvoker implements ToolInvoker {
313278
captureInvocationMetric: (outcome: SentryToolInvocationOutcome) => void;
314279
postProcessParams: {
315280
tool: ToolDefinition;
316-
args: Record<string, unknown>;
317281
catalog: ToolCatalog;
318282
runtime: InvokeOptions['runtime'];
319283
};
@@ -407,7 +371,7 @@ export class DefaultToolInvoker implements ToolInvoker {
407371
});
408372
};
409373

410-
const postProcessParams = this.buildPostProcessParams(tool, args, opts.runtime);
374+
const postProcessParams = this.buildPostProcessParams(tool, opts.runtime);
411375
const xcodeIdeRemoteToolName = tool.xcodeIdeRemoteToolName;
412376
const isDynamicXcodeIdeTool =
413377
tool.workflow === 'xcode-ide' && typeof xcodeIdeRemoteToolName === 'string';

src/utils/tool-registry.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ export async function applyWorkflowSelectionFromManifest(
148148
? postProcessToolResponse({
149149
tool: catalogTool,
150150
response: response as ToolResponse,
151-
args: args as Record<string, unknown>,
152151
catalog,
153152
runtime: 'mcp',
154153
})

0 commit comments

Comments
 (0)