gogcli-mcp 2.0.7 → 2.0.8

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.0.7"
10
+ "version": "2.0.8"
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.0.7",
18
+ "version": "2.0.8",
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.0.7",
4
+ "version": "2.0.8",
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
@@ -8415,8 +8415,8 @@ function emoji() {
8415
8415
  }
8416
8416
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
8417
8417
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
8418
- var mac = (delimiter) => {
8419
- const escapedDelim = escapeRegex(delimiter ?? ":");
8418
+ var mac = (delimiter2) => {
8419
+ const escapedDelim = escapeRegex(delimiter2 ?? ":");
8420
8420
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
8421
8421
  };
8422
8422
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -30863,12 +30863,34 @@ var EMPTY_COMPLETION_RESULT = {
30863
30863
 
30864
30864
  // src/runner.ts
30865
30865
  import { spawn } from "node:child_process";
30866
+ import { delimiter } from "node:path";
30866
30867
  var TIMEOUT_MS = 3e4;
30867
30868
  function envOrUndefined(key) {
30868
30869
  const value = process.env[key];
30869
30870
  if (!value || value.startsWith("${")) return void 0;
30870
30871
  return value;
30871
30872
  }
30873
+ function augmentedPath() {
30874
+ const home = process.env.HOME;
30875
+ const candidates = [
30876
+ process.env.PATH ?? "",
30877
+ "/opt/homebrew/bin",
30878
+ "/usr/local/bin",
30879
+ home ? `${home}/.local/bin` : "",
30880
+ home ? `${home}/go/bin` : ""
30881
+ ];
30882
+ const seen = /* @__PURE__ */ new Set();
30883
+ const parts = [];
30884
+ for (const c of candidates) {
30885
+ if (!c) continue;
30886
+ for (const dir of c.split(delimiter)) {
30887
+ if (!dir || seen.has(dir)) continue;
30888
+ seen.add(dir);
30889
+ parts.push(dir);
30890
+ }
30891
+ }
30892
+ return parts.join(delimiter);
30893
+ }
30872
30894
  function formatTimeout(ms) {
30873
30895
  const seconds = Math.round(ms / 1e3);
30874
30896
  if (seconds >= 60) {
@@ -30891,7 +30913,8 @@ async function run(args, options = {}) {
30891
30913
  const effectiveTimeout = timeout ?? TIMEOUT_MS;
30892
30914
  return new Promise((resolve, reject) => {
30893
30915
  const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
30894
- const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: cleanEnv });
30916
+ const childEnv = { ...cleanEnv, PATH: augmentedPath() };
30917
+ const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
30895
30918
  const stdoutChunks = [];
30896
30919
  const stderrChunks = [];
30897
30920
  let settled = false;
@@ -30926,6 +30949,12 @@ async function run(args, options = {}) {
30926
30949
  clearTimeout(timer);
30927
30950
  if (settled) return;
30928
30951
  settled = true;
30952
+ if (err.code === "ENOENT") {
30953
+ reject(new Error(
30954
+ "gog executable not found. Install gogcli (https://github.com/steipete/gogcli) or set GOG_PATH in your MCP client config to the absolute binary path (run `which gog` in a terminal to find it)."
30955
+ ));
30956
+ return;
30957
+ }
30929
30958
  reject(err);
30930
30959
  });
30931
30960
  });
@@ -32123,7 +32152,7 @@ function registerTasksTools(server2) {
32123
32152
  }
32124
32153
 
32125
32154
  // src/server.ts
32126
- var VERSION = true ? "2.0.7" : "0.0.0";
32155
+ var VERSION = true ? "2.0.8" : "0.0.0";
32127
32156
  function createServer(options) {
32128
32157
  return new McpServer({
32129
32158
  name: options?.name ?? "gogcli",
package/dist/lib.js CHANGED
@@ -12044,8 +12044,8 @@ function emoji() {
12044
12044
  }
12045
12045
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
12046
12046
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
12047
- var mac = (delimiter) => {
12048
- const escapedDelim = escapeRegex(delimiter ?? ":");
12047
+ var mac = (delimiter2) => {
12048
+ const escapedDelim = escapeRegex(delimiter2 ?? ":");
12049
12049
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
12050
12050
  };
12051
12051
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -30770,12 +30770,34 @@ var EMPTY_COMPLETION_RESULT = {
30770
30770
 
30771
30771
  // src/runner.ts
30772
30772
  import { spawn } from "node:child_process";
30773
+ import { delimiter } from "node:path";
30773
30774
  var TIMEOUT_MS = 3e4;
30774
30775
  function envOrUndefined(key) {
30775
30776
  const value = process.env[key];
30776
30777
  if (!value || value.startsWith("${")) return void 0;
30777
30778
  return value;
30778
30779
  }
30780
+ function augmentedPath() {
30781
+ const home = process.env.HOME;
30782
+ const candidates = [
30783
+ process.env.PATH ?? "",
30784
+ "/opt/homebrew/bin",
30785
+ "/usr/local/bin",
30786
+ home ? `${home}/.local/bin` : "",
30787
+ home ? `${home}/go/bin` : ""
30788
+ ];
30789
+ const seen = /* @__PURE__ */ new Set();
30790
+ const parts = [];
30791
+ for (const c of candidates) {
30792
+ if (!c) continue;
30793
+ for (const dir of c.split(delimiter)) {
30794
+ if (!dir || seen.has(dir)) continue;
30795
+ seen.add(dir);
30796
+ parts.push(dir);
30797
+ }
30798
+ }
30799
+ return parts.join(delimiter);
30800
+ }
30779
30801
  function formatTimeout(ms) {
30780
30802
  const seconds = Math.round(ms / 1e3);
30781
30803
  if (seconds >= 60) {
@@ -30798,7 +30820,8 @@ async function run(args, options = {}) {
30798
30820
  const effectiveTimeout = timeout ?? TIMEOUT_MS;
30799
30821
  return new Promise((resolve, reject) => {
30800
30822
  const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
30801
- const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: cleanEnv });
30823
+ const childEnv = { ...cleanEnv, PATH: augmentedPath() };
30824
+ const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
30802
30825
  const stdoutChunks = [];
30803
30826
  const stderrChunks = [];
30804
30827
  let settled = false;
@@ -30833,6 +30856,12 @@ async function run(args, options = {}) {
30833
30856
  clearTimeout(timer);
30834
30857
  if (settled) return;
30835
30858
  settled = true;
30859
+ if (err.code === "ENOENT") {
30860
+ reject(new Error(
30861
+ "gog executable not found. Install gogcli (https://github.com/steipete/gogcli) or set GOG_PATH in your MCP client config to the absolute binary path (run `which gog` in a terminal to find it)."
30862
+ ));
30863
+ return;
30864
+ }
30836
30865
  reject(err);
30837
30866
  });
30838
30867
  });
@@ -32030,7 +32059,7 @@ function registerTasksTools(server) {
32030
32059
  }
32031
32060
 
32032
32061
  // src/server.ts
32033
- var VERSION = true ? "2.0.7" : "0.0.0";
32062
+ var VERSION = true ? "2.0.8" : "0.0.0";
32034
32063
  function createServer(options) {
32035
32064
  return new McpServer({
32036
32065
  name: options?.name ?? "gogcli",
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.0.7",
6
+ "version": "2.0.8",
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.0.7",
3
+ "version": "2.0.8",
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.0.7",
10
+ "version": "2.0.8",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.0.7",
15
+ "version": "2.0.8",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
package/src/runner.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import type { ChildProcess } from 'node:child_process';
3
+ import { delimiter } from 'node:path';
3
4
 
4
5
  export type Spawner = (
5
6
  command: string,
@@ -26,6 +27,32 @@ function envOrUndefined(key: string): string | undefined {
26
27
  return value;
27
28
  }
28
29
 
30
+ // MCP desktop clients often spawn servers with a stripped PATH that excludes
31
+ // Homebrew, user-local, and Go's default install dirs — so even when gog is
32
+ // installed, the spawned server can't find it. Augment the child's PATH with
33
+ // the locations where gogcli is commonly installed.
34
+ function augmentedPath(): string {
35
+ const home = process.env.HOME;
36
+ const candidates = [
37
+ process.env.PATH ?? '',
38
+ '/opt/homebrew/bin',
39
+ '/usr/local/bin',
40
+ home ? `${home}/.local/bin` : '',
41
+ home ? `${home}/go/bin` : '',
42
+ ];
43
+ const seen = new Set<string>();
44
+ const parts: string[] = [];
45
+ for (const c of candidates) {
46
+ if (!c) continue;
47
+ for (const dir of c.split(delimiter)) {
48
+ if (!dir || seen.has(dir)) continue;
49
+ seen.add(dir);
50
+ parts.push(dir);
51
+ }
52
+ }
53
+ return parts.join(delimiter);
54
+ }
55
+
29
56
  function formatTimeout(ms: number): string {
30
57
  const seconds = Math.round(ms / 1000);
31
58
  if (seconds >= 60) {
@@ -55,7 +82,8 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
55
82
  // Strip GOG_ACCESS_TOKEN so gogcli uses stored refresh tokens instead of
56
83
  // a potentially stale direct access token passed through MCP env config.
57
84
  const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
58
- const child = spawner(envOrUndefined('GOG_PATH') ?? 'gog', fullArgs, { env: cleanEnv });
85
+ const childEnv = { ...cleanEnv, PATH: augmentedPath() };
86
+ const child = spawner(envOrUndefined('GOG_PATH') ?? 'gog', fullArgs, { env: childEnv });
59
87
  const stdoutChunks: Buffer[] = [];
60
88
  const stderrChunks: Buffer[] = [];
61
89
  let settled = false;
@@ -90,6 +118,14 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
90
118
  clearTimeout(timer);
91
119
  if (settled) return;
92
120
  settled = true;
121
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
122
+ reject(new Error(
123
+ 'gog executable not found. Install gogcli (https://github.com/steipete/gogcli) ' +
124
+ 'or set GOG_PATH in your MCP client config to the absolute binary path ' +
125
+ '(run `which gog` in a terminal to find it).',
126
+ ));
127
+ return;
128
+ }
93
129
  reject(err);
94
130
  });
95
131
  });
@@ -24,7 +24,7 @@ describe('run', () => {
24
24
  expect(spawner).toHaveBeenCalledWith(
25
25
  'gog',
26
26
  ['--json', '--color=never', '--no-input', 'sheets', 'get', 'id1', 'A1'],
27
- expect.objectContaining({ env: process.env }),
27
+ expect.objectContaining({ env: expect.any(Object) }),
28
28
  );
29
29
  });
30
30
 
@@ -206,6 +206,93 @@ describe('run', () => {
206
206
  .rejects.toThrow('gog not found');
207
207
  });
208
208
 
209
+ it('wraps ENOENT spawn errors with an install-or-set-GOG_PATH hint', async () => {
210
+ const spawner = vi.fn(() => {
211
+ const proc = new EventEmitter() as ReturnType<Spawner>;
212
+ (proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
213
+ (proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
214
+ setTimeout(() => {
215
+ const err = new Error('spawn gog ENOENT') as NodeJS.ErrnoException;
216
+ err.code = 'ENOENT';
217
+ proc.emit('error', err);
218
+ }, 0);
219
+ return proc;
220
+ }) as unknown as Spawner;
221
+ await expect(run(['sheets', 'get', 'id', 'A1'], { spawner }))
222
+ .rejects.toThrow(/gog executable not found.*Install gogcli.*GOG_PATH/s);
223
+ });
224
+
225
+ it('augments child PATH with common gogcli install dirs', async () => {
226
+ const spawner = makeSpawner(0, '{}');
227
+ const originalHome = process.env.HOME;
228
+ const originalPath = process.env.PATH;
229
+ process.env.HOME = '/Users/test';
230
+ process.env.PATH = '/usr/bin:/bin';
231
+ try {
232
+ await run(['sheets', 'metadata', 'id1'], { spawner });
233
+ const passedEnv = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
234
+ const passedPath = passedEnv.PATH!;
235
+ expect(passedPath).toContain('/usr/bin');
236
+ expect(passedPath).toContain('/opt/homebrew/bin');
237
+ expect(passedPath).toContain('/usr/local/bin');
238
+ expect(passedPath).toContain('/Users/test/.local/bin');
239
+ expect(passedPath).toContain('/Users/test/go/bin');
240
+ } finally {
241
+ if (originalHome === undefined) delete process.env.HOME;
242
+ else process.env.HOME = originalHome;
243
+ if (originalPath === undefined) delete process.env.PATH;
244
+ else process.env.PATH = originalPath;
245
+ }
246
+ });
247
+
248
+ it('does not duplicate dirs that are already on PATH', async () => {
249
+ const spawner = makeSpawner(0, '{}');
250
+ const originalPath = process.env.PATH;
251
+ process.env.PATH = '/opt/homebrew/bin:/usr/bin';
252
+ try {
253
+ await run(['sheets', 'metadata', 'id1'], { spawner });
254
+ const passedEnv = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
255
+ const passedPath = passedEnv.PATH!;
256
+ const homebrewCount = passedPath.split(':').filter(d => d === '/opt/homebrew/bin').length;
257
+ expect(homebrewCount).toBe(1);
258
+ } finally {
259
+ if (originalPath === undefined) delete process.env.PATH;
260
+ else process.env.PATH = originalPath;
261
+ }
262
+ });
263
+
264
+ it('augments PATH even when HOME is unset', async () => {
265
+ const spawner = makeSpawner(0, '{}');
266
+ const originalHome = process.env.HOME;
267
+ const originalPath = process.env.PATH;
268
+ delete process.env.HOME;
269
+ process.env.PATH = '/usr/bin';
270
+ try {
271
+ await run(['sheets', 'metadata', 'id1'], { spawner });
272
+ const passedEnv = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
273
+ const passedPath = passedEnv.PATH!;
274
+ expect(passedPath).toContain('/opt/homebrew/bin');
275
+ expect(passedPath).not.toContain('.local/bin');
276
+ } finally {
277
+ if (originalHome !== undefined) process.env.HOME = originalHome;
278
+ if (originalPath === undefined) delete process.env.PATH;
279
+ else process.env.PATH = originalPath;
280
+ }
281
+ });
282
+
283
+ it('handles empty PATH gracefully', async () => {
284
+ const spawner = makeSpawner(0, '{}');
285
+ const originalPath = process.env.PATH;
286
+ delete process.env.PATH;
287
+ try {
288
+ await run(['sheets', 'metadata', 'id1'], { spawner });
289
+ const passedEnv = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
290
+ expect(passedEnv.PATH).toContain('/opt/homebrew/bin');
291
+ } finally {
292
+ if (originalPath !== undefined) process.env.PATH = originalPath;
293
+ }
294
+ });
295
+
209
296
  it('ignores close event if error event already settled the promise', async () => {
210
297
  const spawner = vi.fn(() => {
211
298
  const proc = new EventEmitter() as ReturnType<Spawner>;