gogcli-mcp 2.19.0 → 2.19.2

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
10
- "version": "2.19.0"
10
+ "version": "2.19.2"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli",
16
16
  "source": "./",
17
17
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
18
- "version": "2.19.0",
18
+ "version": "2.19.2",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
3
  "displayName": "gogcli",
4
- "version": "2.19.0",
4
+ "version": "2.19.2",
5
5
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31119,6 +31119,13 @@ function isGogFileArg(arg) {
31119
31119
  return typeof arg !== "string";
31120
31120
  }
31121
31121
  var runExecutor = new AsyncLocalStorage();
31122
+ var defaultExecutor;
31123
+ function setDefaultGogExecutor(executor) {
31124
+ defaultExecutor = executor ? { executor } : void 0;
31125
+ }
31126
+ function activeExecutor() {
31127
+ return runExecutor.getStore() ?? defaultExecutor;
31128
+ }
31122
31129
  var TIMEOUT_MS = 3e4;
31123
31130
  function readonlyEnvEnabled() {
31124
31131
  return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
@@ -31279,7 +31286,7 @@ async function run(args, options = {}) {
31279
31286
  const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
31280
31287
  const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31281
31288
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
31282
- const store = runExecutor.getStore();
31289
+ const store = activeExecutor();
31283
31290
  try {
31284
31291
  let output;
31285
31292
  if (spawner) {
@@ -31296,7 +31303,7 @@ async function run(args, options = {}) {
31296
31303
  }
31297
31304
  async function runBinary(args, options = {}) {
31298
31305
  const { account, spawner, timeout, readonly: readonly2 = false } = options;
31299
- if (!spawner && runExecutor.getStore()) {
31306
+ if (!spawner && activeExecutor()) {
31300
31307
  throw new Error(
31301
31308
  "Raw byte retrieval is not available over the hosted connector (its transport is text-only). Use the text-extraction path instead, or run the local stdio server to fetch bytes."
31302
31309
  );
@@ -33072,7 +33079,7 @@ function registerTasksTools(server) {
33072
33079
  }
33073
33080
 
33074
33081
  // src/server.ts
33075
- var VERSION = true ? "2.19.0" : "0.0.0";
33082
+ var VERSION = true ? "2.19.2" : "0.0.0";
33076
33083
  var BASE_TOOL_REGISTRARS = [
33077
33084
  registerApiTools,
33078
33085
  registerAuthTools,
@@ -33144,7 +33151,7 @@ function useRemoteGogRunner(env = process.env) {
33144
33151
  const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
33145
33152
  const key = readEnvVar("GOG_RUNNER_KEY", { env });
33146
33153
  if (!endpoint || !key) return false;
33147
- runExecutor.enterWith({ executor: makeFlyExecutor(endpoint.replace(/\/+$/, ""), key) });
33154
+ setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ""), key));
33148
33155
  return true;
33149
33156
  }
33150
33157
 
package/dist/lib.js CHANGED
@@ -23016,6 +23016,13 @@ function isGogFileArg(arg) {
23016
23016
  return typeof arg !== "string";
23017
23017
  }
23018
23018
  var runExecutor = new AsyncLocalStorage();
23019
+ var defaultExecutor;
23020
+ function setDefaultGogExecutor(executor) {
23021
+ defaultExecutor = executor ? { executor } : void 0;
23022
+ }
23023
+ function activeExecutor() {
23024
+ return runExecutor.getStore() ?? defaultExecutor;
23025
+ }
23019
23026
  var TIMEOUT_MS = 3e4;
23020
23027
  var MIN_GOG_VERSION = "0.34.1";
23021
23028
  function readonlyEnvEnabled() {
@@ -23177,7 +23184,7 @@ async function run(args, options = {}) {
23177
23184
  const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
23178
23185
  const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
23179
23186
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
23180
- const store = runExecutor.getStore();
23187
+ const store = activeExecutor();
23181
23188
  try {
23182
23189
  let output;
23183
23190
  if (spawner) {
@@ -23194,7 +23201,7 @@ async function run(args, options = {}) {
23194
23201
  }
23195
23202
  async function runBinary(args, options = {}) {
23196
23203
  const { account, spawner, timeout, readonly: readonly2 = false } = options;
23197
- if (!spawner && runExecutor.getStore()) {
23204
+ if (!spawner && activeExecutor()) {
23198
23205
  throw new Error(
23199
23206
  "Raw byte retrieval is not available over the hosted connector (its transport is text-only). Use the text-extraction path instead, or run the local stdio server to fetch bytes."
23200
23207
  );
@@ -24978,7 +24985,7 @@ function registerTasksTools(server) {
24978
24985
  }
24979
24986
 
24980
24987
  // src/server.ts
24981
- var VERSION = true ? "2.19.0" : "0.0.0";
24988
+ var VERSION = true ? "2.19.2" : "0.0.0";
24982
24989
  var BASE_TOOL_REGISTRARS = [
24983
24990
  registerApiTools,
24984
24991
  registerAuthTools,
@@ -25050,7 +25057,7 @@ function useRemoteGogRunner(env = process.env) {
25050
25057
  const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
25051
25058
  const key = readEnvVar("GOG_RUNNER_KEY", { env });
25052
25059
  if (!endpoint || !key) return false;
25053
- runExecutor.enterWith({ executor: makeFlyExecutor(endpoint.replace(/\/+$/, ""), key) });
25060
+ setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ""), key));
25054
25061
  return true;
25055
25062
  }
25056
25063
  export {
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp",
5
5
  "display_name": "gogcli",
6
- "version": "2.19.0",
6
+ "version": "2.19.2",
7
7
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "2.19.0",
3
+ "version": "2.19.2",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp",
5
5
  "description": "MCP server wrapping gogcli for Google service access",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp"
9
9
  },
10
- "version": "2.19.0",
10
+ "version": "2.19.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.19.0",
15
+ "version": "2.19.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -1,5 +1,5 @@
1
1
  import { readEnvVar } from '@chrischall/mcp-utils';
2
- import { runExecutor } from './runner.js';
2
+ import { setDefaultGogExecutor } from './runner.js';
3
3
  import { makeFlyExecutor } from './connector-runtime.js';
4
4
 
5
5
  /**
@@ -19,16 +19,26 @@ import { makeFlyExecutor } from './connector-runtime.js';
19
19
  * executes remotely; leave either unset and nothing changes, which is what
20
20
  * keeps every existing local install on the binary it already has.
21
21
  *
22
- * ## Why `enterWith` and not `run`
22
+ * ## Why a process-wide default and not the AsyncLocalStorage
23
23
  *
24
- * `runExecutor` is an AsyncLocalStorage. The Worker wraps each REQUEST in
25
- * `runExecutor.run(...)` because a Worker isolate serves many of them and the
26
- * executor differs per user. A stdio process is one user for its whole life,
27
- * and its tool calls arrive later as I/O callbacks — which would NOT inherit a
28
- * store established by a `run()` that had already returned. `enterWith` sets it
29
- * for the remainder of this execution and everything descending from it, which
30
- * is the whole process. Using `run()` here would look correct and then fall
31
- * back to spawning on the first actual tool call.
24
+ * `runExecutor` is an AsyncLocalStorage, and the Worker wraps each REQUEST in
25
+ * `runExecutor.run(...)` because one isolate serves many callers whose backend
26
+ * credentials differ. A stdio process is the opposite: one backend for its
27
+ * whole life.
28
+ *
29
+ * This used to reach for `enterWith` on the theory that it "sets the store for
30
+ * the whole process". It does not. `enterWith` sets the store on the async
31
+ * resource that is CURRENT when it runs here, module evaluation — and the
32
+ * tool calls arrive later as I/O events on the transport's own resources, which
33
+ * do not descend from that. So `getStore()` was undefined at exactly the moment
34
+ * `run()` asked, and the seam reverted to spawning a binary the host does not
35
+ * have. Every hosted gog MCP answered "gog executable not found" while pointed
36
+ * at a healthy backend, and the unit test missed it because a test that calls
37
+ * this function itself awaits inside the resource it just mutated.
38
+ *
39
+ * A process-lifetime value is not a scoped value, so it does not live in a
40
+ * scope: `setDefaultGogExecutor` holds it, and a per-request store still beats
41
+ * it (runner.ts `activeExecutor`) so the Worker path is unchanged.
32
42
  *
33
43
  * Call before the server starts, so no tool can be serviced ahead of it.
34
44
  */
@@ -44,6 +54,6 @@ export function useRemoteGogRunner(env: NodeJS.ProcessEnv = process.env): boolea
44
54
  // nothing — either alone is a misconfiguration, and silently spawning
45
55
  // instead would hide it until someone wondered why the binary was needed.
46
56
  if (!endpoint || !key) return false;
47
- runExecutor.enterWith({ executor: makeFlyExecutor(endpoint.replace(/\/+$/, ''), key) });
57
+ setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ''), key));
48
58
  return true;
49
59
  }
package/src/runner.ts CHANGED
@@ -50,6 +50,40 @@ export type GogExecutor = (
50
50
  // `runExecutor.run({ executor }, ...)`; unset, `run()` falls back to spawning.
51
51
  export const runExecutor = new AsyncLocalStorage<{ executor: GogExecutor }>();
52
52
 
53
+ /**
54
+ * The PROCESS-WIDE executor, for a host that has exactly one backend for the
55
+ * whole process — a stdio bin pointed at a Fly runner (`useRemoteGogRunner`).
56
+ *
57
+ * It exists because AsyncLocalStorage cannot express that. `enterWith` sets the
58
+ * store on the async resource that is current when it runs, and a bin runs it
59
+ * during module evaluation; the tool calls arrive later as I/O events on the
60
+ * transport's own resources, which are not descendants of that evaluation, so
61
+ * `getStore()` is undefined exactly where it is needed. That is not a bug in
62
+ * `enterWith` — a process-lifetime default is simply not a scoped value, and
63
+ * storing it in a scope meant the seam silently reverted to spawning a binary
64
+ * the host does not have.
65
+ *
66
+ * A per-request store still WINS over this (see `activeExecutor`), because the
67
+ * Worker serves many callers from one isolate and each has its own backend
68
+ * credential; this is the fallback for the one-backend case, never a second
69
+ * answer to "whose backend is this".
70
+ */
71
+ let defaultExecutor: { executor: GogExecutor } | undefined;
72
+
73
+ /** Install the process-wide executor. Passing undefined clears it (tests). */
74
+ export function setDefaultGogExecutor(executor: GogExecutor | undefined): void {
75
+ defaultExecutor = executor ? { executor } : undefined;
76
+ }
77
+
78
+ /**
79
+ * Whose executor applies right now: the request's, else the process's, else
80
+ * none (meaning `run()` spawns the local binary). Both call sites ask through
81
+ * here so they can never disagree about which of the three it is.
82
+ */
83
+ function activeExecutor(): { executor: GogExecutor } | undefined {
84
+ return runExecutor.getStore() ?? defaultExecutor;
85
+ }
86
+
53
87
  export interface RunOptions {
54
88
  account?: string;
55
89
  spawner?: Spawner;
@@ -328,7 +362,7 @@ export async function run(args: GogArg[], options: RunOptions = {}): Promise<str
328
362
  // successful `gog auth tokens` (or any command echoing a credential) would
329
363
  // otherwise return raw Google tokens (ya29.…/1//…) into model context, where
330
364
  // a sibling tool (gog_gmail_send) could exfiltrate them.
331
- const store = runExecutor.getStore();
365
+ const store = activeExecutor();
332
366
  try {
333
367
  let output: string;
334
368
  if (spawner) {
@@ -358,7 +392,7 @@ export async function runBinary(args: GogArg[], options: RunOptions = {}): Promi
358
392
  // An injected spawner is the stdio/test path and always wins. Otherwise, if an
359
393
  // ambient forward executor is installed (the Worker/Fly connector), refuse:
360
394
  // its text-only transport can't carry bytes intact.
361
- if (!spawner && runExecutor.getStore()) {
395
+ if (!spawner && activeExecutor()) {
362
396
  throw new Error(
363
397
  'Raw byte retrieval is not available over the hosted connector (its transport is text-only). ' +
364
398
  'Use the text-extraction path instead, or run the local stdio server to fetch bytes.',
package/src/worker.ts CHANGED
@@ -38,7 +38,7 @@ import { gogAuth, type GogProps } from './connector-auth.js';
38
38
  // connector with all ~360 tools at once. Add whichever paths you want as separate
39
39
  // connectors in claude.ai (each authorizes with the same connector key).
40
40
 
41
- const VERSION = '2.19.0'; // x-release-please-version
41
+ const VERSION = '2.19.2'; // x-release-please-version
42
42
 
43
43
  // Build an McpAgent subclass whose init() registers `registrars` onto its server,
44
44
  // each handler wrapped in the ALS scope carrying the per-session Fly executor.
@@ -1,15 +1,32 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import { describe, it, expect, vi, afterEach } from 'vitest';
2
5
  import { useRemoteGogRunner } from '../src/remote-runner.js';
3
- import { runExecutor } from '../src/runner.js';
6
+ import { run, runBinary, runExecutor, setDefaultGogExecutor } from '../src/runner.js';
4
7
 
5
8
  /**
6
9
  * The whole point of this seam is that a host without the `gog` binary can
7
- * still serve. Two ways it silently fails: a half-configured env that falls
8
- * back to spawning, and using `run()` instead of `enterWith()` so the store is
9
- * gone by the time a tool call arrives.
10
+ * still serve. Every way it has silently failed so far: a half-configured env
11
+ * that falls back to spawning; a bin that never calls `useRemoteGogRunner()`;
12
+ * and the one that shipped in 2.19.x — parking the executor in an
13
+ * AsyncLocalStorage that the tool calls never inherit, so the seam reported
14
+ * success and then spawned anyway.
15
+ *
16
+ * They share a shape worth naming: every one of them fails by falling back to
17
+ * the local binary, which on a laptop is invisible and on a host is total. So
18
+ * these tests assert on where a call ACTUALLY went, never on whether the wiring
19
+ * step was performed.
10
20
  */
11
21
 
12
- afterEach(() => vi.unstubAllGlobals());
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ vi.unstubAllEnvs();
25
+ // The executor is process-wide BY DESIGN now, so unlike an ALS store it does
26
+ // not unwind when a test ends — without this, the first test to install one
27
+ // would silently hand it to every test that ran after it.
28
+ setDefaultGogExecutor(undefined);
29
+ });
13
30
 
14
31
  describe('useRemoteGogRunner', () => {
15
32
  it('does nothing unless BOTH variables are set, so local installs are untouched', () => {
@@ -30,24 +47,115 @@ describe('useRemoteGogRunner', () => {
30
47
  expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'null', GOG_RUNNER_KEY: 'k' })).toBe(false);
31
48
  });
32
49
 
33
- it('installs an executor that survives into a LATER async callback', async () => {
34
- // The real failure mode this guards: with `run()` the store would be gone
35
- // by the time a tool call arrives as an I/O callback, and the server would
36
- // quietly go back to spawning a binary that is not installed.
50
+ it('addresses and authenticates the backend correctly', async () => {
51
+ // Asserted THROUGH run(), not by reading whichever slot the executor is
52
+ // parked in. The previous version of this test fetched
53
+ // `runExecutor.getStore()` and called what it found, so it was really a
54
+ // test that `enterWith` had been called — which stayed green while the
55
+ // shipped bins routed every call to a local binary (see the next test).
37
56
  const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'ok' }), { status: 200 }));
38
57
  vi.stubGlobal('fetch', fetchMock);
58
+ vi.stubEnv('GOG_PATH', '/nonexistent/gog');
39
59
 
40
60
  expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test/', GOG_RUNNER_KEY: 'secret' })).toBe(true);
41
61
 
42
62
  // Cross a macrotask boundary, the way a stdio tool call does.
43
63
  await new Promise((r) => setTimeout(r, 0));
44
- const store = runExecutor.getStore();
45
- expect(store?.executor).toBeTypeOf('function');
64
+ expect(await run(['--version'])).toBe('ok');
46
65
 
47
- await store!.executor(['--version'], {});
48
66
  const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
49
67
  // Trailing slash trimmed, so the endpoint is never `…//run`.
50
68
  expect(url).toBe('https://r.test/run');
51
69
  expect((init.headers as Record<string, string>).Authorization).toBe('Bearer secret');
52
70
  });
71
+
72
+ it('still routes remotely from a context the store never reached', async () => {
73
+ // THE ONE THE TEST ABOVE CANNOT CATCH, and it shipped broken.
74
+ //
75
+ // `enterWith` mutates the store of the async resource that is current when
76
+ // it runs. The test above calls useRemoteGogRunner() itself, so everything
77
+ // it awaits afterwards is a descendant of that resource and inherits the
78
+ // store — it passes whether or not the seam works where it matters.
79
+ //
80
+ // A bin does something different: it calls useRemoteGogRunner() while the
81
+ // ES module is evaluating, and then `await runMcp(...)` starts serving. The
82
+ // tool calls arrive later as I/O events on the transport's own async
83
+ // resources, which are NOT descendants of module evaluation, so
84
+ // `getStore()` is undefined by the time `run()` asks. Verified against the
85
+ // published 2.19.1 bin: `useRemoteGogRunner()` returned true, and at call
86
+ // time the store was undefined — every hosted gog MCP answered
87
+ // "gog executable not found" while pointed at a healthy backend.
88
+ //
89
+ // `runExecutor.exit()` is that condition exactly, and deterministically:
90
+ // run the call where the ALS store does not apply.
91
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'remote' }), { status: 200 }));
92
+ vi.stubGlobal('fetch', fetchMock);
93
+ // So the spawn fallback cannot accidentally succeed on a machine that has
94
+ // gog installed and make this pass for the wrong reason.
95
+ vi.stubEnv('GOG_PATH', '/nonexistent/gog');
96
+
97
+ expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
98
+
99
+ const output = await runExecutor.exit(() => run(['auth', 'status']));
100
+ expect(output).toBe('remote');
101
+ expect(fetchMock).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('lets a per-request store beat the process-wide default', async () => {
105
+ // The Worker path scopes every request in `runExecutor.run({ executor })`
106
+ // because one isolate serves many callers (connector-runtime.ts). A
107
+ // process-wide default must never shadow that: it is the fallback for a
108
+ // stdio bin, not a second answer to "whose backend is this".
109
+ const perRequest = vi.fn(async () => 'per-request');
110
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ stdout: 'default' }), { status: 200 })));
111
+ expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
112
+
113
+ const output = await runExecutor.run({ executor: perRequest }, () => run(['auth', 'status']));
114
+ expect(output).toBe('per-request');
115
+ expect(perRequest).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it('still refuses binary retrieval when the default is a remote executor', async () => {
119
+ // runBinary's guard asks the same question ("is a text-only forward
120
+ // installed?") and must read the same answer, or a hosted bin would fall
121
+ // through to spawning for bytes while every text call went remote.
122
+ vi.stubEnv('GOG_PATH', '/nonexistent/gog');
123
+ expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
124
+
125
+ await expect(runExecutor.exit(() => runBinary(['drive', 'read']))).rejects.toThrow(/text-only/);
126
+ });
127
+ });
128
+
129
+ /**
130
+ * The seam is opt-in PER BIN — `useRemoteGogRunner()` is a call in each
131
+ * package's `index.ts`, and a package that omits it spawns the binary no
132
+ * matter what the host sets. That shipped: 2.19.0 wired base, sheets, docs,
133
+ * drive and gmail, and left calendar, classroom, contacts and slides behind,
134
+ * so those four answered every tool call on mcp-host with "gog executable not
135
+ * found" — a host with no binary and no way to ask for the backend.
136
+ *
137
+ * Nothing caught it because each package's suite covers its TOOLS, and
138
+ * `src/index.ts` is excluded from the coverage gate in every package (it is
139
+ * the bin: it boots a server and cannot be imported under test). So this
140
+ * asserts on the source text instead, across the whole workspace — the one
141
+ * check that scales to the next sub-package, which will otherwise be added by
142
+ * copying an index.ts that predates the seam.
143
+ */
144
+ describe('every package bin', () => {
145
+ const packagesDir = fileURLToPath(new URL('../../', import.meta.url));
146
+ const bins = readdirSync(packagesDir)
147
+ .filter((name) => name.startsWith('gogcli-mcp'))
148
+ .map((name) => [name, join(packagesDir, name, 'src', 'index.ts')] as const);
149
+
150
+ it('finds every package (guards the glob itself against silently matching nothing)', () => {
151
+ expect(bins.length).toBeGreaterThanOrEqual(9);
152
+ });
153
+
154
+ it.each(bins)('%s installs the remote executor before starting the server', (_name, path) => {
155
+ const source = readFileSync(path, 'utf8');
156
+ expect(source).toContain('useRemoteGogRunner()');
157
+ // Order matters as much as presence: `runMcp` starts serving, so a call
158
+ // placed after it could be beaten by a tool call and fall back to spawning.
159
+ expect(source.indexOf('useRemoteGogRunner()')).toBeLessThan(source.indexOf('runMcp({'));
160
+ });
53
161
  });