Skip to content

Commit 551f4c9

Browse files
authored
fix(server): return valid preview action results (#5966)
Co-authored-by: duncan-vc <247855047+duncan-vc@users.noreply.github.com>
1 parent 402c9e0 commit 551f4c9

4 files changed

Lines changed: 54 additions & 20 deletions

File tree

apps/server/src/mcp/McpHttpServer.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ it.effect("registers annotated tools and preserves authenticated request context
219219
expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false);
220220
expect(clickTool?.tool.annotations?.destructiveHint).toBe(true);
221221
expect(clickTool?.tool.annotations?.openWorldHint).toBe(true);
222+
expect(clickTool?.tool.outputSchema).toEqual({
223+
type: "object",
224+
additionalProperties: false,
225+
description: "The preview action completed successfully.",
226+
});
222227

223228
const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate");
224229
expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false);
@@ -260,15 +265,24 @@ it.effect("registers annotated tools and preserves authenticated request context
260265
alternateTabId,
261266
);
262267

263-
const press = yield* server
264-
.callTool({ name: "preview_press", arguments: { key: "Enter" } })
265-
.pipe(
266-
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
267-
Effect.provideService(McpSchema.McpServerClient, client),
268-
);
269-
expect(press.isError).toBe(false);
270-
expect(press.structuredContent).toBeNull();
271-
expect(press.content).toEqual([{ type: "text", text: "null" }]);
268+
const actionRequests = [
269+
{ name: "preview_click", arguments: { x: 10, y: 10 } },
270+
{ name: "preview_type", arguments: { text: "Hello" } },
271+
{ name: "preview_press", arguments: { key: "Enter" } },
272+
{ name: "preview_scroll", arguments: { deltaY: 100 } },
273+
{ name: "preview_wait_for", arguments: { text: "Example" } },
274+
];
275+
for (const request of actionRequests) {
276+
const result = yield* server
277+
.callTool(request)
278+
.pipe(
279+
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
280+
Effect.provideService(McpSchema.McpServerClient, client),
281+
);
282+
expect(result.isError).toBe(false);
283+
expect(result.structuredContent).toEqual({});
284+
expect(result.content).toEqual([{ type: "text", text: "{}" }]);
285+
}
272286
}),
273287
).pipe(Effect.provide(TestLayer)),
274288
);

apps/server/src/mcp/toolkits/preview/handlers.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,14 @@ const handlers = {
7272
invokeTargeted<PreviewAutomationSetColorSchemeResult>("setColorScheme", input),
7373
preview_snapshot: (input) => invokeTargeted<PreviewAutomationSnapshot>("snapshot", input ?? {}),
7474
preview_click: (input) =>
75-
invokeTargeted<void>("click", input, input.timeoutMs).pipe(Effect.as(null)),
76-
preview_type: (input) =>
77-
invokeTargeted<void>("type", input, input.timeoutMs).pipe(Effect.as(null)),
78-
preview_press: (input) => invokeTargeted<void>("press", input).pipe(Effect.as(null)),
79-
preview_scroll: (input) => invokeTargeted<void>("scroll", input).pipe(Effect.as(null)),
75+
invokeTargeted<void>("click", input, input.timeoutMs).pipe(Effect.as({})),
76+
preview_type: (input) => invokeTargeted<void>("type", input, input.timeoutMs).pipe(Effect.as({})),
77+
preview_press: (input) => invokeTargeted<void>("press", input).pipe(Effect.as({})),
78+
preview_scroll: (input) => invokeTargeted<void>("scroll", input).pipe(Effect.as({})),
8079
preview_evaluate: (input) =>
8180
invokeTargeted<unknown>("evaluate", input).pipe(Effect.map((result) => result ?? null)),
8281
preview_wait_for: (input) =>
83-
invokeTargeted<void>("waitFor", input, input.timeoutMs).pipe(Effect.as(null)),
82+
invokeTargeted<void>("waitFor", input, input.timeoutMs).pipe(Effect.as({})),
8483
preview_recording_start: (input) =>
8584
invokeTargeted<PreviewAutomationRecordingStatus>("recordingStart", input ?? {}),
8685
preview_recording_stop: (input) =>

apps/server/src/mcp/toolkits/preview/tools.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,20 @@ it("exports provider-compatible object schemas with described parameters", () =>
5555
}
5656
}
5757
});
58+
59+
it("exports exact object result schemas for preview actions", () => {
60+
const actionNames = [
61+
"preview_click",
62+
"preview_type",
63+
"preview_press",
64+
"preview_scroll",
65+
"preview_wait_for",
66+
] as const;
67+
for (const name of actionNames) {
68+
expect(Tool.getJsonSchemaFromSchema(PreviewToolkit.tools[name].successSchema)).toEqual({
69+
type: "object",
70+
additionalProperties: false,
71+
description: "The preview action completed successfully.",
72+
});
73+
}
74+
});

