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...@@ -219,6 +219,11 @@ it.effect("registers annotated tools and preserves authenticated request context
219 expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false);219 expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false);
220 expect(clickTool?.tool.annotations?.destructiveHint).toBe(true);220 expect(clickTool?.tool.annotations?.destructiveHint).toBe(true);
221 expect(clickTool?.tool.annotations?.openWorldHint).toBe(true);221 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
223 const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate");228 const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate");
224 expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false);229 expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false);
...@@ -260,15 +265,24 @@ it.effect("registers annotated tools and preserves authenticated request context...@@ -260,15 +265,24 @@ it.effect("registers annotated tools and preserves authenticated request context
260 alternateTabId,265 alternateTabId,
261 );266 );
262267
263 const press = yield* server268 const actionRequests = [
264 .callTool({ name: "preview_press", arguments: { key: "Enter" } })269 { name: "preview_click", arguments: { x: 10, y: 10 } },
265 .pipe(270 { name: "preview_type", arguments: { text: "Hello" } },
266 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),271 { name: "preview_press", arguments: { key: "Enter" } },
267 Effect.provideService(McpSchema.McpServerClient, client),272 { name: "preview_scroll", arguments: { deltaY: 100 } },
268 );273 { name: "preview_wait_for", arguments: { text: "Example" } },
269 expect(press.isError).toBe(false);274 ];
270 expect(press.structuredContent).toBeNull();275 for (const request of actionRequests) {
271 expect(press.content).toEqual([{ type: "text", text: "null" }]);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 }
272 }),286 }),
273 ).pipe(Effect.provide(TestLayer)),287 ).pipe(Effect.provide(TestLayer)),
274);288);
apps/server/src/mcp/toolkits/preview/handlers.ts+5-6
...@@ -72,15 +72,14 @@ const handlers = {...@@ -72,15 +72,14 @@ const handlers = {
72 invokeTargeted<PreviewAutomationSetColorSchemeResult>("setColorScheme", input),72 invokeTargeted<PreviewAutomationSetColorSchemeResult>("setColorScheme", input),
73 preview_snapshot: (input) => invokeTargeted<PreviewAutomationSnapshot>("snapshot", input ?? {}),73 preview_snapshot: (input) => invokeTargeted<PreviewAutomationSnapshot>("snapshot", input ?? {}),
74 preview_click: (input) =>74 preview_click: (input) =>
75 invokeTargeted<void>("click", input, input.timeoutMs).pipe(Effect.as(null)),75 invokeTargeted<void>("click", input, input.timeoutMs).pipe(Effect.as({})),
76 preview_type: (input) =>76 preview_type: (input) => invokeTargeted<void>("type", input, input.timeoutMs).pipe(Effect.as({})),
77 invokeTargeted<void>("type", input, input.timeoutMs).pipe(Effect.as(null)),77 preview_press: (input) => invokeTargeted<void>("press", input).pipe(Effect.as({})),
78 preview_press: (input) => invokeTargeted<void>("press", input).pipe(Effect.as(null)),78 preview_scroll: (input) => invokeTargeted<void>("scroll", input).pipe(Effect.as({})),
79 preview_scroll: (input) => invokeTargeted<void>("scroll", input).pipe(Effect.as(null)),
80 preview_evaluate: (input) =>79 preview_evaluate: (input) =>
81 invokeTargeted<unknown>("evaluate", input).pipe(Effect.map((result) => result ?? null)),80 invokeTargeted<unknown>("evaluate", input).pipe(Effect.map((result) => result ?? null)),
82 preview_wait_for: (input) =>81 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({})),
84 preview_recording_start: (input) =>83 preview_recording_start: (input) =>
85 invokeTargeted<PreviewAutomationRecordingStatus>("recordingStart", input ?? {}),84 invokeTargeted<PreviewAutomationRecordingStatus>("recordingStart", input ?? {}),
86 preview_recording_stop: (input) =>85 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", () =>...@@ -55,3 +55,20 @@ it("exports provider-compatible object schemas with described parameters", () =>
55 }55 }
56 }56 }
57});57});
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 = [...@@ -29,6 +29,10 @@ const dependencies = [
29 PreviewAutomationBroker.PreviewAutomationBroker,29 PreviewAutomationBroker.PreviewAutomationBroker,
30];30];
3131
32const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate({
33 description: "The preview action completed successfully.",
34});
35
32const browserTool = <T extends Tool.Any>(tool: T): T =>36const browserTool = <T extends Tool.Any>(tool: T): T =>
33 tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T;37 tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T;
3438
...@@ -117,7 +121,7 @@ export const PreviewClickTool = browserTool(...@@ -117,7 +121,7 @@ export const PreviewClickTool = browserTool(
117 description:121 description:
118 "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.",122 "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.",
119 parameters: PreviewAutomationClickInput,123 parameters: PreviewAutomationClickInput,
120 success: Schema.Null,124 success: PreviewActionResult,
121 failure: PreviewAutomationError,125 failure: PreviewAutomationError,
122 dependencies,126 dependencies,
123 }).annotate(Tool.Title, "Click preview page"),127 }).annotate(Tool.Title, "Click preview page"),
...@@ -128,7 +132,7 @@ export const PreviewTypeTool = browserTool(...@@ -128,7 +132,7 @@ export const PreviewTypeTool = browserTool(
128 description:132 description:
129 "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.",133 "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.",
130 parameters: PreviewAutomationTypeInput,134 parameters: PreviewAutomationTypeInput,
131 success: Schema.Null,135 success: PreviewActionResult,
132 failure: PreviewAutomationError,136 failure: PreviewAutomationError,
133 dependencies,137 dependencies,
134 }).annotate(Tool.Title, "Type into preview page"),138 }).annotate(Tool.Title, "Type into preview page"),
...@@ -139,7 +143,7 @@ export const PreviewPressTool = browserTool(...@@ -139,7 +143,7 @@ export const PreviewPressTool = browserTool(
139 description:143 description:
140 "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']}.",144 "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']}.",
141 parameters: PreviewAutomationPressInput,145 parameters: PreviewAutomationPressInput,
142 success: Schema.Null,146 success: PreviewActionResult,
143 failure: PreviewAutomationError,147 failure: PreviewAutomationError,
144 dependencies,148 dependencies,
145 }).annotate(Tool.Title, "Press key in preview page"),149 }).annotate(Tool.Title, "Press key in preview page"),
...@@ -150,7 +154,7 @@ export const PreviewScrollTool = safeBrowserTool(...@@ -150,7 +154,7 @@ export const PreviewScrollTool = safeBrowserTool(
150 description:154 description:
151 "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.",155 "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.",
152 parameters: PreviewAutomationScrollInput,156 parameters: PreviewAutomationScrollInput,
153 success: Schema.Null,157 success: PreviewActionResult,
154 failure: PreviewAutomationError,158 failure: PreviewAutomationError,
155 dependencies,159 dependencies,
156 }).annotate(Tool.Title, "Scroll preview page"),160 }).annotate(Tool.Title, "Scroll preview page"),
...@@ -172,7 +176,7 @@ export const PreviewWaitForTool = readonlyBrowserTool(...@@ -172,7 +176,7 @@ export const PreviewWaitForTool = readonlyBrowserTool(
172 description:176 description:
173 "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.",177 "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.",
174 parameters: PreviewAutomationWaitForInput,178 parameters: PreviewAutomationWaitForInput,
175 success: Schema.Null,179 success: PreviewActionResult,
176 failure: PreviewAutomationError,180 failure: PreviewAutomationError,
177 dependencies,181 dependencies,
178 }).annotate(Tool.Title, "Wait for preview page condition"),182 }).annotate(Tool.Title, "Wait for preview page condition"),