@flrande/browserctl 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -57
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -253
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1436
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { ErrorCode } from "./errors";
4
- import { createErr, createOk } from "./envelope";
5
-
6
- describe("createOk", () => {
7
- it("returns a success envelope", () => {
8
- expect(createOk({ value: 42 })).toEqual({
9
- ok: true,
10
- data: { value: 42 }
11
- });
12
- });
13
- });
14
-
15
- describe("createErr", () => {
16
- it("returns an error envelope", () => {
17
- expect(createErr(ErrorCode.E_INVALID_ARG, "bad input")).toEqual({
18
- ok: false,
19
- error: {
20
- code: "E_INVALID_ARG",
21
- message: "bad input"
22
- }
23
- });
24
- });
25
- });
@@ -1,31 +0,0 @@
1
- import type { ErrorCode } from "./errors";
2
-
3
- export type ToolSuccessEnvelope<TData> = {
4
- ok: true;
5
- data: TData;
6
- };
7
-
8
- export type ToolErrorEnvelope = {
9
- ok: false;
10
- error: {
11
- code: ErrorCode;
12
- message: string;
13
- };
14
- };
15
-
16
- export function createOk<TData>(data: TData): ToolSuccessEnvelope<TData> {
17
- return {
18
- ok: true,
19
- data
20
- };
21
- }
22
-
23
- export function createErr(code: ErrorCode, message: string): ToolErrorEnvelope {
24
- return {
25
- ok: false,
26
- error: {
27
- code,
28
- message
29
- }
30
- };
31
- }
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { ErrorCode } from "./errors";
4
-
5
- describe("ErrorCode", () => {
6
- it("contains the canonical error code registry", () => {
7
- expect(ErrorCode).toEqual({
8
- E_INVALID_ARG: "E_INVALID_ARG",
9
- E_NOT_FOUND: "E_NOT_FOUND",
10
- E_CONFLICT: "E_CONFLICT",
11
- E_TIMEOUT: "E_TIMEOUT",
12
- E_DRIVER_UNAVAILABLE: "E_DRIVER_UNAVAILABLE",
13
- E_PERMISSION: "E_PERMISSION",
14
- E_INTERNAL: "E_INTERNAL"
15
- });
16
- });
17
- });
@@ -1,11 +0,0 @@
1
- export const ErrorCode = {
2
- E_INVALID_ARG: "E_INVALID_ARG",
3
- E_NOT_FOUND: "E_NOT_FOUND",
4
- E_CONFLICT: "E_CONFLICT",
5
- E_TIMEOUT: "E_TIMEOUT",
6
- E_DRIVER_UNAVAILABLE: "E_DRIVER_UNAVAILABLE",
7
- E_PERMISSION: "E_PERMISSION",
8
- E_INTERNAL: "E_INTERNAL"
9
- } as const;
10
-
11
- export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
@@ -1,3 +0,0 @@
1
- export * from "./errors";
2
- export * from "./envelope";
3
- export * from "./tools";
@@ -1,3 +0,0 @@
1
- import type { ToolErrorEnvelope, ToolSuccessEnvelope } from "./envelope";
2
-
3
- export type ToolResponse<TData> = ToolSuccessEnvelope<TData> | ToolErrorEnvelope;
@@ -1,3 +0,0 @@
1
- export * from "./tool-map";
2
- export * from "./server";
3
- export * from "./sdk-server";
@@ -1,139 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
- import type { CallToolResult, Implementation } from "@modelcontextprotocol/sdk/types.js";
5
- import { Readable, Writable } from "node:stream";
6
- import * as z from "zod/v4";
7
-
8
- import { ErrorCode } from "../../protocol/src/index";
9
-
10
- import {
11
- createMcpStdioServer,
12
- type McpStdioServer,
13
- type McpToolCallEnvelope
14
- } from "./server";
15
- import { buildToolMap, type ToolMap } from "./tool-map";
16
-
17
- const UNKNOWN_TRACE_ID = "trace:unknown";
18
- const DEFAULT_SERVER_INFO: Implementation = {
19
- name: "browserd",
20
- version: "0.1.0"
21
- };
22
-
23
- export type McpSdkServerOptions = {
24
- toolMap?: ToolMap;
25
- serverInfo?: Implementation;
26
- };
27
-
28
- export type McpSdkStdioTransport = {
29
- input?: Readable;
30
- output?: Writable;
31
- };
32
-
33
- export type McpSdkServer = {
34
- readonly mcpServer: McpServer;
35
- readonly mcpStdioServer: McpStdioServer;
36
- listTools(): string[];
37
- connect(transport: Transport): Promise<void>;
38
- connectStdio(transport?: McpSdkStdioTransport): Promise<void>;
39
- close(): Promise<void>;
40
- };
41
-
42
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
43
- return typeof value === "object" && value !== null && !Array.isArray(value);
44
- }
45
-
46
- function resolveTraceId(rawArgs: unknown): string {
47
- if (!isObjectRecord(rawArgs)) {
48
- return UNKNOWN_TRACE_ID;
49
- }
50
-
51
- const traceId = rawArgs.traceId;
52
- if (typeof traceId !== "string") {
53
- return UNKNOWN_TRACE_ID;
54
- }
55
-
56
- const trimmedTraceId = traceId.trim();
57
- return trimmedTraceId.length > 0 ? trimmedTraceId : UNKNOWN_TRACE_ID;
58
- }
59
-
60
- function stringifyContent(value: unknown): string {
61
- if (typeof value === "string") {
62
- return value;
63
- }
64
-
65
- try {
66
- return JSON.stringify(value);
67
- } catch {
68
- return String(value);
69
- }
70
- }
71
-
72
- function toCallToolResult(envelope: McpToolCallEnvelope): CallToolResult {
73
- if (envelope.ok) {
74
- return {
75
- content: [
76
- {
77
- type: "text",
78
- text: stringifyContent(envelope.data ?? {})
79
- }
80
- ],
81
- structuredContent: envelope
82
- };
83
- }
84
-
85
- const code = envelope.error?.code ?? ErrorCode.E_INTERNAL;
86
- const message = envelope.error?.message ?? "Unexpected tool failure.";
87
- return {
88
- content: [
89
- {
90
- type: "text",
91
- text: `${code}: ${message}`
92
- }
93
- ],
94
- structuredContent: envelope,
95
- isError: true
96
- };
97
- }
98
-
99
- export function createMcpSdkServer(options: McpSdkServerOptions = {}): McpSdkServer {
100
- const toolMap = options.toolMap ?? buildToolMap();
101
- const mcpStdioServer = createMcpStdioServer(toolMap);
102
- const mcpServer = new McpServer(options.serverInfo ?? DEFAULT_SERVER_INFO);
103
-
104
- for (const toolName of mcpStdioServer.listTools()) {
105
- mcpServer.registerTool(
106
- toolName,
107
- {
108
- description: `Browser control tool: ${toolName}`,
109
- inputSchema: z.object({}).passthrough()
110
- },
111
- async (rawArgs) => {
112
- const response = await mcpStdioServer.callTool({
113
- name: toolName,
114
- traceId: resolveTraceId(rawArgs),
115
- arguments: rawArgs
116
- });
117
- return toCallToolResult(response);
118
- }
119
- );
120
- }
121
-
122
- return {
123
- mcpServer,
124
- mcpStdioServer,
125
- listTools() {
126
- return mcpStdioServer.listTools();
127
- },
128
- connect(transport: Transport) {
129
- return mcpServer.connect(transport);
130
- },
131
- connectStdio(transport: McpSdkStdioTransport = {}) {
132
- const stdioTransport = new StdioServerTransport(transport.input, transport.output);
133
- return mcpServer.connect(stdioTransport);
134
- },
135
- close() {
136
- return mcpServer.close();
137
- }
138
- };
139
- }
@@ -1,281 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { ErrorCode, createOk } from "../../protocol/src/index";
6
-
7
- import { createMcpSdkServer } from "./sdk-server";
8
- import { createMcpStdioServer } from "./server";
9
- import { buildToolMap } from "./tool-map";
10
-
11
- describe("tool map", () => {
12
- it("includes browser.status", () => {
13
- const map = buildToolMap();
14
-
15
- expect(map.has("browser.status")).toBe(true);
16
- });
17
- });
18
-
19
- describe("createMcpStdioServer", () => {
20
- it("returns success envelope for a successful handler result", async () => {
21
- const server = createMcpStdioServer(
22
- new Map([
23
- [
24
- "browser.status",
25
- async () =>
26
- createOk({
27
- status: "ok"
28
- })
29
- ]
30
- ])
31
- );
32
-
33
- const response = await server.callTool({
34
- name: "browser.status",
35
- traceId: "trace:test",
36
- arguments: {
37
- sessionId: "session:alpha",
38
- profile: "profile:alpha",
39
- targetId: "target:alpha"
40
- }
41
- });
42
-
43
- expect(response).toEqual({
44
- ok: true,
45
- traceId: "trace:test",
46
- sessionId: "session:alpha",
47
- profile: "profile:alpha",
48
- targetId: "target:alpha",
49
- data: {
50
- status: "ok"
51
- }
52
- });
53
- });
54
-
55
- it("returns validation errors through the common envelope", async () => {
56
- const server = createMcpStdioServer();
57
-
58
- const response = await server.callTool({
59
- name: "browser.status",
60
- traceId: "trace:validation",
61
- arguments: {
62
- sessionId: ""
63
- }
64
- });
65
-
66
- expect(response).toEqual({
67
- ok: false,
68
- traceId: "trace:validation",
69
- sessionId: "session:unknown",
70
- error: {
71
- code: ErrorCode.E_INVALID_ARG,
72
- message: "sessionId is required and must be a non-empty string."
73
- }
74
- });
75
- });
76
-
77
- it("returns not found for unknown tools", async () => {
78
- const server = createMcpStdioServer();
79
-
80
- const response = await server.callTool({
81
- name: "browser.unknown",
82
- traceId: "trace:unknown",
83
- arguments: {
84
- sessionId: "session:alpha"
85
- }
86
- });
87
-
88
- expect(response).toEqual({
89
- ok: false,
90
- traceId: "trace:unknown",
91
- sessionId: "session:alpha",
92
- error: {
93
- code: ErrorCode.E_NOT_FOUND,
94
- message: "Unknown tool: browser.unknown"
95
- }
96
- });
97
- });
98
-
99
- it("returns a dedicated code for not implemented tools", async () => {
100
- const server = createMcpStdioServer();
101
-
102
- const response = await server.callTool({
103
- name: "browser.profile.list",
104
- traceId: "trace:not-implemented",
105
- arguments: {
106
- sessionId: "session:alpha"
107
- }
108
- });
109
-
110
- expect(response).toEqual({
111
- ok: false,
112
- traceId: "trace:not-implemented",
113
- sessionId: "session:alpha",
114
- error: {
115
- code: ErrorCode.E_DRIVER_UNAVAILABLE,
116
- message: "Tool is not implemented: browser.profile.list"
117
- }
118
- });
119
- });
120
-
121
- it("wraps thrown handler errors as E_INTERNAL envelopes", async () => {
122
- const server = createMcpStdioServer(
123
- new Map([
124
- [
125
- "browser.status",
126
- async () => {
127
- throw new Error("boom");
128
- }
129
- ]
130
- ])
131
- );
132
-
133
- const response = await server.callTool({
134
- name: "browser.status",
135
- traceId: "trace:throw",
136
- arguments: {
137
- sessionId: "session:alpha"
138
- }
139
- });
140
-
141
- expect(response).toEqual({
142
- ok: false,
143
- traceId: "trace:throw",
144
- sessionId: "session:alpha",
145
- error: {
146
- code: ErrorCode.E_INTERNAL,
147
- message: "boom"
148
- }
149
- });
150
- });
151
-
152
- it("uses the provided toolMap override instead of defaults", async () => {
153
- const server = createMcpStdioServer(
154
- new Map([
155
- [
156
- "custom.ping",
157
- async () =>
158
- createOk({
159
- pong: true
160
- })
161
- ]
162
- ])
163
- );
164
-
165
- expect(server.listTools()).toEqual(["custom.ping"]);
166
-
167
- const response = await server.callTool({
168
- name: "custom.ping",
169
- traceId: "trace:override",
170
- arguments: {
171
- sessionId: "session:alpha"
172
- }
173
- });
174
-
175
- expect(response).toEqual({
176
- ok: true,
177
- traceId: "trace:override",
178
- sessionId: "session:alpha",
179
- data: {
180
- pong: true
181
- }
182
- });
183
- });
184
- });
185
-
186
- describe("createMcpSdkServer", () => {
187
- it("registers tool names and returns structured envelopes through MCP", async () => {
188
- const sdkServer = createMcpSdkServer({
189
- toolMap: new Map([
190
- [
191
- "browser.status",
192
- async (args) =>
193
- createOk({
194
- status: "ok",
195
- sessionId: args.sessionId
196
- })
197
- ]
198
- ]),
199
- serverInfo: {
200
- name: "browserctl-test-server",
201
- version: "0.0.0-test"
202
- }
203
- });
204
-
205
- const client = new Client({
206
- name: "browserctl-test-client",
207
- version: "0.0.0-test"
208
- });
209
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
210
- await Promise.all([sdkServer.connect(serverTransport), client.connect(clientTransport)]);
211
-
212
- try {
213
- const toolsResult = await client.listTools();
214
- expect(toolsResult.tools.map((tool) => tool.name)).toEqual(["browser.status"]);
215
-
216
- const result = await client.callTool({
217
- name: "browser.status",
218
- arguments: {
219
- sessionId: "session:alpha",
220
- traceId: "trace:alpha"
221
- }
222
- });
223
-
224
- expect("structuredContent" in result).toBe(true);
225
- if (!("structuredContent" in result)) {
226
- throw new Error("Expected a CallToolResult payload with structuredContent.");
227
- }
228
-
229
- expect(result.isError).not.toBe(true);
230
- expect(result.structuredContent).toMatchObject({
231
- ok: true,
232
- traceId: "trace:alpha",
233
- sessionId: "session:alpha",
234
- data: {
235
- status: "ok",
236
- sessionId: "session:alpha"
237
- }
238
- });
239
- } finally {
240
- await client.close();
241
- await sdkServer.close();
242
- }
243
- });
244
-
245
- it("maps envelope validation errors to MCP tool errors", async () => {
246
- const sdkServer = createMcpSdkServer();
247
- const client = new Client({
248
- name: "browserctl-test-client",
249
- version: "0.0.0-test"
250
- });
251
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
252
- await Promise.all([sdkServer.connect(serverTransport), client.connect(clientTransport)]);
253
-
254
- try {
255
- const result = await client.callTool({
256
- name: "browser.status",
257
- arguments: {
258
- sessionId: ""
259
- }
260
- });
261
-
262
- expect("structuredContent" in result).toBe(true);
263
- if (!("structuredContent" in result)) {
264
- throw new Error("Expected a CallToolResult payload with structuredContent.");
265
- }
266
-
267
- expect(result.isError).toBe(true);
268
- expect(result.structuredContent).toMatchObject({
269
- ok: false,
270
- sessionId: "session:unknown",
271
- error: {
272
- code: ErrorCode.E_INVALID_ARG,
273
- message: "sessionId is required and must be a non-empty string."
274
- }
275
- });
276
- } finally {
277
- await client.close();
278
- await sdkServer.close();
279
- }
280
- });
281
- });
@@ -1,183 +0,0 @@
1
- import { ErrorCode, createErr, createOk, type ToolResponse } from "../../protocol/src/index";
2
-
3
- import { buildToolMap, type ToolCallArgs, type ToolMap } from "./tool-map";
4
-
5
- const UNKNOWN_TRACE_ID = "trace:unknown";
6
- const UNKNOWN_SESSION_ID = "session:unknown";
7
-
8
- export type McpToolCallRequest = {
9
- name: string;
10
- arguments?: unknown;
11
- traceId?: string;
12
- };
13
-
14
- export type McpToolCallEnvelope<TData = Record<string, unknown>> = {
15
- ok: boolean;
16
- traceId: string;
17
- sessionId: string;
18
- profile?: string;
19
- targetId?: string;
20
- data?: TData;
21
- error?: {
22
- code: string;
23
- message: string;
24
- };
25
- };
26
-
27
- export type McpStdioServer = {
28
- readonly toolMap: ToolMap;
29
- listTools(): string[];
30
- callTool(request: McpToolCallRequest): Promise<McpToolCallEnvelope>;
31
- };
32
-
33
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
34
- return typeof value === "object" && value !== null && !Array.isArray(value);
35
- }
36
-
37
- function resolveTraceId(value: string | undefined): string {
38
- if (typeof value !== "string") {
39
- return UNKNOWN_TRACE_ID;
40
- }
41
-
42
- const trimmedValue = value.trim();
43
- return trimmedValue.length === 0 ? UNKNOWN_TRACE_ID : trimmedValue;
44
- }
45
-
46
- function toErrorMessage(error: unknown): string {
47
- if (error instanceof Error) {
48
- return error.message;
49
- }
50
-
51
- return "Unexpected tool failure.";
52
- }
53
-
54
- export function validateToolCallArgs(value: unknown): ToolResponse<ToolCallArgs> {
55
- if (!isObjectRecord(value)) {
56
- return createErr(ErrorCode.E_INVALID_ARG, "Tool arguments must be an object.");
57
- }
58
-
59
- const sessionIdValue = value.sessionId;
60
- if (typeof sessionIdValue !== "string" || sessionIdValue.trim().length === 0) {
61
- return createErr(ErrorCode.E_INVALID_ARG, "sessionId is required and must be a non-empty string.");
62
- }
63
-
64
- const profileValue = value.profile;
65
- if (profileValue !== undefined && (typeof profileValue !== "string" || profileValue.trim().length === 0)) {
66
- return createErr(ErrorCode.E_INVALID_ARG, "profile must be a non-empty string when provided.");
67
- }
68
-
69
- const targetIdValue = value.targetId;
70
- if (targetIdValue !== undefined && (typeof targetIdValue !== "string" || targetIdValue.trim().length === 0)) {
71
- return createErr(ErrorCode.E_INVALID_ARG, "targetId must be a non-empty string when provided.");
72
- }
73
-
74
- const timeoutMsValue = value.timeoutMs;
75
- if (
76
- timeoutMsValue !== undefined &&
77
- (typeof timeoutMsValue !== "number" || !Number.isFinite(timeoutMsValue) || timeoutMsValue < 0)
78
- ) {
79
- return createErr(
80
- ErrorCode.E_INVALID_ARG,
81
- "timeoutMs must be a non-negative finite number when provided."
82
- );
83
- }
84
-
85
- const normalizedArgs: ToolCallArgs = {
86
- ...value,
87
- sessionId: sessionIdValue.trim()
88
- };
89
-
90
- if (typeof profileValue === "string") {
91
- normalizedArgs.profile = profileValue.trim();
92
- }
93
-
94
- if (typeof targetIdValue === "string") {
95
- normalizedArgs.targetId = targetIdValue.trim();
96
- }
97
-
98
- if (typeof timeoutMsValue === "number") {
99
- normalizedArgs.timeoutMs = timeoutMsValue;
100
- }
101
-
102
- return createOk(normalizedArgs);
103
- }
104
-
105
- export function adaptToolResponse<TData>(
106
- context: Pick<ToolCallArgs, "sessionId" | "profile" | "targetId">,
107
- traceId: string,
108
- response: ToolResponse<TData>
109
- ): McpToolCallEnvelope<TData> {
110
- if (response.ok) {
111
- return {
112
- ok: true,
113
- traceId,
114
- sessionId: context.sessionId,
115
- profile: context.profile,
116
- targetId: context.targetId,
117
- data: response.data
118
- };
119
- }
120
-
121
- return {
122
- ok: false,
123
- traceId,
124
- sessionId: context.sessionId,
125
- profile: context.profile,
126
- targetId: context.targetId,
127
- error: response.error
128
- };
129
- }
130
-
131
- export function createMcpStdioServer(toolMap: ToolMap = buildToolMap()): McpStdioServer {
132
- return {
133
- toolMap,
134
- listTools() {
135
- return [...toolMap.keys()];
136
- },
137
- async callTool(request) {
138
- const traceId = resolveTraceId(request.traceId);
139
- const rawToolName = request.name;
140
- if (typeof rawToolName !== "string" || rawToolName.trim().length === 0) {
141
- return adaptToolResponse(
142
- {
143
- sessionId: UNKNOWN_SESSION_ID
144
- },
145
- traceId,
146
- createErr(ErrorCode.E_INVALID_ARG, "Tool name must be a non-empty string.")
147
- );
148
- }
149
-
150
- const validatedArgs = validateToolCallArgs(request.arguments);
151
- if (!validatedArgs.ok) {
152
- return adaptToolResponse(
153
- {
154
- sessionId: UNKNOWN_SESSION_ID
155
- },
156
- traceId,
157
- validatedArgs
158
- );
159
- }
160
-
161
- const toolName = rawToolName.trim();
162
- const handler = toolMap.get(toolName);
163
- if (handler === undefined) {
164
- return adaptToolResponse(
165
- validatedArgs.data,
166
- traceId,
167
- createErr(ErrorCode.E_NOT_FOUND, `Unknown tool: ${toolName}`)
168
- );
169
- }
170
-
171
- try {
172
- const response = await handler(validatedArgs.data);
173
- return adaptToolResponse(validatedArgs.data, traceId, response);
174
- } catch (error) {
175
- return adaptToolResponse(
176
- validatedArgs.data,
177
- traceId,
178
- createErr(ErrorCode.E_INTERNAL, toErrorMessage(error))
179
- );
180
- }
181
- }
182
- };
183
- }