apps/server/src/mcp/toolkits/preview/tools.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const dependencies = [
2929
PreviewAutomationBroker.PreviewAutomationBroker,
3030
];
3131

32+
const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate({
33+
description: "The preview action completed successfully.",
34+
});
35+
3236
const browserTool = <T extends Tool.Any>(tool: T): T =>
3337
tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T;
3438

@@ -117,7 +121,7 @@ export const PreviewClickTool = browserTool(
117121
description:
118122
"Click exactly one target in the tab selected by tabId, or this agent session's current tab when omitted. Prefer a Playwright locator; selector accepts legacy CSS; x and y must be supplied together.",
119123
parameters: PreviewAutomationClickInput,
120-
success: Schema.Null,
124+
success: PreviewActionResult,
121125
failure: PreviewAutomationError,
122126
dependencies,
123127
}).annotate(Tool.Title, "Click preview page"),
@@ -128,7 +132,7 @@ export const PreviewTypeTool = browserTool(
128132
description:
129133
"Insert literal text into one input in the tab selected by tabId, or this agent session's current tab when omitted. Prefer a Playwright locator; set clear=true to replace existing text.",
130134
parameters: PreviewAutomationTypeInput,
131-
success: Schema.Null,
135+
success: PreviewActionResult,
132136
failure: PreviewAutomationError,
133137
dependencies,
134138
}).annotate(Tool.Title, "Type into preview page"),
@@ -139,7 +143,7 @@ export const PreviewPressTool = browserTool(
139143
description:
140144
"Press one keyboard key in the tab selected by tabId, or this agent session's current tab when omitted. Examples: {key:'Enter'}, {key:'Escape'}, or {key:'a',modifiers:['Meta']}.",
141145
parameters: PreviewAutomationPressInput,
142-
success: Schema.Null,
146+
success: PreviewActionResult,
143147
failure: PreviewAutomationError,
144148
dependencies,
145149
}).annotate(Tool.Title, "Press key in preview page"),
@@ -150,7 +154,7 @@ export const PreviewScrollTool = safeBrowserTool(
150154
description:
151155
"Scroll the tab selected by tabId, or this agent session's current tab when omitted. Positive deltaY scrolls down and positive deltaX scrolls right; a locator/selector targets a container.",
152156
parameters: PreviewAutomationScrollInput,
153-
success: Schema.Null,
157+
success: PreviewActionResult,
154158
failure: PreviewAutomationError,
155159
dependencies,
156160
}).annotate(Tool.Title, "Scroll preview page"),
@@ -172,7 +176,7 @@ export const PreviewWaitForTool = readonlyBrowserTool(
172176
description:
173177
"Wait in the tab selected by tabId, or this agent session's current tab when omitted, until all supplied locator, selector, text, and URL conditions match.",
174178
parameters: PreviewAutomationWaitForInput,
175-
success: Schema.Null,
179+
success: PreviewActionResult,
176180
failure: PreviewAutomationError,
177181
dependencies,
178182
}).annotate(Tool.Title, "Wait for preview page condition"),

0 commit comments

Comments
 (0)