@opencode/browse 0.0.6-dev.17 → 0.0.6-dev.19
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.
- package/dist/{cli-b4sbkefj.mjs → cli-22fe7etn.mjs} +771 -644
- package/dist/cli.mjs +9 -9
- package/dist/{relay-helpers-pjcbxs7p.mjs → helpers-rta4d2as.mjs} +1 -1
- package/dist/{main-4p2p9eey.mjs → main-2wrztakb.mjs} +4 -4
- package/dist/main-75rdz6bc.mjs +130 -0
- package/dist/{main-3qpkwaa7.mjs → main-8a245rm1.mjs} +24 -22
- package/dist/{main-e4stqtf2.mjs → main-90enpn5q.mjs} +1622 -1477
- package/dist/{main-kvmxefp1.mjs → main-knahxbey.mjs} +4 -4
- package/dist/main-ts9jjqs8.mjs +4 -0
- package/dist/{main-zwe0c783.mjs → main-xatp12cr.mjs} +10 -22
- package/dist/{main-g5mke6rn.mjs → main-yd50d95g.mjs} +120 -118
- package/dist/{mcp-2smdsn8z.mjs → mcp-p0mcjvq9.mjs} +584 -481
- package/dist/{relay-ns525pms.mjs → relay-tfvn856e.mjs} +15746 -15309
- package/dist/{runtime-update-0mddkfy5.mjs → runtime-update-pxbj49tf.mjs} +7 -8
- package/dist/{setup-f2qjnvre.mjs → setup-y8pd11b8.mjs} +813 -707
- package/dist/{update-cyy1grjj.mjs → update-5fx8t344.mjs} +2 -2
- package/dist/{version-wrebjhy7.mjs → version-cjhq7cm9.mjs} +1 -1
- package/package.json +1 -2
- package/dist/main-4bht27ry.mjs +0 -72
- package/dist/main-y7c1atbf.mjs +0 -4
|
@@ -1,288 +1,316 @@
|
|
|
1
1
|
import {
|
|
2
2
|
autoUpdateStatus
|
|
3
|
-
} from "./main-
|
|
3
|
+
} from "./main-yd50d95g.mjs";
|
|
4
4
|
import {
|
|
5
|
-
parseTargetSelection2,
|
|
6
5
|
getObject2,
|
|
7
|
-
getString2,
|
|
8
6
|
Service,
|
|
9
7
|
layerFetchMcp,
|
|
10
8
|
relayBuildProblem,
|
|
11
9
|
ensureRelay,
|
|
12
10
|
ensureExtensionConnected
|
|
13
|
-
} from "./main-
|
|
11
|
+
} from "./main-8a245rm1.mjs";
|
|
12
|
+
import"./main-75rdz6bc.mjs";
|
|
14
13
|
import {
|
|
15
14
|
opencodeBrowserVersion2
|
|
16
|
-
} from "./main-
|
|
17
|
-
import {
|
|
18
|
-
listSecrets,
|
|
19
|
-
readClient,
|
|
20
|
-
settingKeys,
|
|
21
|
-
settingSpecs,
|
|
22
|
-
securityVariables
|
|
23
|
-
} from "./main-e4stqtf2.mjs";
|
|
15
|
+
} from "./main-ts9jjqs8.mjs";
|
|
24
16
|
import {
|
|
25
17
|
updateJson2,
|
|
26
18
|
createUpdateWatcher2
|
|
27
|
-
} from "./main-
|
|
28
|
-
import
|
|
19
|
+
} from "./main-2wrztakb.mjs";
|
|
20
|
+
import {
|
|
21
|
+
settingKeys,
|
|
22
|
+
settingSpecs,
|
|
23
|
+
securityVariables,
|
|
24
|
+
browsersNote2,
|
|
25
|
+
mcpUpdateNote2,
|
|
26
|
+
objectSchema,
|
|
27
|
+
emptyInputSchema,
|
|
28
|
+
sessionSchemaProperty,
|
|
29
|
+
sessionAliasSchemaProperty,
|
|
30
|
+
sessionOnlyInputSchema,
|
|
31
|
+
ensureSessionExists,
|
|
32
|
+
parseExecuteArguments,
|
|
33
|
+
parseAdoptArguments,
|
|
34
|
+
requiredStringField,
|
|
35
|
+
optionalStringField,
|
|
36
|
+
optionalBooleanField,
|
|
37
|
+
optionalPositiveIntegerField,
|
|
38
|
+
optionalStringArrayField,
|
|
39
|
+
requireObject,
|
|
40
|
+
toolResultForValue2,
|
|
41
|
+
mcpErrorMessage2,
|
|
42
|
+
toolResult,
|
|
43
|
+
readClient,
|
|
44
|
+
listSecrets
|
|
45
|
+
} from "./main-90enpn5q.mjs";
|
|
29
46
|
|
|
30
47
|
// src/mcp.ts
|
|
31
48
|
import { NodeStdio } from "@effect/platform-node";
|
|
32
|
-
import { Config, Context, Effect, Layer, Option,
|
|
49
|
+
import { Config, Context, Effect as Effect7, Layer, Option, Schedule } from "effect";
|
|
33
50
|
import { McpProtocol, McpSchema, McpServer } from "effect/ai";
|
|
51
|
+
import path5 from "node:path";
|
|
52
|
+
import { fileURLToPath } from "node:url";
|
|
53
|
+
|
|
54
|
+
// src/mcp/browser.ts
|
|
55
|
+
import { Effect } from "effect";
|
|
34
56
|
import fs from "node:fs/promises";
|
|
35
57
|
import path from "node:path";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
|
|
59
|
+
// src/mcp/registry.ts
|
|
60
|
+
var groups = [];
|
|
61
|
+
function registerMcpTools(options) {
|
|
62
|
+
groups.push({
|
|
63
|
+
order: options.order,
|
|
64
|
+
build: options.build
|
|
65
|
+
});
|
|
43
66
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
description: "Optional session id. Defaults to this MCP server's current session."
|
|
48
|
-
};
|
|
49
|
-
var sessionAliasSchemaProperty = { type: "string", description: "Same as id." };
|
|
50
|
-
var sessionOnlyInputSchema = objectSchema({
|
|
51
|
-
session: sessionSchemaProperty
|
|
52
|
-
});
|
|
53
|
-
function makeToolSpecs(relay, currentSession, update = () => {
|
|
54
|
-
return;
|
|
55
|
-
}, browsers = []) {
|
|
56
|
-
const browserNote = browsersNote(browsers);
|
|
67
|
+
function createMcpToolContext(options) {
|
|
68
|
+
const browsers = options.browsers ?? [];
|
|
69
|
+
const browserNote = browsersNote2(browsers);
|
|
57
70
|
const browserProperty = {
|
|
58
71
|
type: "string",
|
|
59
72
|
description: `Optional browser id for a new session's tabs; omit for the default browser. A session stays in its browser.${browserNote}`
|
|
60
73
|
};
|
|
61
|
-
const resolveSessionId = (input, field = "session") => optionalStringField(input, field) ?? (field === "id" ? optionalStringField(input, "session") : undefined) ?? currentSession.id;
|
|
74
|
+
const resolveSessionId = (input, field = "session") => optionalStringField(input, field) ?? (field === "id" ? optionalStringField(input, "session") : undefined) ?? options.currentSession.id;
|
|
62
75
|
const establishCurrentSession = (id) => {
|
|
63
|
-
currentSession.id = id;
|
|
64
|
-
currentSession.established = true;
|
|
76
|
+
options.currentSession.id = id;
|
|
77
|
+
options.currentSession.established = true;
|
|
65
78
|
};
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
createIfMissing: !args.session,
|
|
89
|
-
...args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs },
|
|
90
|
-
...args.targetSelection ? { targetSelection: args.targetSelection } : {},
|
|
91
|
-
...args.browser ? { browser: args.browser } : {}
|
|
92
|
-
});
|
|
93
|
-
const recreated = !args.session && currentSession.established && result.session.created === true;
|
|
94
|
-
establishCurrentSession(sessionId);
|
|
95
|
-
return {
|
|
96
|
-
...result,
|
|
97
|
-
...recreated ? { notice: `Recreated session '${sessionId}' — relay had no such session; page and state were reset.` } : {}
|
|
98
|
-
};
|
|
99
|
-
})
|
|
100
|
-
},
|
|
79
|
+
return {
|
|
80
|
+
relay: options.relay,
|
|
81
|
+
currentSession: options.currentSession,
|
|
82
|
+
packageRoot: options.packageRoot,
|
|
83
|
+
update: options.update ?? (() => {
|
|
84
|
+
return;
|
|
85
|
+
}),
|
|
86
|
+
browsers,
|
|
87
|
+
browserNote,
|
|
88
|
+
browserProperty,
|
|
89
|
+
resolveSessionId,
|
|
90
|
+
establishCurrentSession
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildRegisteredMcpTools(ctx) {
|
|
94
|
+
return [...groups].sort((left, right) => left.order - right.order).flatMap((group) => group.build(ctx));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/mcp/browser.ts
|
|
98
|
+
registerMcpTools({
|
|
99
|
+
order: 110,
|
|
100
|
+
build: (ctx) => [
|
|
101
101
|
{
|
|
102
102
|
name: "status",
|
|
103
|
+
requiresRelayCompatibility: false,
|
|
103
104
|
description: "Return relay, extension, target, and session status.",
|
|
104
105
|
inputSchema: emptyInputSchema,
|
|
105
106
|
readOnly: true,
|
|
106
107
|
destructive: false,
|
|
107
108
|
idempotent: false,
|
|
108
109
|
handle: () => Effect.gen(function* () {
|
|
109
|
-
const [version, status] = yield* Effect.all([relay.version, relay.extensionStatus]);
|
|
110
|
+
const [version, status] = yield* Effect.all([ctx.relay.version, ctx.relay.extensionStatus]);
|
|
110
111
|
const buildProblem = relayBuildProblem(version);
|
|
111
|
-
const available = updateJson2(update());
|
|
112
|
-
return {
|
|
112
|
+
const available = updateJson2(ctx.update());
|
|
113
|
+
return {
|
|
114
|
+
endpoint: ctx.relay.endpoint,
|
|
115
|
+
currentSession: ctx.currentSession.id,
|
|
116
|
+
version,
|
|
117
|
+
status,
|
|
118
|
+
...buildProblem ? { buildProblem } : {},
|
|
119
|
+
...available ? { update: available } : {},
|
|
120
|
+
autoUpdate: autoUpdateStatus()
|
|
121
|
+
};
|
|
113
122
|
})
|
|
114
|
-
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
registerMcpTools({
|
|
127
|
+
order: 420,
|
|
128
|
+
build: (ctx) => [
|
|
115
129
|
{
|
|
116
|
-
name: "
|
|
117
|
-
description:
|
|
130
|
+
name: "tabs_cleanup",
|
|
131
|
+
description: "Close tabs agents opened that nobody has used for a while (no agent command and not visited by the user). Tabs the user shared or pinned, the active tab, tabs playing audio, recording, or waiting for a handoff stay open. Call it when you finish a task to leave the user's browser tidy; Browse also does this automatically after the user's idle setting (10 minutes unless they changed it).",
|
|
118
132
|
inputSchema: objectSchema({
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
idleMinutes: {
|
|
134
|
+
type: "integer",
|
|
135
|
+
minimum: 0,
|
|
136
|
+
description: "Close tabs idle at least this long. 0 closes every eligible agent tab. Defaults to the user's setting (10 minutes; still 10 when the user set automatic cleanup to never)."
|
|
137
|
+
}
|
|
123
138
|
}),
|
|
124
139
|
readOnly: false,
|
|
125
|
-
destructive:
|
|
126
|
-
idempotent:
|
|
127
|
-
handle: (input) =>
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const browser = optionalStringField(input, "browser");
|
|
132
|
-
const session = yield* relay.sessionNew(requestedId, {
|
|
133
|
-
...readOnly ? { readOnly: true } : {},
|
|
134
|
-
...allowedOrigins ? { allowedOrigins } : {},
|
|
135
|
-
...browser ? { browser } : {}
|
|
136
|
-
});
|
|
137
|
-
establishCurrentSession(session.id);
|
|
138
|
-
return { session };
|
|
139
|
-
})
|
|
140
|
+
destructive: true,
|
|
141
|
+
idempotent: true,
|
|
142
|
+
handle: (input) => {
|
|
143
|
+
const value = typeof input === "object" && input !== null && "idleMinutes" in input ? input.idleMinutes : undefined;
|
|
144
|
+
return ctx.relay.tabsCleanup(typeof value === "number" && value >= 0 ? value : undefined);
|
|
145
|
+
}
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
|
-
name: "
|
|
143
|
-
description:
|
|
148
|
+
name: "settings",
|
|
149
|
+
description: `Read or change the user's Browse settings, the same ones as the extension's options page: where agent tabs open (window), how they're grouped (grouping), when untouched agent tabs close (idleMinutes), the agent cursor (cursor), human-like input (humanInput) and handoff notifications (handoffNotifications). Use it only when the user asks to change how browse behaves, never on your own initiative. Without \`set\` it returns each setting's stored value, the value in effect, and the relay environment variable that overrides it, if any. With \`set\` it validates every value strictly, applies the change at once (the options page updates live) and returns what changed: tell the user what you changed. A setting an environment variable fixes is not changed, and the result says so. Security switches (${securityVariables.join(", ")}) are environment-only and can never be read or changed here.`,
|
|
144
150
|
inputSchema: objectSchema({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
set: {
|
|
152
|
+
type: "object",
|
|
153
|
+
description: "Settings to change; omit to only read them.",
|
|
154
|
+
properties: Object.fromEntries(settingKeys.map((key) => [key, { enum: [...settingSpecs[key].values], description: settingSpecs[key].label }])),
|
|
155
|
+
additionalProperties: false
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
148
158
|
readOnly: false,
|
|
149
159
|
destructive: false,
|
|
150
160
|
idempotent: true,
|
|
151
|
-
handle: (input) =>
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
readOnly: true,
|
|
165
|
-
destructive: false,
|
|
166
|
-
idempotent: false,
|
|
167
|
-
handle: () => relay.sessions.pipe(Effect.map((sessions) => ({ sessions })))
|
|
168
|
-
},
|
|
161
|
+
handle: (input) => {
|
|
162
|
+
const set = getObject2(input)?.set;
|
|
163
|
+
if (set === undefined)
|
|
164
|
+
return ctx.relay.settingsGet;
|
|
165
|
+
const changes = getObject2(set);
|
|
166
|
+
return changes ? ctx.relay.settingsSet(changes) : Effect.fail(new Error('set must be an object, for example { "grouping": "shared" }'));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
registerMcpTools({
|
|
172
|
+
order: 700,
|
|
173
|
+
build: (ctx) => [
|
|
169
174
|
{
|
|
170
|
-
name: "
|
|
171
|
-
|
|
175
|
+
name: "skill",
|
|
176
|
+
requiresRelayCompatibility: false,
|
|
177
|
+
description: "Return the Browse agent skill instructions.",
|
|
172
178
|
inputSchema: emptyInputSchema,
|
|
173
179
|
readOnly: true,
|
|
174
180
|
destructive: false,
|
|
175
181
|
idempotent: true,
|
|
176
|
-
handle: () => Effect.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
name: "session_use",
|
|
180
|
-
description: "Set this MCP server's current Browse session id.",
|
|
181
|
-
inputSchema: objectSchema({
|
|
182
|
-
id: { type: "string", description: "Existing Browse session id." }
|
|
183
|
-
}, ["id"]),
|
|
184
|
-
readOnly: false,
|
|
185
|
-
destructive: false,
|
|
186
|
-
idempotent: true,
|
|
187
|
-
handle: (input) => Effect.gen(function* () {
|
|
188
|
-
const id = yield* Effect.try(() => requiredStringField(input, "id"));
|
|
189
|
-
yield* ensureSessionExists(relay, id);
|
|
190
|
-
establishCurrentSession(id);
|
|
191
|
-
return { currentSession: currentSession.id };
|
|
192
|
-
})
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: "session_reset",
|
|
196
|
-
description: "Reset a Browse session's state and page.",
|
|
197
|
-
inputSchema: objectSchema({
|
|
198
|
-
id: sessionSchemaProperty,
|
|
199
|
-
session: sessionAliasSchemaProperty
|
|
200
|
-
}),
|
|
201
|
-
readOnly: false,
|
|
202
|
-
destructive: true,
|
|
203
|
-
idempotent: false,
|
|
204
|
-
handle: (input) => Effect.gen(function* () {
|
|
205
|
-
const id = resolveSessionId(input, "id");
|
|
206
|
-
const reset = yield* relay.sessionReset(id);
|
|
207
|
-
establishCurrentSession(id);
|
|
208
|
-
return reset;
|
|
182
|
+
handle: () => Effect.tryPromise({
|
|
183
|
+
try: () => fs.readFile(path.join(ctx.packageRoot, "skills", "browse", "SKILL.md"), "utf8"),
|
|
184
|
+
catch: (cause) => new Error("read opencode-browser skill", { cause })
|
|
209
185
|
})
|
|
210
|
-
}
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/mcp/execute.ts
|
|
191
|
+
import { Effect as Effect2 } from "effect";
|
|
192
|
+
registerMcpTools({
|
|
193
|
+
order: 100,
|
|
194
|
+
build: (ctx) => [
|
|
211
195
|
{
|
|
212
|
-
name: "
|
|
213
|
-
|
|
196
|
+
name: "execute",
|
|
197
|
+
listsBrowsers: true,
|
|
198
|
+
description: `Execute Playwright JavaScript against the Browse session. Code runs in a sandbox on the relay without process, require, import(), fs, child_process, http/https or Node fetch; save and read files with the files helper inside the execute file roots, and cookie access is limited to the session's pages. The result includes console logs, warnings, a bounded execution-context diagnostic when relevant, and an aftermath summary (URL movement, navigations, error counts, handoffs, and a bot challenge or block page when one appears: hand interactive ones to the user with handoff(), never solve or retry-loop them).${ctx.browserNote}`,
|
|
214
199
|
inputSchema: objectSchema({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
200
|
+
code: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: `JavaScript code to execute. It receives browser, context, page, state, modules (path, url, util, crypto, buffer, zlib, events, stream), files (dir, path, write, read), fillInput, fillInputs, snapshot(options?) for compact semantic outlines, search, explicit diffs, or automatic deltas, persistent compatible ref(id) locators, webmcp (list/call), screenshot helpers, ariaSnapshot, ghostCursor, handoff, demonstrate, network capture, pageConsole (start/stop capturing the page's own console messages and errors, off by default because pages can detect it), secrets.type(locator, name) to type a stored site secret or a granted Deadbolt login ("deadbolt:<site>[/<username>]", { field: "username" }, or a password/code with { submit: true }; see secrets_list) only on its allowed origins, without seeing it, secrets.signIn({ site?, username? }) for a whole sign-in (same as the signin tool), and secrets.usePasskey({ site, username }) to pick the passkey account for the tab's next passkey request.`
|
|
203
|
+
},
|
|
204
|
+
session: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description: "Optional existing Browse session id. Explicit ids must already exist; omit this field to use the MCP server's current session, which is created when needed."
|
|
207
|
+
},
|
|
208
|
+
targetUrl: {
|
|
209
|
+
type: "string",
|
|
210
|
+
description: "Optional URL substring selecting an existing attached page. This does not navigate or open a URL; use page.goto() for that."
|
|
211
|
+
},
|
|
212
|
+
targetIndex: {
|
|
213
|
+
type: "integer",
|
|
214
|
+
minimum: 0,
|
|
215
|
+
description: "Optional zero-based attached page index selector."
|
|
216
|
+
},
|
|
217
|
+
browser: ctx.browserProperty,
|
|
218
|
+
timeoutMs: {
|
|
219
|
+
type: "integer",
|
|
220
|
+
minimum: 0,
|
|
221
|
+
description: "Optional overall script timeout in milliseconds (paused while handoff() waits on the user); defaults to 60000, 0 disables."
|
|
227
222
|
}
|
|
228
|
-
|
|
229
|
-
})
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: "session_adopt",
|
|
233
|
-
description: "Make an attached tab the Browse session's default page for subsequent bare execute calls. Omit targetUrl and targetIndex when only one user-attached tab is available.",
|
|
234
|
-
inputSchema: objectSchema({
|
|
235
|
-
session: { type: "string", description: "Optional existing Browse session id. Explicit ids must already exist; omit this field to use the MCP server's current session, which is created when needed." },
|
|
236
|
-
targetUrl: { type: "string", description: "Adopt an existing attached page whose URL contains this text. Omit when only one user-attached tab is available. This does not navigate or open a URL." },
|
|
237
|
-
targetIndex: { type: "integer", minimum: 0, description: "Adopt the attached page at this zero-based target index." },
|
|
238
|
-
tabId: { type: "integer", description: "Adopt the user's tab with this Chrome tab id (the tab= value in status)." }
|
|
239
|
-
}),
|
|
223
|
+
}, ["code"]),
|
|
240
224
|
readOnly: false,
|
|
241
|
-
destructive:
|
|
225
|
+
destructive: true,
|
|
242
226
|
idempotent: false,
|
|
243
|
-
handle: (input) =>
|
|
244
|
-
const args = yield*
|
|
245
|
-
const sessionId = args.session ?? currentSession.id;
|
|
246
|
-
|
|
227
|
+
handle: (input) => Effect2.gen(function* () {
|
|
228
|
+
const args = yield* Effect2.try(() => parseExecuteArguments(input));
|
|
229
|
+
const sessionId = args.session ?? ctx.currentSession.id;
|
|
230
|
+
yield* ensureExtensionConnected({ relay: ctx.relay, sessionId, waitForReconnect: true });
|
|
231
|
+
const result = yield* ctx.relay.execute({
|
|
247
232
|
sessionId,
|
|
233
|
+
code: args.code,
|
|
248
234
|
createIfMissing: !args.session,
|
|
235
|
+
...args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs },
|
|
249
236
|
...args.targetSelection ? { targetSelection: args.targetSelection } : {},
|
|
250
|
-
...args.
|
|
237
|
+
...args.browser ? { browser: args.browser } : {}
|
|
251
238
|
});
|
|
252
|
-
|
|
253
|
-
|
|
239
|
+
const recreated = !args.session && ctx.currentSession.established && result.session.created === true;
|
|
240
|
+
ctx.establishCurrentSession(sessionId);
|
|
241
|
+
return {
|
|
242
|
+
...result,
|
|
243
|
+
...recreated ? { notice: `Recreated session '${sessionId}' — relay had no such session; page and state were reset.` } : {}
|
|
244
|
+
};
|
|
254
245
|
})
|
|
255
|
-
}
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// src/mcp/network.ts
|
|
251
|
+
import { Effect as Effect3 } from "effect";
|
|
252
|
+
import path2 from "node:path";
|
|
253
|
+
registerMcpTools({
|
|
254
|
+
order: 300,
|
|
255
|
+
build: (ctx) => [
|
|
256
256
|
{
|
|
257
257
|
name: "network_start",
|
|
258
258
|
description: "Start session-scoped network capture. Browse records normalized Playwright exchanges; HAR is only the optional export format. Bodies are embedded by default with per-body and total memory limits.",
|
|
259
259
|
inputSchema: objectSchema({
|
|
260
|
-
session: {
|
|
260
|
+
session: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Optional existing session id. Omit to use or create this MCP server's current session."
|
|
263
|
+
},
|
|
261
264
|
urlFilter: { type: "string", description: "Capture only request URLs containing this text." },
|
|
262
|
-
resourceTypes: {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
resourceTypes: {
|
|
266
|
+
type: "array",
|
|
267
|
+
items: { type: "string" },
|
|
268
|
+
description: "Optional Playwright resource types such as fetch and xhr."
|
|
269
|
+
},
|
|
270
|
+
content: {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["embed", "omit"],
|
|
273
|
+
description: "Request and response body mode. Defaults to embed."
|
|
274
|
+
},
|
|
275
|
+
maxBodyBytes: {
|
|
276
|
+
type: "integer",
|
|
277
|
+
minimum: 1,
|
|
278
|
+
description: "Maximum bytes captured from each body. Defaults to 1000000."
|
|
279
|
+
},
|
|
280
|
+
maxTotalBodyBytes: {
|
|
281
|
+
type: "integer",
|
|
282
|
+
minimum: 1,
|
|
283
|
+
description: "Maximum body bytes retained for the capture. Defaults to 25000000."
|
|
284
|
+
},
|
|
285
|
+
maxEntries: {
|
|
286
|
+
type: "integer",
|
|
287
|
+
minimum: 1,
|
|
288
|
+
description: "Maximum captured requests. Defaults to 1000."
|
|
289
|
+
},
|
|
290
|
+
maxDurationMs: {
|
|
291
|
+
type: "integer",
|
|
292
|
+
minimum: 1,
|
|
293
|
+
maximum: 86400000,
|
|
294
|
+
description: "The capture ends by itself, discarding what it captured, after this long. Defaults to 1800000 (30 minutes). It also ends when this MCP server exits."
|
|
295
|
+
}
|
|
268
296
|
}),
|
|
269
297
|
readOnly: false,
|
|
270
298
|
destructive: false,
|
|
271
299
|
idempotent: false,
|
|
272
|
-
handle: (input) =>
|
|
300
|
+
handle: (input) => Effect3.gen(function* () {
|
|
273
301
|
const object = requireObject(input);
|
|
274
302
|
const explicitSession = optionalStringField(object, "session");
|
|
275
|
-
const sessionId = explicitSession ?? currentSession.id;
|
|
303
|
+
const sessionId = explicitSession ?? ctx.currentSession.id;
|
|
276
304
|
if (explicitSession) {
|
|
277
|
-
yield* ensureSessionExists(relay, sessionId);
|
|
305
|
+
yield* ensureSessionExists(ctx.relay, sessionId);
|
|
278
306
|
} else {
|
|
279
|
-
yield* relay.sessionEnsure(sessionId);
|
|
280
|
-
currentSession.established = true;
|
|
307
|
+
yield* ctx.relay.sessionEnsure(sessionId);
|
|
308
|
+
ctx.currentSession.established = true;
|
|
281
309
|
}
|
|
282
|
-
yield* ensureExtensionConnected({ relay, sessionId, waitForReconnect: true });
|
|
310
|
+
yield* ensureExtensionConnected({ relay: ctx.relay, sessionId, waitForReconnect: true });
|
|
283
311
|
const content = optionalStringField(object, "content");
|
|
284
312
|
if (content !== undefined && content !== "embed" && content !== "omit") {
|
|
285
|
-
return yield*
|
|
313
|
+
return yield* Effect3.fail(new Error("content must be embed or omit"));
|
|
286
314
|
}
|
|
287
315
|
const urlFilter = optionalStringField(object, "urlFilter");
|
|
288
316
|
const resourceTypes = optionalStringArrayField(object, "resourceTypes");
|
|
@@ -290,7 +318,7 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
290
318
|
const maxTotalBodyBytes = optionalPositiveIntegerField(object, "maxTotalBodyBytes");
|
|
291
319
|
const maxEntries = optionalPositiveIntegerField(object, "maxEntries");
|
|
292
320
|
const maxDurationMs = optionalPositiveIntegerField(object, "maxDurationMs");
|
|
293
|
-
const result = yield* relay.networkStart({
|
|
321
|
+
const result = yield* ctx.relay.networkStart({
|
|
294
322
|
sessionId,
|
|
295
323
|
...urlFilter ? { urlFilter } : {},
|
|
296
324
|
...resourceTypes && resourceTypes.length > 0 ? { resourceTypes } : {},
|
|
@@ -305,14 +333,15 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
305
333
|
},
|
|
306
334
|
{
|
|
307
335
|
name: "network_status",
|
|
336
|
+
requiresRelayCompatibility: false,
|
|
308
337
|
description: "Return bounded metadata for a session's active network capture. Captured values are never included.",
|
|
309
338
|
inputSchema: sessionOnlyInputSchema,
|
|
310
339
|
readOnly: true,
|
|
311
340
|
destructive: false,
|
|
312
341
|
idempotent: true,
|
|
313
342
|
handle: (input) => {
|
|
314
|
-
const sessionId = resolveSessionId(input);
|
|
315
|
-
return relay.networkStatus({ sessionId }).pipe(
|
|
343
|
+
const sessionId = ctx.resolveSessionId(input);
|
|
344
|
+
return ctx.relay.networkStatus({ sessionId }).pipe(Effect3.map((result) => ({ session: sessionId, ...result })));
|
|
316
345
|
}
|
|
317
346
|
},
|
|
318
347
|
{
|
|
@@ -320,22 +349,25 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
320
349
|
description: "Stop network capture. Optionally write a credential-redacted HAR and store lossless credential values in a reusable secret profile. At least one of outputPath or secrets is required.",
|
|
321
350
|
inputSchema: objectSchema({
|
|
322
351
|
session: sessionSchemaProperty,
|
|
323
|
-
outputPath: {
|
|
352
|
+
outputPath: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Optional artifact path, resolved against the MCP process working directory. The HAR contains stable ${BC_SECRET_N} references (and redaction-only ${BC_REDACTED_N} for passwords, one-time codes and token-like URL path segments), not captured values."
|
|
355
|
+
},
|
|
324
356
|
secrets: { type: "string", description: "Optional reusable profile name for captured credential values." }
|
|
325
357
|
}),
|
|
326
358
|
readOnly: false,
|
|
327
359
|
destructive: false,
|
|
328
360
|
idempotent: false,
|
|
329
|
-
handle: (input) =>
|
|
330
|
-
const sessionId = resolveSessionId(input);
|
|
361
|
+
handle: (input) => Effect3.gen(function* () {
|
|
362
|
+
const sessionId = ctx.resolveSessionId(input);
|
|
331
363
|
const outputPath = optionalStringField(input, "outputPath");
|
|
332
364
|
const secrets = optionalStringField(input, "secrets");
|
|
333
365
|
if (!outputPath && !secrets) {
|
|
334
|
-
return yield*
|
|
366
|
+
return yield* Effect3.fail(new Error("network_stop requires outputPath, secrets, or both"));
|
|
335
367
|
}
|
|
336
|
-
return yield* relay.networkStop({
|
|
368
|
+
return yield* ctx.relay.networkStop({
|
|
337
369
|
sessionId,
|
|
338
|
-
...outputPath ? { outputPath:
|
|
370
|
+
...outputPath ? { outputPath: path2.resolve(outputPath) } : {},
|
|
339
371
|
...secrets ? { secrets } : {}
|
|
340
372
|
});
|
|
341
373
|
})
|
|
@@ -347,19 +379,38 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
347
379
|
readOnly: false,
|
|
348
380
|
destructive: true,
|
|
349
381
|
idempotent: true,
|
|
350
|
-
handle: (input) => relay.networkCancel({ sessionId: resolveSessionId(input) })
|
|
351
|
-
}
|
|
382
|
+
handle: (input) => ctx.relay.networkCancel({ sessionId: ctx.resolveSessionId(input) })
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/mcp/recording.ts
|
|
388
|
+
import { Effect as Effect4 } from "effect";
|
|
389
|
+
import path3 from "node:path";
|
|
390
|
+
registerMcpTools({
|
|
391
|
+
order: 400,
|
|
392
|
+
build: (ctx) => [
|
|
352
393
|
{
|
|
353
394
|
name: "recording_start",
|
|
354
395
|
description: "Start recording the current session tab. CDP mode records video to WebM or MP4; tab-capture mode supports WebM and optional audio.",
|
|
355
396
|
inputSchema: objectSchema({
|
|
356
397
|
session: sessionSchemaProperty,
|
|
357
|
-
outputPath: {
|
|
358
|
-
|
|
398
|
+
outputPath: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "Recording artifact path, resolved against the MCP process working directory."
|
|
401
|
+
},
|
|
402
|
+
mode: {
|
|
403
|
+
type: "string",
|
|
404
|
+
enum: ["auto", "tab-capture", "cdp"],
|
|
405
|
+
description: "Recording backend. Defaults to auto."
|
|
406
|
+
},
|
|
359
407
|
audio: { type: "boolean", description: "Capture tab audio in tab-capture mode." },
|
|
360
408
|
frameRate: { type: "integer", minimum: 1, maximum: 60, description: "Requested frame rate." },
|
|
361
409
|
maxDurationMs: { type: "integer", minimum: 1, description: "Maximum recording duration in milliseconds." },
|
|
362
|
-
cursor: {
|
|
410
|
+
cursor: {
|
|
411
|
+
type: "boolean",
|
|
412
|
+
description: "Draw the ghost cursor in the recording, for demo videos. Off by default: recordings show only the page, with nothing from Browse in the frame."
|
|
413
|
+
}
|
|
363
414
|
}, ["outputPath"]),
|
|
364
415
|
readOnly: false,
|
|
365
416
|
destructive: false,
|
|
@@ -368,17 +419,17 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
368
419
|
const object = requireObject(input);
|
|
369
420
|
const mode = optionalStringField(object, "mode");
|
|
370
421
|
if (mode !== undefined && mode !== "auto" && mode !== "tab-capture" && mode !== "cdp") {
|
|
371
|
-
return
|
|
422
|
+
return Effect4.fail(new Error("mode must be auto, tab-capture, or cdp"));
|
|
372
423
|
}
|
|
373
424
|
const frameRate = optionalPositiveIntegerField(object, "frameRate");
|
|
374
425
|
if (frameRate !== undefined && frameRate > 60)
|
|
375
|
-
return
|
|
426
|
+
return Effect4.fail(new Error("frameRate must be at most 60"));
|
|
376
427
|
const maxDurationMs = optionalPositiveIntegerField(object, "maxDurationMs");
|
|
377
428
|
const audio = optionalBooleanField(object, "audio");
|
|
378
429
|
const cursor = optionalBooleanField(object, "cursor");
|
|
379
|
-
return relay.recordingStart({
|
|
380
|
-
sessionId: resolveSessionId(object),
|
|
381
|
-
outputPath:
|
|
430
|
+
return ctx.relay.recordingStart({
|
|
431
|
+
sessionId: ctx.resolveSessionId(object),
|
|
432
|
+
outputPath: path3.resolve(requiredStringField(object, "outputPath")),
|
|
382
433
|
...mode ? { mode } : {},
|
|
383
434
|
...audio === undefined ? {} : { audio },
|
|
384
435
|
...frameRate === undefined ? {} : { frameRate },
|
|
@@ -394,52 +445,22 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
394
445
|
readOnly: false,
|
|
395
446
|
destructive: false,
|
|
396
447
|
idempotent: false,
|
|
397
|
-
handle: (input) => relay.recordingStop({ sessionId: resolveSessionId(input) })
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}),
|
|
405
|
-
readOnly: false,
|
|
406
|
-
destructive: true,
|
|
407
|
-
idempotent: true,
|
|
408
|
-
handle: (input) => {
|
|
409
|
-
const value = typeof input === "object" && input !== null && "idleMinutes" in input ? input.idleMinutes : undefined;
|
|
410
|
-
return relay.tabsCleanup(typeof value === "number" && value >= 0 ? value : undefined);
|
|
411
|
-
}
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
name: "settings",
|
|
415
|
-
description: `Read or change the user's Browse settings, the same ones as the extension's options page: where agent tabs open (window), how they're grouped (grouping), when untouched agent tabs close (idleMinutes), the agent cursor (cursor), human-like input (humanInput) and handoff notifications (handoffNotifications). Use it only when the user asks to change how browse behaves, never on your own initiative. Without \`set\` it returns each setting's stored value, the value in effect, and the relay environment variable that overrides it, if any. With \`set\` it validates every value strictly, applies the change at once (the options page updates live) and returns what changed: tell the user what you changed. A setting an environment variable fixes is not changed, and the result says so. Security switches (${securityVariables.join(", ")}) are environment-only and can never be read or changed here.`,
|
|
416
|
-
inputSchema: objectSchema({
|
|
417
|
-
set: {
|
|
418
|
-
type: "object",
|
|
419
|
-
description: "Settings to change; omit to only read them.",
|
|
420
|
-
properties: Object.fromEntries(settingKeys.map((key) => [key, { enum: [...settingSpecs[key].values], description: settingSpecs[key].label }])),
|
|
421
|
-
additionalProperties: false
|
|
422
|
-
}
|
|
423
|
-
}),
|
|
424
|
-
readOnly: false,
|
|
425
|
-
destructive: false,
|
|
426
|
-
idempotent: true,
|
|
427
|
-
handle: (input) => {
|
|
428
|
-
const set = getObject2(input)?.set;
|
|
429
|
-
if (set === undefined)
|
|
430
|
-
return relay.settingsGet;
|
|
431
|
-
const changes = getObject2(set);
|
|
432
|
-
return changes ? relay.settingsSet(changes) : Effect.fail(new Error('set must be an object, for example { "grouping": "shared" }'));
|
|
433
|
-
}
|
|
434
|
-
},
|
|
448
|
+
handle: (input) => ctx.relay.recordingStop({ sessionId: ctx.resolveSessionId(input) })
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
});
|
|
452
|
+
registerMcpTools({
|
|
453
|
+
order: 500,
|
|
454
|
+
build: (ctx) => [
|
|
435
455
|
{
|
|
436
456
|
name: "recording_status",
|
|
457
|
+
requiresRelayCompatibility: false,
|
|
437
458
|
description: "Return bounded status and quality counters for a session recording.",
|
|
438
459
|
inputSchema: sessionOnlyInputSchema,
|
|
439
460
|
readOnly: true,
|
|
440
461
|
destructive: false,
|
|
441
462
|
idempotent: true,
|
|
442
|
-
handle: (input) => relay.recordingStatus({ sessionId: resolveSessionId(input) })
|
|
463
|
+
handle: (input) => ctx.relay.recordingStatus({ sessionId: ctx.resolveSessionId(input) })
|
|
443
464
|
},
|
|
444
465
|
{
|
|
445
466
|
name: "recording_cancel",
|
|
@@ -448,15 +469,25 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
448
469
|
readOnly: false,
|
|
449
470
|
destructive: true,
|
|
450
471
|
idempotent: true,
|
|
451
|
-
handle: (input) => relay.recordingCancel({ sessionId: resolveSessionId(input) })
|
|
472
|
+
handle: (input) => ctx.relay.recordingCancel({ sessionId: ctx.resolveSessionId(input) })
|
|
452
473
|
},
|
|
453
474
|
{
|
|
454
475
|
name: "flight_recorder_start",
|
|
455
476
|
description: "Start a rolling in-memory video buffer for the current session tab. Saving a clip does not stop buffering.",
|
|
456
477
|
inputSchema: objectSchema({
|
|
457
478
|
session: sessionSchemaProperty,
|
|
458
|
-
retentionMs: {
|
|
459
|
-
|
|
479
|
+
retentionMs: {
|
|
480
|
+
type: "integer",
|
|
481
|
+
minimum: 1000,
|
|
482
|
+
maximum: 120000,
|
|
483
|
+
description: "Rolling retention window. Defaults to 60000."
|
|
484
|
+
},
|
|
485
|
+
frameRate: {
|
|
486
|
+
type: "integer",
|
|
487
|
+
minimum: 1,
|
|
488
|
+
maximum: 60,
|
|
489
|
+
description: "Saved clip frame rate. Defaults to 60."
|
|
490
|
+
},
|
|
460
491
|
cursor: { type: "boolean", description: "Draw the ghost cursor in the buffered video. Off by default." }
|
|
461
492
|
}),
|
|
462
493
|
readOnly: false,
|
|
@@ -467,8 +498,8 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
467
498
|
const retentionMs = optionalPositiveIntegerField(object, "retentionMs");
|
|
468
499
|
const frameRate = optionalPositiveIntegerField(object, "frameRate");
|
|
469
500
|
const cursor = optionalBooleanField(object, "cursor");
|
|
470
|
-
return relay.flightRecorderStart({
|
|
471
|
-
sessionId: resolveSessionId(object),
|
|
501
|
+
return ctx.relay.flightRecorderStart({
|
|
502
|
+
sessionId: ctx.resolveSessionId(object),
|
|
472
503
|
...retentionMs === undefined ? {} : { retentionMs },
|
|
473
504
|
...frameRate === undefined ? {} : { frameRate },
|
|
474
505
|
...cursor === undefined ? {} : { cursor }
|
|
@@ -477,20 +508,28 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
477
508
|
},
|
|
478
509
|
{
|
|
479
510
|
name: "flight_recorder_status",
|
|
511
|
+
requiresRelayCompatibility: false,
|
|
480
512
|
description: "Return bounded rolling-buffer duration, frame, byte, and drop counters.",
|
|
481
513
|
inputSchema: sessionOnlyInputSchema,
|
|
482
514
|
readOnly: true,
|
|
483
515
|
destructive: false,
|
|
484
516
|
idempotent: true,
|
|
485
|
-
handle: (input) => relay.flightRecorderStatus({ sessionId: resolveSessionId(input) })
|
|
517
|
+
handle: (input) => ctx.relay.flightRecorderStatus({ sessionId: ctx.resolveSessionId(input) })
|
|
486
518
|
},
|
|
487
519
|
{
|
|
488
520
|
name: "flight_recorder_save_last",
|
|
489
521
|
description: "Encode and save the most recent buffered browser video without stopping the flight recorder.",
|
|
490
522
|
inputSchema: objectSchema({
|
|
491
523
|
session: sessionSchemaProperty,
|
|
492
|
-
outputPath: {
|
|
493
|
-
|
|
524
|
+
outputPath: {
|
|
525
|
+
type: "string",
|
|
526
|
+
description: "Fresh .webm or .mp4 path, resolved against the MCP process working directory."
|
|
527
|
+
},
|
|
528
|
+
durationMs: {
|
|
529
|
+
type: "integer",
|
|
530
|
+
minimum: 1,
|
|
531
|
+
description: "Recent duration to save. Defaults to 30000 and cannot exceed retention."
|
|
532
|
+
}
|
|
494
533
|
}, ["outputPath"]),
|
|
495
534
|
readOnly: false,
|
|
496
535
|
destructive: false,
|
|
@@ -498,9 +537,9 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
498
537
|
handle: (input) => {
|
|
499
538
|
const object = requireObject(input);
|
|
500
539
|
const durationMs = optionalPositiveIntegerField(object, "durationMs");
|
|
501
|
-
return relay.flightRecorderSaveLast({
|
|
502
|
-
sessionId: resolveSessionId(object),
|
|
503
|
-
outputPath:
|
|
540
|
+
return ctx.relay.flightRecorderSaveLast({
|
|
541
|
+
sessionId: ctx.resolveSessionId(object),
|
|
542
|
+
outputPath: path3.resolve(requiredStringField(object, "outputPath")),
|
|
504
543
|
...durationMs === undefined ? {} : { durationMs }
|
|
505
544
|
});
|
|
506
545
|
}
|
|
@@ -512,21 +551,45 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
512
551
|
readOnly: false,
|
|
513
552
|
destructive: true,
|
|
514
553
|
idempotent: true,
|
|
515
|
-
handle: (input) => relay.flightRecorderCancel({ sessionId: resolveSessionId(input) })
|
|
516
|
-
}
|
|
554
|
+
handle: (input) => ctx.relay.flightRecorderCancel({ sessionId: ctx.resolveSessionId(input) })
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// src/mcp/secrets.ts
|
|
560
|
+
import { Effect as Effect5 } from "effect";
|
|
561
|
+
import path4 from "node:path";
|
|
562
|
+
registerMcpTools({
|
|
563
|
+
order: 600,
|
|
564
|
+
build: (ctx) => [
|
|
517
565
|
{
|
|
518
566
|
name: "secrets_list",
|
|
567
|
+
requiresRelayCompatibility: false,
|
|
519
568
|
description: 'List what agents may type without seeing: site secrets the user stored (names, kinds and allowed origins; type them with secrets.type(target, name) in execute; only the user adds them with `opencode-browser secrets add`), and Deadbolt logins the user granted to Browse for this session\'s allowed sites (title, username, which values and passkeys, never values; sign in with the signin tool or secrets.signIn(), or type one value with secrets.type(target, "deadbolt:<site>[/<username>]", { field })). Passkeys are listed by account (username, displayName). Only the user grants Deadbolt logins, in Deadbolt (`deadbolt agents allow <site>`, or `deadbolt agents allow --all`).',
|
|
520
569
|
inputSchema: sessionOnlyInputSchema,
|
|
521
570
|
readOnly: true,
|
|
522
571
|
destructive: false,
|
|
523
572
|
idempotent: true,
|
|
524
|
-
handle: (input) =>
|
|
573
|
+
handle: (input) => Effect5.gen(function* () {
|
|
525
574
|
const secrets = yield* listSecrets();
|
|
526
575
|
if (!readClient()) {
|
|
527
|
-
return {
|
|
576
|
+
return {
|
|
577
|
+
secrets,
|
|
578
|
+
deadbolt: {
|
|
579
|
+
connected: false,
|
|
580
|
+
logins: [],
|
|
581
|
+
passkeys: [],
|
|
582
|
+
note: "Browse isn't connected to Deadbolt. The user can connect it with `opencode-browser deadbolt connect` and grant logins with `deadbolt agents allow <site>`."
|
|
583
|
+
}
|
|
584
|
+
};
|
|
528
585
|
}
|
|
529
|
-
const deadbolt = yield* relay.deadboltLogins({ sessionId: resolveSessionId(input) }).pipe(
|
|
586
|
+
const deadbolt = yield* ctx.relay.deadboltLogins({ sessionId: ctx.resolveSessionId(input) }).pipe(Effect5.catch((error) => Effect5.succeed({
|
|
587
|
+
connected: true,
|
|
588
|
+
origins: [],
|
|
589
|
+
logins: [],
|
|
590
|
+
passkeys: [],
|
|
591
|
+
error: { code: "unavailable", message: error.message }
|
|
592
|
+
})));
|
|
530
593
|
return { secrets, deadbolt };
|
|
531
594
|
})
|
|
532
595
|
},
|
|
@@ -534,39 +597,59 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
534
597
|
name: "signin",
|
|
535
598
|
description: 'Sign in to a site in the session\'s page with a Deadbolt login the user granted to Browse, without seeing any value. The relay does the whole sign-in in one call: username, submit, password, an authenticator (TOTP) code when asked (split boxes too), "Use password instead" toggles, and Sign in with Google when a Google login is granted. A username also picks the passkey account when the site signs in with a passkey. Returns only {status: signed_in | needs_user | needs_choice | not_granted | failed, step, reason}; needs_choice comes with choices [{username, displayName}] when several passkeys could sign in: call again with one username. needs_user means a captcha, a code sent by SMS or email, a push approval or a new-device check: hand it to the user with handoff() in execute. not_granted: tell the user what to allow (`deadbolt agents allow <site>`). Navigate to the sign-in page first, or pass site.',
|
|
536
599
|
inputSchema: objectSchema({
|
|
537
|
-
site: {
|
|
538
|
-
|
|
600
|
+
site: {
|
|
601
|
+
type: "string",
|
|
602
|
+
description: "The login's site: a host like github.com, or an exact origin. Defaults to the page's site; the page goes there first when it is elsewhere."
|
|
603
|
+
},
|
|
604
|
+
username: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: "Which login (and which passkey account), when several are granted for the site."
|
|
607
|
+
},
|
|
539
608
|
session: sessionSchemaProperty,
|
|
540
609
|
timeoutMs: { type: "integer", minimum: 1, description: "Overall limit. Defaults to 90000." }
|
|
541
610
|
}),
|
|
542
611
|
readOnly: false,
|
|
543
612
|
destructive: true,
|
|
544
613
|
idempotent: false,
|
|
545
|
-
handle: (input) =>
|
|
614
|
+
handle: (input) => Effect5.gen(function* () {
|
|
546
615
|
const object = input === undefined ? {} : requireObject(input);
|
|
547
616
|
const site = optionalStringField(object, "site");
|
|
548
617
|
const username = optionalStringField(object, "username");
|
|
549
618
|
const timeoutMs = optionalPositiveIntegerField(object, "timeoutMs");
|
|
550
619
|
const explicitSession = optionalStringField(object, "session");
|
|
551
|
-
const sessionId = explicitSession ?? currentSession.id;
|
|
552
|
-
yield* ensureExtensionConnected({ relay, sessionId, waitForReconnect: true });
|
|
553
|
-
const options = {
|
|
620
|
+
const sessionId = explicitSession ?? ctx.currentSession.id;
|
|
621
|
+
yield* ensureExtensionConnected({ relay: ctx.relay, sessionId, waitForReconnect: true });
|
|
622
|
+
const options = {
|
|
623
|
+
...site ? { site } : {},
|
|
624
|
+
...username ? { username } : {},
|
|
625
|
+
...timeoutMs === undefined ? {} : { timeoutMs }
|
|
626
|
+
};
|
|
554
627
|
const outerTimeoutMs = timeoutMs === undefined ? 120000 : Math.max(60000, timeoutMs + 1e4);
|
|
555
|
-
const result = yield* relay.execute({
|
|
556
|
-
|
|
628
|
+
const result = yield* ctx.relay.execute({
|
|
629
|
+
sessionId,
|
|
630
|
+
code: `return await secrets.signIn(${JSON.stringify(options)})`,
|
|
631
|
+
createIfMissing: !explicitSession,
|
|
632
|
+
timeoutMs: outerTimeoutMs
|
|
633
|
+
});
|
|
634
|
+
ctx.establishCurrentSession(sessionId);
|
|
557
635
|
if (result.isError)
|
|
558
|
-
return yield*
|
|
559
|
-
return {
|
|
636
|
+
return yield* Effect5.fail(new Error(result.text));
|
|
637
|
+
return {
|
|
638
|
+
...result.value,
|
|
639
|
+
...result.aftermath?.endUrl ? { url: result.aftermath.endUrl } : {},
|
|
640
|
+
...result.warnings?.length ? { warnings: result.warnings } : {}
|
|
641
|
+
};
|
|
560
642
|
})
|
|
561
643
|
},
|
|
562
644
|
{
|
|
563
645
|
name: "secrets_status",
|
|
646
|
+
requiresRelayCompatibility: false,
|
|
564
647
|
description: "Return secret profile references, sources, and expiration metadata without revealing credential values.",
|
|
565
648
|
inputSchema: objectSchema({ name: { type: "string", description: "Secret profile name." } }, ["name"]),
|
|
566
649
|
readOnly: true,
|
|
567
650
|
destructive: false,
|
|
568
651
|
idempotent: true,
|
|
569
|
-
handle: (input) => relay.authStatus({ name: requiredStringField(input, "name") })
|
|
652
|
+
handle: (input) => ctx.relay.authStatus({ name: requiredStringField(input, "name") })
|
|
570
653
|
},
|
|
571
654
|
{
|
|
572
655
|
name: "secrets_refresh",
|
|
@@ -584,8 +667,8 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
584
667
|
const object = requireObject(input);
|
|
585
668
|
const timeoutMs = optionalPositiveIntegerField(object, "timeoutMs");
|
|
586
669
|
const urlFilter = optionalStringField(object, "urlFilter");
|
|
587
|
-
return relay.authRefresh({
|
|
588
|
-
sessionId: resolveSessionId(object),
|
|
670
|
+
return ctx.relay.authRefresh({
|
|
671
|
+
sessionId: ctx.resolveSessionId(object),
|
|
589
672
|
name: requiredStringField(object, "name"),
|
|
590
673
|
...urlFilter ? { urlFilter } : {},
|
|
591
674
|
...timeoutMs === undefined ? {} : { timeoutMs }
|
|
@@ -610,30 +693,204 @@ function makeToolSpecs(relay, currentSession, update = () => {
|
|
|
610
693
|
const args = optionalStringArrayField(object, "args");
|
|
611
694
|
const cwd = optionalStringField(object, "cwd");
|
|
612
695
|
const timeoutMs = optionalPositiveIntegerField(object, "timeoutMs");
|
|
613
|
-
return relay.authRun({
|
|
696
|
+
return ctx.relay.authRun({
|
|
614
697
|
name: requiredStringField(object, "name"),
|
|
615
698
|
command: requiredStringField(object, "command"),
|
|
616
699
|
...args ? { args } : {},
|
|
617
|
-
cwd:
|
|
700
|
+
cwd: path4.resolve(cwd ?? process.cwd()),
|
|
618
701
|
...timeoutMs === undefined ? {} : { timeoutMs }
|
|
619
702
|
});
|
|
620
703
|
}
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// src/mcp/session.ts
|
|
709
|
+
import { Effect as Effect6, Predicate } from "effect";
|
|
710
|
+
registerMcpTools({
|
|
711
|
+
order: 200,
|
|
712
|
+
build: (ctx) => [
|
|
713
|
+
{
|
|
714
|
+
name: "session_new",
|
|
715
|
+
listsBrowsers: true,
|
|
716
|
+
description: `Create a Browse session and make it current for this MCP server.${ctx.browserNote}`,
|
|
717
|
+
inputSchema: objectSchema({
|
|
718
|
+
browser: ctx.browserProperty,
|
|
719
|
+
id: { type: "string", description: "Optional lowercase session id." },
|
|
720
|
+
readOnly: {
|
|
721
|
+
type: "boolean",
|
|
722
|
+
description: "Create a read-only session: the relay rejects input-dispatching CDP so scripts can inspect but not click or type."
|
|
723
|
+
},
|
|
724
|
+
allowedOrigins: {
|
|
725
|
+
type: "array",
|
|
726
|
+
items: { type: "string" },
|
|
727
|
+
description: "Optional domain allowlist: hosts or origins this session's tabs may load (example.com, *.example.com, https://app.example.com, http://localhost:3000). Navigations elsewhere are refused or sent back to about:blank; private and loopback addresses need an exact entry."
|
|
728
|
+
}
|
|
729
|
+
}),
|
|
730
|
+
readOnly: false,
|
|
731
|
+
destructive: false,
|
|
732
|
+
idempotent: false,
|
|
733
|
+
handle: (input) => Effect6.gen(function* () {
|
|
734
|
+
const requestedId = optionalStringField(input, "id");
|
|
735
|
+
const readOnly = optionalBooleanField(input, "readOnly");
|
|
736
|
+
const allowedOrigins = optionalStringArrayField(input, "allowedOrigins");
|
|
737
|
+
const browser = optionalStringField(input, "browser");
|
|
738
|
+
const session = yield* ctx.relay.sessionNew(requestedId, {
|
|
739
|
+
...readOnly ? { readOnly: true } : {},
|
|
740
|
+
...allowedOrigins ? { allowedOrigins } : {},
|
|
741
|
+
...browser ? { browser } : {}
|
|
742
|
+
});
|
|
743
|
+
ctx.establishCurrentSession(session.id);
|
|
744
|
+
return { session };
|
|
745
|
+
})
|
|
621
746
|
},
|
|
622
747
|
{
|
|
623
|
-
name: "
|
|
624
|
-
description: "
|
|
748
|
+
name: "session_allowlist",
|
|
749
|
+
description: "Set the domain allowlist of a session (the current one by default): the hosts or origins its tabs may load. Pass an empty list to remove it. Use it before typing secrets or working on sites with untrusted content.",
|
|
750
|
+
inputSchema: objectSchema({
|
|
751
|
+
session: { type: "string", description: "Session id; defaults to the current session." },
|
|
752
|
+
allowedOrigins: {
|
|
753
|
+
type: "array",
|
|
754
|
+
items: { type: "string" },
|
|
755
|
+
description: "Patterns: example.com, *.example.com, https://app.example.com, http://localhost:3000. Empty removes the allowlist."
|
|
756
|
+
}
|
|
757
|
+
}, ["allowedOrigins"]),
|
|
758
|
+
readOnly: false,
|
|
759
|
+
destructive: false,
|
|
760
|
+
idempotent: true,
|
|
761
|
+
handle: (input) => Effect6.gen(function* () {
|
|
762
|
+
const sessionId = ctx.resolveSessionId(input);
|
|
763
|
+
const allowedOrigins = requireObject(input).allowedOrigins;
|
|
764
|
+
if (!Array.isArray(allowedOrigins) || !allowedOrigins.every((item) => Predicate.isString(item))) {
|
|
765
|
+
return yield* Effect6.fail(new Error("allowedOrigins must be an array of strings"));
|
|
766
|
+
}
|
|
767
|
+
return { session: yield* ctx.relay.sessionAllowlist(sessionId, allowedOrigins) };
|
|
768
|
+
})
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: "session_list",
|
|
772
|
+
requiresRelayCompatibility: false,
|
|
773
|
+
description: "List Browse sessions.",
|
|
774
|
+
inputSchema: emptyInputSchema,
|
|
775
|
+
readOnly: true,
|
|
776
|
+
destructive: false,
|
|
777
|
+
idempotent: false,
|
|
778
|
+
handle: () => ctx.relay.sessions.pipe(Effect6.map((sessions) => ({ sessions })))
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "session_current",
|
|
782
|
+
requiresRelayCompatibility: false,
|
|
783
|
+
description: "Return this MCP server's current Browse session id.",
|
|
625
784
|
inputSchema: emptyInputSchema,
|
|
626
785
|
readOnly: true,
|
|
627
786
|
destructive: false,
|
|
628
787
|
idempotent: true,
|
|
629
|
-
handle: () =>
|
|
630
|
-
|
|
631
|
-
|
|
788
|
+
handle: () => Effect6.succeed({ currentSession: ctx.currentSession.id })
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "session_use",
|
|
792
|
+
description: "Set this MCP server's current Browse session id.",
|
|
793
|
+
inputSchema: objectSchema({
|
|
794
|
+
id: { type: "string", description: "Existing Browse session id." }
|
|
795
|
+
}, ["id"]),
|
|
796
|
+
readOnly: false,
|
|
797
|
+
destructive: false,
|
|
798
|
+
idempotent: true,
|
|
799
|
+
handle: (input) => Effect6.gen(function* () {
|
|
800
|
+
const id = yield* Effect6.try(() => requiredStringField(input, "id"));
|
|
801
|
+
yield* ensureSessionExists(ctx.relay, id);
|
|
802
|
+
ctx.establishCurrentSession(id);
|
|
803
|
+
return { currentSession: ctx.currentSession.id };
|
|
804
|
+
})
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: "session_reset",
|
|
808
|
+
description: "Reset a Browse session's state and page.",
|
|
809
|
+
inputSchema: objectSchema({
|
|
810
|
+
id: sessionSchemaProperty,
|
|
811
|
+
session: sessionAliasSchemaProperty
|
|
812
|
+
}),
|
|
813
|
+
readOnly: false,
|
|
814
|
+
destructive: true,
|
|
815
|
+
idempotent: false,
|
|
816
|
+
handle: (input) => Effect6.gen(function* () {
|
|
817
|
+
const id = ctx.resolveSessionId(input, "id");
|
|
818
|
+
const reset = yield* ctx.relay.sessionReset(id);
|
|
819
|
+
ctx.establishCurrentSession(id);
|
|
820
|
+
return reset;
|
|
821
|
+
})
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "session_delete",
|
|
825
|
+
description: "Delete a Browse session.",
|
|
826
|
+
inputSchema: objectSchema({
|
|
827
|
+
id: sessionSchemaProperty,
|
|
828
|
+
session: sessionAliasSchemaProperty
|
|
829
|
+
}),
|
|
830
|
+
readOnly: false,
|
|
831
|
+
destructive: true,
|
|
832
|
+
idempotent: true,
|
|
833
|
+
handle: (input) => Effect6.gen(function* () {
|
|
834
|
+
const id = ctx.resolveSessionId(input, "id");
|
|
835
|
+
const result = yield* ctx.relay.sessionDelete(id);
|
|
836
|
+
if (ctx.currentSession.id === id) {
|
|
837
|
+
ctx.currentSession.id = `mcp-${crypto.randomUUID().slice(0, 8)}`;
|
|
838
|
+
ctx.currentSession.established = false;
|
|
839
|
+
}
|
|
840
|
+
return { ...result, currentSession: ctx.currentSession.id };
|
|
841
|
+
})
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "session_adopt",
|
|
845
|
+
description: "Make an attached tab the Browse session's default page for subsequent bare execute calls. Omit targetUrl and targetIndex when only one user-attached tab is available.",
|
|
846
|
+
inputSchema: objectSchema({
|
|
847
|
+
session: {
|
|
848
|
+
type: "string",
|
|
849
|
+
description: "Optional existing Browse session id. Explicit ids must already exist; omit this field to use the MCP server's current session, which is created when needed."
|
|
850
|
+
},
|
|
851
|
+
targetUrl: {
|
|
852
|
+
type: "string",
|
|
853
|
+
description: "Adopt an existing attached page whose URL contains this text. Omit when only one user-attached tab is available. This does not navigate or open a URL."
|
|
854
|
+
},
|
|
855
|
+
targetIndex: {
|
|
856
|
+
type: "integer",
|
|
857
|
+
minimum: 0,
|
|
858
|
+
description: "Adopt the attached page at this zero-based target index."
|
|
859
|
+
},
|
|
860
|
+
tabId: {
|
|
861
|
+
type: "integer",
|
|
862
|
+
description: "Adopt the user's tab with this Chrome tab id (the tab= value in status)."
|
|
863
|
+
}
|
|
864
|
+
}),
|
|
865
|
+
readOnly: false,
|
|
866
|
+
destructive: false,
|
|
867
|
+
idempotent: false,
|
|
868
|
+
handle: (input) => Effect6.gen(function* () {
|
|
869
|
+
const args = yield* Effect6.try(() => parseAdoptArguments(input));
|
|
870
|
+
const sessionId = args.session ?? ctx.currentSession.id;
|
|
871
|
+
const result = yield* ctx.relay.sessionAdopt({
|
|
872
|
+
sessionId,
|
|
873
|
+
createIfMissing: !args.session,
|
|
874
|
+
...args.targetSelection ? { targetSelection: args.targetSelection } : {},
|
|
875
|
+
...args.tabId === undefined ? {} : { tabId: args.tabId }
|
|
876
|
+
});
|
|
877
|
+
ctx.establishCurrentSession(sessionId);
|
|
878
|
+
return {
|
|
879
|
+
...result,
|
|
880
|
+
confirmation: `Adopted session '${result.session.id}' default page: ${result.adoptedUrl}`
|
|
881
|
+
};
|
|
632
882
|
})
|
|
633
883
|
}
|
|
634
|
-
]
|
|
884
|
+
]
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// src/mcp.ts
|
|
888
|
+
var packageRoot = path5.dirname(path5.dirname(fileURLToPath(import.meta.url)));
|
|
889
|
+
function shimSession() {
|
|
890
|
+
const value = process.env.OPENCODE_BROWSER_MCP_SESSION;
|
|
891
|
+
return value && /^mcp-[0-9a-f]{8}$/.test(value) ? value : undefined;
|
|
635
892
|
}
|
|
636
|
-
var registerTools =
|
|
893
|
+
var registerTools = Effect7.gen(function* () {
|
|
637
894
|
const server = yield* McpServer.McpServer;
|
|
638
895
|
const relay = yield* Service;
|
|
639
896
|
const configuredSession = Option.getOrUndefined(yield* Config.option(Config.String("OPENCODE_BROWSER_SESSION")));
|
|
@@ -654,7 +911,7 @@ var registerTools = Effect.gen(function* () {
|
|
|
654
911
|
return result;
|
|
655
912
|
return new McpSchema.CallToolResult({
|
|
656
913
|
...result,
|
|
657
|
-
content: [...result.content, McpSchema.TextContent.make({ text:
|
|
914
|
+
content: [...result.content, McpSchema.TextContent.make({ text: mcpUpdateNote2(notice) })]
|
|
658
915
|
});
|
|
659
916
|
};
|
|
660
917
|
const register = (spec) => server.addTool({
|
|
@@ -671,37 +928,33 @@ var registerTools = Effect.gen(function* () {
|
|
|
671
928
|
}),
|
|
672
929
|
annotations: Context.empty(),
|
|
673
930
|
handle: (payload) => {
|
|
674
|
-
const operation =
|
|
675
|
-
return operation.pipe(
|
|
676
|
-
onFailure: (error) => toolResult({ text:
|
|
677
|
-
onSuccess: (value) =>
|
|
678
|
-
}),
|
|
931
|
+
const operation = spec.requiresRelayCompatibility !== false ? ensureRelay({ relay }).pipe(Effect7.flatMap((readiness) => readiness.buildProblem ? Effect7.fail(new Error(readiness.buildProblem)) : spec.handle(payload))) : spec.handle(payload);
|
|
932
|
+
return operation.pipe(Effect7.match({
|
|
933
|
+
onFailure: (error) => toolResult({ text: mcpErrorMessage2(spec.name, error.message), isError: true }),
|
|
934
|
+
onSuccess: (value) => toolResultForValue2(value)
|
|
935
|
+
}), Effect7.map((result) => withUpdateNote(spec.name, result)));
|
|
679
936
|
}
|
|
680
937
|
});
|
|
681
|
-
const connectedBrowsers = relay.extensionStatus.pipe(
|
|
682
|
-
let browsers = yield* connectedBrowsers.pipe(
|
|
683
|
-
|
|
684
|
-
|
|
938
|
+
const connectedBrowsers = relay.extensionStatus.pipe(Effect7.map((status) => (status.browsers ?? []).map(({ id, name, default: isDefault }) => ({ id, name, default: isDefault }))), Effect7.orElseSucceed(() => []));
|
|
939
|
+
let browsers = yield* connectedBrowsers.pipe(Effect7.timeoutOption(1000), Effect7.map(Option.getOrElse(() => [])));
|
|
940
|
+
const buildSpecs = (currentBrowsers) => buildRegisteredMcpTools(createMcpToolContext({
|
|
941
|
+
relay,
|
|
942
|
+
currentSession,
|
|
943
|
+
packageRoot,
|
|
944
|
+
update: () => updates.current,
|
|
945
|
+
browsers: currentBrowsers
|
|
946
|
+
}));
|
|
947
|
+
yield* Effect7.forEach(buildSpecs(browsers), register, { discard: true });
|
|
948
|
+
const refreshBrowsers = Effect7.gen(function* () {
|
|
685
949
|
const next = yield* connectedBrowsers;
|
|
686
|
-
if (
|
|
950
|
+
if (browsersNote2(next) === browsersNote2(browsers))
|
|
687
951
|
return;
|
|
688
952
|
browsers = next;
|
|
689
|
-
const specs =
|
|
690
|
-
yield*
|
|
953
|
+
const specs = buildSpecs(browsers).filter((spec) => spec.listsBrowsers === true);
|
|
954
|
+
yield* Effect7.forEach(specs, register, { discard: true });
|
|
691
955
|
});
|
|
692
|
-
yield* refreshBrowsers.pipe(
|
|
956
|
+
yield* refreshBrowsers.pipe(Effect7.repeat(Schedule.spaced("5 seconds")), Effect7.forkDetach);
|
|
693
957
|
});
|
|
694
|
-
var browserTools = new Set(["execute", "session_new"]);
|
|
695
|
-
function mcpUpdateNote(notice) {
|
|
696
|
-
return `Note: Browse has an update: ${notice.summary}. Tell the user they can run \`${notice.command}\` and then restart opencode, or ask you to run it for them.`;
|
|
697
|
-
}
|
|
698
|
-
function shimSession() {
|
|
699
|
-
const value = process.env.OPENCODE_BROWSER_MCP_SESSION;
|
|
700
|
-
return value && /^mcp-[0-9a-f]{8}$/.test(value) ? value : undefined;
|
|
701
|
-
}
|
|
702
|
-
function mcpToolRequiresRelayCompatibility(name) {
|
|
703
|
-
return !["status", "session_list", "session_current", "network_status", "recording_status", "flight_recorder_status", "secrets_status", "secrets_list", "skill"].includes(name);
|
|
704
|
-
}
|
|
705
958
|
var mcpServerLayer = McpServer.layerStdio({
|
|
706
959
|
name: "browse",
|
|
707
960
|
version: opencodeBrowserVersion2,
|
|
@@ -709,162 +962,12 @@ var mcpServerLayer = McpServer.layerStdio({
|
|
|
709
962
|
});
|
|
710
963
|
var mcpToolsLayer = Layer.effectDiscard(registerTools);
|
|
711
964
|
var runMcpServer = Layer.launch(mcpToolsLayer.pipe(Layer.provide(mcpServerLayer), Layer.provide(NodeStdio.layer), Layer.provide(layerFetchMcp)));
|
|
712
|
-
var ensureSessionExists = Effect.fnUntraced(function* (relay, id) {
|
|
713
|
-
const sessions = yield* relay.sessions;
|
|
714
|
-
const exists = sessions.some((session) => {
|
|
715
|
-
return session.id === id;
|
|
716
|
-
});
|
|
717
|
-
if (!exists) {
|
|
718
|
-
return yield* Effect.fail(new Error(`Session not found: ${id}`));
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
function parseExecuteArguments(input) {
|
|
722
|
-
const object = requireObject(input);
|
|
723
|
-
const code = requiredStringField(object, "code");
|
|
724
|
-
const session = optionalStringField(object, "session");
|
|
725
|
-
const timeoutMs = optionalNonNegativeIntegerField(object, "timeoutMs");
|
|
726
|
-
const targetSelection = parseMcpTargetSelection(object);
|
|
727
|
-
const browser = optionalStringField(object, "browser");
|
|
728
|
-
return {
|
|
729
|
-
code,
|
|
730
|
-
...session ? { session } : {},
|
|
731
|
-
...timeoutMs === undefined ? {} : { timeoutMs },
|
|
732
|
-
...targetSelection ? { targetSelection } : {},
|
|
733
|
-
...browser ? { browser } : {}
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
function parseAdoptArguments(input) {
|
|
737
|
-
const object = requireObject(input);
|
|
738
|
-
const session = optionalStringField(object, "session");
|
|
739
|
-
const targetSelection = parseMcpTargetSelection(object);
|
|
740
|
-
const tabId = typeof object.tabId === "number" && Number.isInteger(object.tabId) ? object.tabId : undefined;
|
|
741
|
-
if (tabId !== undefined && targetSelection)
|
|
742
|
-
throw new Error("Use only one of tabId, targetUrl, or targetIndex");
|
|
743
|
-
return {
|
|
744
|
-
...session ? { session } : {},
|
|
745
|
-
...targetSelection ? { targetSelection } : {},
|
|
746
|
-
...tabId === undefined ? {} : { tabId }
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
function parseMcpTargetSelection(input) {
|
|
750
|
-
const urlIncludes = optionalStringField(input, "targetUrl");
|
|
751
|
-
const selection = parseTargetSelection2({
|
|
752
|
-
...urlIncludes ? { urlIncludes } : {},
|
|
753
|
-
...input.targetIndex === undefined ? {} : { index: input.targetIndex }
|
|
754
|
-
});
|
|
755
|
-
return selection?.urlIncludes || selection?.index !== undefined ? selection : undefined;
|
|
756
|
-
}
|
|
757
|
-
function requiredStringField(input, field) {
|
|
758
|
-
const value = optionalStringField(input, field);
|
|
759
|
-
if (!value) {
|
|
760
|
-
throw new Error(`${field} is required`);
|
|
761
|
-
}
|
|
762
|
-
return value;
|
|
763
|
-
}
|
|
764
|
-
function optionalStringField(input, field) {
|
|
765
|
-
const value = getString2(requireObject(input), field);
|
|
766
|
-
return value ? value : undefined;
|
|
767
|
-
}
|
|
768
|
-
function optionalBooleanField(input, field) {
|
|
769
|
-
const value = requireObject(input)[field];
|
|
770
|
-
return Predicate.isBoolean(value) ? value : undefined;
|
|
771
|
-
}
|
|
772
|
-
function optionalPositiveIntegerField(input, field) {
|
|
773
|
-
const value = requireObject(input)[field];
|
|
774
|
-
if (value === undefined)
|
|
775
|
-
return;
|
|
776
|
-
if (!Predicate.isNumber(value) || !Number.isInteger(value) || value <= 0) {
|
|
777
|
-
throw new Error(`${field} must be a positive integer`);
|
|
778
|
-
}
|
|
779
|
-
return value;
|
|
780
|
-
}
|
|
781
|
-
function optionalNonNegativeIntegerField(input, field) {
|
|
782
|
-
const value = requireObject(input)[field];
|
|
783
|
-
if (value === undefined)
|
|
784
|
-
return;
|
|
785
|
-
if (!Predicate.isNumber(value) || !Number.isInteger(value) || value < 0) {
|
|
786
|
-
throw new Error(`${field} must be a non-negative integer`);
|
|
787
|
-
}
|
|
788
|
-
return value;
|
|
789
|
-
}
|
|
790
|
-
function optionalStringArrayField(input, field) {
|
|
791
|
-
const value = requireObject(input)[field];
|
|
792
|
-
if (value === undefined)
|
|
793
|
-
return;
|
|
794
|
-
if (!Array.isArray(value) || !value.every((item) => Predicate.isString(item) && item.length > 0)) {
|
|
795
|
-
throw new Error(`${field} must be an array of non-empty strings`);
|
|
796
|
-
}
|
|
797
|
-
return value;
|
|
798
|
-
}
|
|
799
|
-
function requireObject(input) {
|
|
800
|
-
const object = getObject2(input);
|
|
801
|
-
if (!object) {
|
|
802
|
-
throw new Error("Expected arguments object");
|
|
803
|
-
}
|
|
804
|
-
return object;
|
|
805
|
-
}
|
|
806
|
-
function stringifyResult(value) {
|
|
807
|
-
if (Predicate.isString(value)) {
|
|
808
|
-
return value;
|
|
809
|
-
}
|
|
810
|
-
return JSON.stringify(value, null, 2);
|
|
811
|
-
}
|
|
812
|
-
function toolResultForValue(value) {
|
|
813
|
-
const object = getObject2(value);
|
|
814
|
-
const isError = object?.isError === true;
|
|
815
|
-
const media = Array.isArray(object?.media) ? object.media.flatMap((item) => {
|
|
816
|
-
const image = getObject2(item);
|
|
817
|
-
const mimeType = getString2(image, "mimeType");
|
|
818
|
-
const data = getString2(image, "data");
|
|
819
|
-
return image?.type === "image" && mimeType !== undefined && data !== undefined ? [{ data, mimeType }] : [];
|
|
820
|
-
}) : [];
|
|
821
|
-
const errorText = isError ? getString2(object, "text") : undefined;
|
|
822
|
-
if (media.length > 0) {
|
|
823
|
-
const { media: _media, ...structuredContent } = object ?? {};
|
|
824
|
-
const text = errorText ?? stringifyResult(structuredContent);
|
|
825
|
-
return new McpSchema.CallToolResult({
|
|
826
|
-
content: [
|
|
827
|
-
McpSchema.TextContent.make({ text }),
|
|
828
|
-
...media.map((image) => McpSchema.ImageContent.make({
|
|
829
|
-
data: new Uint8Array(Buffer.from(image.data, "base64")),
|
|
830
|
-
mimeType: image.mimeType
|
|
831
|
-
}))
|
|
832
|
-
],
|
|
833
|
-
structuredContent,
|
|
834
|
-
isError
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
const text = errorText ?? stringifyResult(value);
|
|
838
|
-
return toolResult({ text, ...object ? { structuredContent: object } : {}, isError });
|
|
839
|
-
}
|
|
840
|
-
function mcpErrorMessage(tool, message) {
|
|
841
|
-
if (!message.startsWith("Session not found:")) {
|
|
842
|
-
return message;
|
|
843
|
-
}
|
|
844
|
-
return tool === "execute" || tool === "session_adopt" ? `${message} Create it with session_new first, or omit the explicit session id to use the MCP current session.` : `${message} Create it with session_new first.`;
|
|
845
|
-
}
|
|
846
|
-
function toolResult(options) {
|
|
847
|
-
return new McpSchema.CallToolResult({
|
|
848
|
-
content: [McpSchema.TextContent.make({ text: options.text })],
|
|
849
|
-
structuredContent: options.structuredContent,
|
|
850
|
-
isError: options.isError
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
function objectSchema(properties, required = []) {
|
|
854
|
-
return {
|
|
855
|
-
type: "object",
|
|
856
|
-
properties,
|
|
857
|
-
required: [...required],
|
|
858
|
-
additionalProperties: false
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
965
|
export {
|
|
862
|
-
browsersNote,
|
|
863
|
-
mcpErrorMessage,
|
|
966
|
+
browsersNote2 as browsersNote,
|
|
967
|
+
mcpErrorMessage2 as mcpErrorMessage,
|
|
864
968
|
mcpServerLayer,
|
|
865
|
-
mcpToolRequiresRelayCompatibility,
|
|
866
969
|
mcpToolsLayer,
|
|
867
|
-
mcpUpdateNote,
|
|
970
|
+
mcpUpdateNote2 as mcpUpdateNote,
|
|
868
971
|
runMcpServer,
|
|
869
|
-
toolResultForValue
|
|
972
|
+
toolResultForValue2 as toolResultForValue
|
|
870
973
|
};
|