gclm-code 1.0.0 → 1.0.1

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 (43) hide show
  1. package/README.md +1 -1
  2. package/bin/gc.js +53 -25
  3. package/bin/install-runtime.js +253 -0
  4. package/package.json +10 -5
  5. package/vendor/manifest.json +92 -0
  6. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
  7. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
  8. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
  9. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
  10. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
  11. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
  12. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
  13. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
  14. package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
  15. package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
  16. package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
  17. package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
  18. package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
  19. package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
  20. package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
  21. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
  22. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
  23. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
  24. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
  25. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
  26. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
  27. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
  28. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
  29. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
  30. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
  31. package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
  32. package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
  33. package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
  34. package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
  35. package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
  36. package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
  37. package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
  38. package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
  39. package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
  40. package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
  41. package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
  42. package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
  43. package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
@@ -0,0 +1,69 @@
1
+ export type {
2
+ ComputerExecutor,
3
+ DisplayGeometry,
4
+ FrontmostApp,
5
+ InstalledApp,
6
+ ResolvePrepareCaptureResult,
7
+ RunningApp,
8
+ ScreenshotResult,
9
+ } from "./executor.js";
10
+
11
+ export type {
12
+ AppGrant,
13
+ CuAppPermTier,
14
+ ComputerUseHostAdapter,
15
+ ComputerUseOverrides,
16
+ ComputerUseSessionContext,
17
+ CoordinateMode,
18
+ CuGrantFlags,
19
+ CuPermissionRequest,
20
+ CuPermissionResponse,
21
+ CuSubGates,
22
+ CuTeachPermissionRequest,
23
+ Logger,
24
+ ResolvedAppRequest,
25
+ ScreenshotDims,
26
+ TeachStepRequest,
27
+ TeachStepResult,
28
+ } from "./types.js";
29
+
30
+ export { DEFAULT_GRANT_FLAGS } from "./types.js";
31
+
32
+ export {
33
+ SENTINEL_BUNDLE_IDS,
34
+ getSentinelCategory,
35
+ } from "./sentinelApps.js";
36
+ export type { SentinelCategory } from "./sentinelApps.js";
37
+
38
+ export {
39
+ categoryToTier,
40
+ getDefaultTierForApp,
41
+ getDeniedCategory,
42
+ getDeniedCategoryByDisplayName,
43
+ getDeniedCategoryForApp,
44
+ isPolicyDenied,
45
+ } from "./deniedApps.js";
46
+ export type { DeniedCategory } from "./deniedApps.js";
47
+
48
+ export { isSystemKeyCombo, normalizeKeySequence } from "./keyBlocklist.js";
49
+
50
+ export { ALL_SUB_GATES_OFF, ALL_SUB_GATES_ON } from "./subGates.js";
51
+
52
+ export { API_RESIZE_PARAMS, targetImageSize } from "./imageResize.js";
53
+ export type { ResizeParams } from "./imageResize.js";
54
+
55
+ export { defersLockAcquire, handleToolCall } from "./toolCalls.js";
56
+ export type {
57
+ CuCallTelemetry,
58
+ CuCallToolResult,
59
+ CuErrorKind,
60
+ } from "./toolCalls.js";
61
+
62
+ export { bindSessionContext, createComputerUseMcpServer } from "./mcpServer.js";
63
+ export { buildComputerUseTools } from "./tools.js";
64
+
65
+ export {
66
+ comparePixelAtLocation,
67
+ validateClickTarget,
68
+ } from "./pixelCompare.js";
69
+ export type { CropRawPatchFn, PixelCompareResult } from "./pixelCompare.js";
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Key combos that cross app boundaries or terminate processes. Gated behind
3
+ * the `systemKeyCombos` grant flag. When that flag is off, the `key` tool
4
+ * rejects these and returns a tool error telling the model to request the
5
+ * flag; all other combos work normally.
6
+ *
7
+ * Matching is canonicalized: every modifier alias the Rust executor accepts
8
+ * collapses to one canonical name. Without this, `command+q` / `meta+q` /
9
+ * `cmd+alt+escape` bypass the gate — see keyBlocklist.test.ts for the three
10
+ * bypass forms and the Rust parity check that catches future alias drift.
11
+ */
12
+
13
+ /**
14
+ * Every modifier alias enigo_wrap.rs accepts (two copies: :351-359, :564-572),
15
+ * mapped to one canonical per Key:: variant. Left/right variants collapse —
16
+ * the blocklist doesn't distinguish which Ctrl.
17
+ *
18
+ * Canonical names are Rust's own variant names lowercased. Blocklist entries
19
+ * below use ONLY these. "meta" reads odd for Cmd+Q but it's honest: Rust
20
+ * sends Key::Meta, which is Cmd on darwin and Win on win32.
21
+ */
22
+ const CANONICAL_MODIFIER: Readonly<Record<string, string>> = {
23
+ // Key::Meta — "meta"|"super"|"command"|"cmd"|"windows"|"win"
24
+ meta: "meta",
25
+ super: "meta",
26
+ command: "meta",
27
+ cmd: "meta",
28
+ windows: "meta",
29
+ win: "meta",
30
+ // Key::Control + LControl + RControl
31
+ ctrl: "ctrl",
32
+ control: "ctrl",
33
+ lctrl: "ctrl",
34
+ lcontrol: "ctrl",
35
+ rctrl: "ctrl",
36
+ rcontrol: "ctrl",
37
+ // Key::Shift + LShift + RShift
38
+ shift: "shift",
39
+ lshift: "shift",
40
+ rshift: "shift",
41
+ // Key::Alt and Key::Option — distinct Rust variants but same keycode on
42
+ // darwin (kVK_Option). Collapse: cmd+alt+escape and cmd+option+escape
43
+ // both Force Quit.
44
+ alt: "alt",
45
+ option: "alt",
46
+ };
47
+
48
+ /** Sort order for canonicals. ctrl < alt < shift < meta. */
49
+ const MODIFIER_ORDER = ["ctrl", "alt", "shift", "meta"];
50
+
51
+ /**
52
+ * Canonical-form entries only. Every modifier must be a CANONICAL_MODIFIER
53
+ * *value* (not key), modifiers must be in MODIFIER_ORDER, non-modifier last.
54
+ * The self-consistency test enforces this.
55
+ */
56
+ const BLOCKED_DARWIN = new Set([
57
+ "meta+q", // Cmd+Q — quit frontmost app
58
+ "shift+meta+q", // Cmd+Shift+Q — log out
59
+ "alt+meta+escape", // Cmd+Option+Esc — Force Quit dialog
60
+ "meta+tab", // Cmd+Tab — app switcher
61
+ "meta+space", // Cmd+Space — Spotlight
62
+ "ctrl+meta+q", // Ctrl+Cmd+Q — lock screen
63
+ ]);
64
+
65
+ const BLOCKED_WIN32 = new Set([
66
+ "ctrl+alt+delete", // Secure Attention Sequence
67
+ "alt+f4", // close window
68
+ "alt+tab", // window switcher
69
+ "meta+l", // Win+L — lock
70
+ "meta+d", // Win+D — show desktop
71
+ ]);
72
+
73
+ /**
74
+ * Partition into sorted-canonical modifiers and non-modifier keys.
75
+ * Shared by normalizeKeySequence (join for display) and isSystemKeyCombo
76
+ * (check mods+each-key to catch the cmd+q+a suffix bypass).
77
+ */
78
+ function partitionKeys(seq: string): { mods: string[]; keys: string[] } {
79
+ const parts = seq
80
+ .toLowerCase()
81
+ .split("+")
82
+ .map((p) => p.trim())
83
+ .filter(Boolean);
84
+ const mods: string[] = [];
85
+ const keys: string[] = [];
86
+ for (const p of parts) {
87
+ const canonical = CANONICAL_MODIFIER[p];
88
+ if (canonical !== undefined) {
89
+ mods.push(canonical);
90
+ } else {
91
+ keys.push(p);
92
+ }
93
+ }
94
+ // Dedupe: "cmd+command+q" → "meta+q", not "meta+meta+q".
95
+ const uniqueMods = [...new Set(mods)];
96
+ uniqueMods.sort(
97
+ (a, b) => MODIFIER_ORDER.indexOf(a) - MODIFIER_ORDER.indexOf(b),
98
+ );
99
+ return { mods: uniqueMods, keys };
100
+ }
101
+
102
+ /**
103
+ * Normalize "Cmd + Shift + Q" → "shift+meta+q": lowercase, trim, alias →
104
+ * canonical, dedupe, sort modifiers, non-modifiers last.
105
+ */
106
+ export function normalizeKeySequence(seq: string): string {
107
+ const { mods, keys } = partitionKeys(seq);
108
+ return [...mods, ...keys].join("+");
109
+ }
110
+
111
+ /**
112
+ * True if the sequence would fire a blocked OS shortcut.
113
+ *
114
+ * Checks mods + EACH non-modifier key individually, not just the full
115
+ * joined string. `cmd+q+a` → Rust presses Cmd, then Q (Cmd+Q fires here),
116
+ * then A. Exact-match against "meta+q+a" misses; checking "meta+q" and
117
+ * "meta+a" separately catches the Q.
118
+ *
119
+ * Modifiers-only sequences ("cmd+shift") are checked as-is — no key to
120
+ * pair with, and no blocklist entry is modifier-only, so this is a no-op
121
+ * that falls through to false. Covers the click-modifier case where
122
+ * `left_click(text="cmd")` is legitimate.
123
+ */
124
+ export function isSystemKeyCombo(
125
+ seq: string,
126
+ platform: "darwin" | "win32",
127
+ ): boolean {
128
+ const blocklist = platform === "darwin" ? BLOCKED_DARWIN : BLOCKED_WIN32;
129
+ const { mods, keys } = partitionKeys(seq);
130
+ const prefix = mods.length > 0 ? mods.join("+") + "+" : "";
131
+
132
+ // No non-modifier keys (e.g. "cmd+shift" as click-modifiers) — check the
133
+ // whole thing. Never matches (no blocklist entry is modifier-only) but
134
+ // keeps the contract simple: every call reaches a .has().
135
+ if (keys.length === 0) {
136
+ return blocklist.has(mods.join("+"));
137
+ }
138
+
139
+ // mods + each key. Any hit blocks the whole sequence.
140
+ for (const key of keys) {
141
+ if (blocklist.has(prefix + key)) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+
148
+ export const _test = {
149
+ CANONICAL_MODIFIER,
150
+ BLOCKED_DARWIN,
151
+ BLOCKED_WIN32,
152
+ MODIFIER_ORDER,
153
+ };
@@ -0,0 +1,313 @@
1
+ /**
2
+ * MCP server factory + session-context binder.
3
+ *
4
+ * Two entry points:
5
+ *
6
+ * `bindSessionContext` — the wrapper closure. Takes a `ComputerUseSessionContext`
7
+ * (getters + callbacks backed by host session state), returns a dispatcher.
8
+ * Reusable by both the MCP CallTool handler here AND Cowork's
9
+ * `InternalServerDefinition.handleToolCall` (which doesn't go through MCP).
10
+ * This replaces the duplicated wrapper closures in apps/desktop/…/serverDef.ts
11
+ * and the Gclm Code CLI's CU host wrapper — both did the same thing: build `ComputerUseOverrides`
12
+ * fresh from getters, call `handleToolCall`, stash screenshot, merge permissions.
13
+ *
14
+ * `createComputerUseMcpServer` — the Server object. When `context` is provided,
15
+ * the CallTool handler is real (uses `bindSessionContext`). When not, it's the
16
+ * legacy stub that returns a not-wired error. The tool-schema ListTools handler
17
+ * is the same either way.
18
+ */
19
+
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
+ import {
23
+ CallToolRequestSchema,
24
+ ListToolsRequestSchema,
25
+ } from "@modelcontextprotocol/sdk/types.js";
26
+
27
+ import type { ScreenshotResult } from "./executor.js";
28
+ import type { CuCallToolResult } from "./toolCalls.js";
29
+ import {
30
+ defersLockAcquire,
31
+ handleToolCall,
32
+ resetMouseButtonHeld,
33
+ } from "./toolCalls.js";
34
+ import { buildComputerUseTools } from "./tools.js";
35
+ import type {
36
+ AppGrant,
37
+ ComputerUseHostAdapter,
38
+ ComputerUseOverrides,
39
+ ComputerUseSessionContext,
40
+ CoordinateMode,
41
+ CuGrantFlags,
42
+ CuPermissionResponse,
43
+ } from "./types.js";
44
+ import { DEFAULT_GRANT_FLAGS } from "./types.js";
45
+
46
+ const DEFAULT_LOCK_HELD_MESSAGE =
47
+ "Another Gclm Code session is currently using the computer. Wait for that " +
48
+ "session to finish, or find a non-computer-use approach.";
49
+
50
+ /**
51
+ * Dedupe `granted` into `existing` on bundleId, spread truthy-only flags over
52
+ * defaults+existing. Truthy-only: a subsequent `request_access` that doesn't
53
+ * request clipboard can't revoke an earlier clipboard grant — revocation lives
54
+ * in a Settings page, not here.
55
+ *
56
+ * Same merge both hosts implemented independently today.
57
+ */
58
+ function mergePermissionResponse(
59
+ existing: readonly AppGrant[],
60
+ existingFlags: CuGrantFlags,
61
+ response: CuPermissionResponse,
62
+ ): { apps: AppGrant[]; flags: CuGrantFlags } {
63
+ const seen = new Set(existing.map((a) => a.bundleId));
64
+ const apps = [
65
+ ...existing,
66
+ ...response.granted.filter((g) => !seen.has(g.bundleId)),
67
+ ];
68
+ const truthyFlags = Object.fromEntries(
69
+ Object.entries(response.flags).filter(([, v]) => v === true),
70
+ );
71
+ const flags: CuGrantFlags = {
72
+ ...DEFAULT_GRANT_FLAGS,
73
+ ...existingFlags,
74
+ ...truthyFlags,
75
+ };
76
+ return { apps, flags };
77
+ }
78
+
79
+ /**
80
+ * Bind session state to a reusable dispatcher. The returned function is the
81
+ * wrapper closure: async lock gate → build overrides fresh → `handleToolCall`
82
+ * → stash screenshot → strip piggybacked fields.
83
+ *
84
+ * The last-screenshot blob is held in a closure cell here (not on `ctx`), so
85
+ * hosts don't need to guarantee `ctx` object identity across calls — they just
86
+ * need to hold onto the returned dispatcher. Cowork caches per
87
+ * `InternalServerContext` in a WeakMap; the CLI host constructs once at server creation.
88
+ */
89
+ export function bindSessionContext(
90
+ adapter: ComputerUseHostAdapter,
91
+ coordinateMode: CoordinateMode,
92
+ ctx: ComputerUseSessionContext,
93
+ ): (name: string, args: unknown) => Promise<CuCallToolResult> {
94
+ const { logger, serverName } = adapter;
95
+
96
+ // Screenshot blob persists here across calls — NOT on `ctx`. Hosts hold
97
+ // onto the returned dispatcher; that's the identity that matters.
98
+ let lastScreenshot: ScreenshotResult | undefined;
99
+
100
+ const wrapPermission = ctx.onPermissionRequest
101
+ ? async (
102
+ req: Parameters<NonNullable<typeof ctx.onPermissionRequest>>[0],
103
+ signal: AbortSignal,
104
+ ): Promise<CuPermissionResponse> => {
105
+ const response = await ctx.onPermissionRequest!(req, signal);
106
+ const { apps, flags } = mergePermissionResponse(
107
+ ctx.getAllowedApps(),
108
+ ctx.getGrantFlags(),
109
+ response,
110
+ );
111
+ logger.debug(
112
+ `[${serverName}] permission result: granted=${response.granted.length} denied=${response.denied.length}`,
113
+ );
114
+ ctx.onAllowedAppsChanged?.(apps, flags);
115
+ return response;
116
+ }
117
+ : undefined;
118
+
119
+ const wrapTeachPermission = ctx.onTeachPermissionRequest
120
+ ? async (
121
+ req: Parameters<NonNullable<typeof ctx.onTeachPermissionRequest>>[0],
122
+ signal: AbortSignal,
123
+ ): Promise<CuPermissionResponse> => {
124
+ const response = await ctx.onTeachPermissionRequest!(req, signal);
125
+ logger.debug(
126
+ `[${serverName}] teach permission result: granted=${response.granted.length} denied=${response.denied.length}`,
127
+ );
128
+ // Teach doesn't request grant flags — preserve existing.
129
+ const { apps } = mergePermissionResponse(
130
+ ctx.getAllowedApps(),
131
+ ctx.getGrantFlags(),
132
+ response,
133
+ );
134
+ ctx.onAllowedAppsChanged?.(apps, {
135
+ ...DEFAULT_GRANT_FLAGS,
136
+ ...ctx.getGrantFlags(),
137
+ });
138
+ return response;
139
+ }
140
+ : undefined;
141
+
142
+ return async (name, args) => {
143
+ // ─── Async lock gate ─────────────────────────────────────────────────
144
+ // Replaces the sync Gate-3 in `handleToolCall` — we pass
145
+ // `checkCuLock: undefined` below so it no-ops. Hosts with
146
+ // cross-process locks (O_EXCL file) await the real primitive here
147
+ // instead of pre-computing + feeding a fake sync result.
148
+ if (ctx.checkCuLock) {
149
+ const lock = await ctx.checkCuLock();
150
+ if (lock.holder !== undefined && !lock.isSelf) {
151
+ const text =
152
+ ctx.formatLockHeldMessage?.(lock.holder) ?? DEFAULT_LOCK_HELD_MESSAGE;
153
+ return {
154
+ content: [{ type: "text", text }],
155
+ isError: true,
156
+ telemetry: { error_kind: "cu_lock_held" },
157
+ };
158
+ }
159
+ if (lock.holder === undefined && !defersLockAcquire(name)) {
160
+ await ctx.acquireCuLock?.();
161
+ // Re-check: the awaits above yield the microtask queue, so another
162
+ // session's check+acquire can interleave with ours. Hosts where
163
+ // acquire is a no-op when already held (Cowork's CuLockManager) give
164
+ // no signal that we lost — verify we're now the holder before
165
+ // proceeding. The CLI's O_EXCL file lock would surface this as a throw from
166
+ // acquire instead; this re-check is a belt-and-suspenders for that
167
+ // path too.
168
+ const recheck = await ctx.checkCuLock();
169
+ if (recheck.holder !== undefined && !recheck.isSelf) {
170
+ const text =
171
+ ctx.formatLockHeldMessage?.(recheck.holder) ??
172
+ DEFAULT_LOCK_HELD_MESSAGE;
173
+ return {
174
+ content: [{ type: "text", text }],
175
+ isError: true,
176
+ telemetry: { error_kind: "cu_lock_held" },
177
+ };
178
+ }
179
+ // Fresh holder → any prior session's mouseButtonHeld is stale.
180
+ // Mirrors what Gate-3 does on the acquire branch. After the
181
+ // re-check so we only clear module state when we actually won.
182
+ resetMouseButtonHeld();
183
+ }
184
+ }
185
+
186
+ // ─── Build overrides fresh ───────────────────────────────────────────
187
+ // Blob-first; dims-fallback with base64:"" when the closure cell is
188
+ // unset (cross-respawn). scaleCoord reads dims; pixelCompare sees "" →
189
+ // isEmpty → skip.
190
+ const dimsFallback = lastScreenshot
191
+ ? undefined
192
+ : ctx.getLastScreenshotDims?.();
193
+
194
+ // Per-call AbortController for dialog dismissal. Aborted in `finally` —
195
+ // if handleToolCall finishes (MCP timeout, throw) before the user
196
+ // answers, the host's dialog handler sees the abort and tears down.
197
+ const dialogAbort = new AbortController();
198
+
199
+ const overrides: ComputerUseOverrides = {
200
+ allowedApps: [...ctx.getAllowedApps()],
201
+ grantFlags: ctx.getGrantFlags(),
202
+ userDeniedBundleIds: ctx.getUserDeniedBundleIds(),
203
+ coordinateMode,
204
+ selectedDisplayId: ctx.getSelectedDisplayId(),
205
+ displayPinnedByModel: ctx.getDisplayPinnedByModel?.(),
206
+ displayResolvedForApps: ctx.getDisplayResolvedForApps?.(),
207
+ lastScreenshot:
208
+ lastScreenshot ??
209
+ (dimsFallback ? { ...dimsFallback, base64: "" } : undefined),
210
+ onPermissionRequest: wrapPermission
211
+ ? (req) => wrapPermission(req, dialogAbort.signal)
212
+ : undefined,
213
+ onTeachPermissionRequest: wrapTeachPermission
214
+ ? (req) => wrapTeachPermission(req, dialogAbort.signal)
215
+ : undefined,
216
+ onAppsHidden: ctx.onAppsHidden,
217
+ getClipboardStash: ctx.getClipboardStash,
218
+ onClipboardStashChanged: ctx.onClipboardStashChanged,
219
+ onResolvedDisplayUpdated: ctx.onResolvedDisplayUpdated,
220
+ onDisplayPinned: ctx.onDisplayPinned,
221
+ onDisplayResolvedForApps: ctx.onDisplayResolvedForApps,
222
+ onTeachModeActivated: ctx.onTeachModeActivated,
223
+ onTeachStep: ctx.onTeachStep,
224
+ onTeachWorking: ctx.onTeachWorking,
225
+ getTeachModeActive: ctx.getTeachModeActive,
226
+ // Undefined → handleToolCall's sync Gate-3 no-ops. The async gate
227
+ // above already ran.
228
+ checkCuLock: undefined,
229
+ acquireCuLock: undefined,
230
+ isAborted: ctx.isAborted,
231
+ };
232
+
233
+ logger.debug(
234
+ `[${serverName}] tool=${name} allowedApps=${overrides.allowedApps.length} coordMode=${coordinateMode}`,
235
+ );
236
+
237
+ // ─── Dispatch ────────────────────────────────────────────────────────
238
+ try {
239
+ const result = await handleToolCall(adapter, name, args, overrides);
240
+
241
+ if (result.screenshot) {
242
+ lastScreenshot = result.screenshot;
243
+ const { base64: _blob, ...dims } = result.screenshot;
244
+ logger.debug(`[${serverName}] screenshot dims: ${JSON.stringify(dims)}`);
245
+ ctx.onScreenshotCaptured?.(dims);
246
+ }
247
+
248
+ return result;
249
+ } finally {
250
+ dialogAbort.abort();
251
+ }
252
+ };
253
+ }
254
+
255
+ export function createComputerUseMcpServer(
256
+ adapter: ComputerUseHostAdapter,
257
+ coordinateMode: CoordinateMode,
258
+ context?: ComputerUseSessionContext,
259
+ ): Server {
260
+ const { serverName, logger } = adapter;
261
+
262
+ const server = new Server(
263
+ { name: serverName, version: "0.1.3" },
264
+ { capabilities: { tools: {}, logging: {} } },
265
+ );
266
+
267
+ const tools = buildComputerUseTools(
268
+ adapter.executor.capabilities,
269
+ coordinateMode,
270
+ );
271
+
272
+ server.setRequestHandler(ListToolsRequestSchema, async () =>
273
+ adapter.isDisabled() ? { tools: [] } : { tools },
274
+ );
275
+
276
+ if (context) {
277
+ const dispatch = bindSessionContext(adapter, coordinateMode, context);
278
+ server.setRequestHandler(
279
+ CallToolRequestSchema,
280
+ async (request): Promise<CallToolResult> => {
281
+ const { screenshot: _s, telemetry: _t, ...result } = await dispatch(
282
+ request.params.name,
283
+ request.params.arguments ?? {},
284
+ );
285
+ return result;
286
+ },
287
+ );
288
+ return server;
289
+ }
290
+
291
+ // Legacy: no context → stub handler. Reached only if something calls the
292
+ // server over MCP transport WITHOUT going through a binder (a wiring
293
+ // regression). Clear error instead of silent failure.
294
+ server.setRequestHandler(
295
+ CallToolRequestSchema,
296
+ async (request): Promise<CallToolResult> => {
297
+ logger.warn(
298
+ `[${serverName}] tool call "${request.params.name}" reached the stub handler — no session context bound. Per-session state unavailable.`,
299
+ );
300
+ return {
301
+ content: [
302
+ {
303
+ type: "text",
304
+ text: "This computer-use server instance is not wired to a session. Per-session app permissions are not available on this code path.",
305
+ },
306
+ ],
307
+ isError: true,
308
+ };
309
+ },
310
+ );
311
+
312
+ return server;
313
+ }