| author | |
| committer | |
| log | 551f4c99c1c42c767daea1f2920cfc8fbc78a1cb |
| tree | d0574f22fc706702e4e76eaeff4fa085732e5d1d |
| parent | 402c9e0748ced0125c1b40793f7d170efb9b6b2a |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 219 | expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false); |
| 220 | 220 | expect(clickTool?.tool.annotations?.destructiveHint).toBe(true); |
| 221 | 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 | }); | |
| 222 | 227 | |
| 223 | 228 | const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate"); |
| 224 | 229 | expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false); |
| ... | ... | @@ -260,15 +265,24 @@ it.effect("registers annotated tools and preserves authenticated request context |
| 260 | 265 | alternateTabId, |
| 261 | 266 | ); |
| 262 | 267 | |
| 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 | } | |
| 272 | 286 | }), |
| 273 | 287 | ).pipe(Effect.provide(TestLayer)), |
| 274 | 288 | ); |
apps/server/src/mcp/toolkits/preview/handlers.ts+5-6| ... | ... | @@ -72,15 +72,14 @@ const handlers = { |
| 72 | 72 | invokeTargeted<PreviewAutomationSetColorSchemeResult>("setColorScheme", input), |
| 73 | 73 | preview_snapshot: (input) => invokeTargeted<PreviewAutomationSnapshot>("snapshot", input ?? {}), |
| 74 | 74 | 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({})), | |
| 80 | 79 | preview_evaluate: (input) => |
| 81 | 80 | invokeTargeted<unknown>("evaluate", input).pipe(Effect.map((result) => result ?? null)), |
| 82 | 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 | 83 | preview_recording_start: (input) => |
| 85 | 84 | invokeTargeted<PreviewAutomationRecordingStatus>("recordingStart", input ?? {}), |
| 86 | 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 | 55 | } |
| 56 | 56 | } |
| 57 | 57 | }); |
| 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+9-5| ... | ... | @@ -29,6 +29,10 @@ const dependencies = [ |
| 29 | 29 | PreviewAutomationBroker.PreviewAutomationBroker, |
| 30 | 30 | ]; |
| 31 | 31 | |
| 32 | const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate({ | |
| 33 | description: "The preview action completed successfully.", | |
| 34 | }); | |
| 35 | ||
| 32 | 36 | const browserTool = <T extends Tool.Any>(tool: T): T => |
| 33 | 37 | tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T; |
| 34 | 38 | |
| ... | ... | @@ -117,7 +121,7 @@ export const PreviewClickTool = browserTool( |
| 117 | 121 | description: |
| 118 | 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 | 123 | parameters: PreviewAutomationClickInput, |
| 120 | success: Schema.Null, | |
| 124 | success: PreviewActionResult, | |
| 121 | 125 | failure: PreviewAutomationError, |
| 122 | 126 | dependencies, |
| 123 | 127 | }).annotate(Tool.Title, "Click preview page"), |
| ... | ... | @@ -128,7 +132,7 @@ export const PreviewTypeTool = browserTool( |
| 128 | 132 | description: |
| 129 | 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 | 134 | parameters: PreviewAutomationTypeInput, |
| 131 | success: Schema.Null, | |
| 135 | success: PreviewActionResult, | |
| 132 | 136 | failure: PreviewAutomationError, |
| 133 | 137 | dependencies, |
| 134 | 138 | }).annotate(Tool.Title, "Type into preview page"), |
| ... | ... | @@ -139,7 +143,7 @@ export const PreviewPressTool = browserTool( |
| 139 | 143 | description: |
| 140 | 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 | 145 | parameters: PreviewAutomationPressInput, |
| 142 | success: Schema.Null, | |
| 146 | success: PreviewActionResult, | |
| 143 | 147 | failure: PreviewAutomationError, |
| 144 | 148 | dependencies, |
| 145 | 149 | }).annotate(Tool.Title, "Press key in preview page"), |
| ... | ... | @@ -150,7 +154,7 @@ export const PreviewScrollTool = safeBrowserTool( |
| 150 | 154 | description: |
| 151 | 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 | 156 | parameters: PreviewAutomationScrollInput, |
| 153 | success: Schema.Null, | |
| 157 | success: PreviewActionResult, | |
| 154 | 158 | failure: PreviewAutomationError, |
| 155 | 159 | dependencies, |
| 156 | 160 | }).annotate(Tool.Title, "Scroll preview page"), |
| ... | ... | @@ -172,7 +176,7 @@ export const PreviewWaitForTool = readonlyBrowserTool( |
| 172 | 176 | description: |
| 173 | 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 | 178 | parameters: PreviewAutomationWaitForInput, |
| 175 | success: Schema.Null, | |
| 179 | success: PreviewActionResult, | |
| 176 | 180 | failure: PreviewAutomationError, |
| 177 | 181 | dependencies, |
| 178 | 182 | }).annotate(Tool.Title, "Wait for preview page condition"), |