authorgravatar for duncan@ommsocial.co.zaduncan-vc <duncan@ommsocial.co.za> 2026-08-15 05:04:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:04:21+00:00
log551f4c99c1c42c767daea1f2920cfc8fbc78a1cb
treed0574f22fc706702e4e76eaeff4fa085732e5d1d
parent402c9e0748ced0125c1b40793f7d170efb9b6b2a
signature Signed by PGP key B5690EEEBB952194

fix(server): return valid preview action results (#5966)

Co-authored-by: duncan-vc <247855047+duncan-vc@users.noreply.github.com>

4 files changed, 54 insertions(+), 20 deletions(-)

apps/server/src/mcp/McpHttpServer.test.ts+23-9
......@@ -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+5-6
......@@ -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+17
......@@ -55,3 +55,20 @@ it("exports provider-compatible object schemas with described parameters", () =>
5555 }
5656 }
5757});
58
59it("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+9-5
......@@ -29,6 +29,10 @@ const dependencies = [
2929 PreviewAutomationBroker.PreviewAutomationBroker,
3030];
3131
32const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate({
33 description: "The preview action completed successfully.",
34});
35
3236const 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"),