gogcli-mcp 2.0.7 → 2.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
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>",
@@ -46,9 +46,9 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.6.2",
49
- "@vitest/coverage-v8": "^4.1.2",
49
+ "@vitest/coverage-v8": "^4.1.6",
50
50
  "esbuild": "^0.28.0",
51
51
  "typescript": "^6.0.2",
52
- "vitest": "^4.1.2"
52
+ "vitest": "^4.1.6"
53
53
  }
54
54
  }
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.9",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.0.7",
15
+ "version": "2.0.9",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
package/src/lib.ts CHANGED
@@ -15,5 +15,14 @@ export {
15
15
  } from './server.js';
16
16
  export { run } from './runner.js';
17
17
  export type { RunOptions, Spawner } from './runner.js';
18
- export { accountParam, runOrDiagnose, toText, toError } from './tools/utils.js';
18
+ export {
19
+ accountParam,
20
+ runOrDiagnose,
21
+ toText,
22
+ toError,
23
+ ids,
24
+ paginationParams,
25
+ pushPaginationFlags,
26
+ registerRunTool,
27
+ } from './tools/utils.js';
19
28
  export type { ToolResult } from './tools/utils.js';
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,66 @@ function envOrUndefined(key: string): string | undefined {
26
27
  return value;
27
28
  }
28
29
 
30
+ // Strip ambient secrets from the child env so gogcli only sees its own
31
+ // configured credentials. GOG_ACCESS_TOKEN is the original target: gogcli
32
+ // would otherwise try to use a (potentially stale) directly-passed token
33
+ // instead of the stored refresh token. The broader patterns are
34
+ // defense-in-depth — the parent process's shell may have other Google /
35
+ // cloud / API secrets in scope that the child has no business seeing.
36
+ function sanitizedEnv(): NodeJS.ProcessEnv {
37
+ const result: NodeJS.ProcessEnv = {};
38
+ for (const [key, value] of Object.entries(process.env)) {
39
+ if (key === 'GOG_ACCESS_TOKEN') continue;
40
+ if (key === 'GOOGLE_APPLICATION_CREDENTIALS') continue;
41
+ if (/(_TOKEN|_SECRET|_API_KEY|_PRIVATE_KEY)$/.test(key)) continue;
42
+ result[key] = value;
43
+ }
44
+ return result;
45
+ }
46
+
47
+ // Redact bearer/refresh-token patterns from error text before surfacing
48
+ // it back to the MCP client. If gog ever emits a token in stderr (e.g.
49
+ // from a verbose log mode), this prevents it from leaking to the model.
50
+ const TOKEN_PATTERNS: RegExp[] = [
51
+ /Bearer\s+[A-Za-z0-9._\-+/=]+/gi,
52
+ /ya29\.[A-Za-z0-9._\-]+/g, // OAuth2 access tokens
53
+ /1\/\/[A-Za-z0-9._\-]+/g, // OAuth2 refresh tokens
54
+ /AIza[A-Za-z0-9_\-]{35}/g, // Google API keys
55
+ ];
56
+ export function redactSecrets(text: string): string {
57
+ let redacted = text;
58
+ for (const re of TOKEN_PATTERNS) {
59
+ redacted = redacted.replace(re, '[REDACTED]');
60
+ }
61
+ return redacted;
62
+ }
63
+
64
+ // MCP desktop clients often spawn servers with a stripped PATH that excludes
65
+ // Homebrew, user-local, and Go's default install dirs — so even when gog is
66
+ // installed, the spawned server can't find it. Augment the child's PATH with
67
+ // the locations where gogcli is commonly installed.
68
+ function augmentedPath(): string {
69
+ const home = process.env.HOME;
70
+ const candidates = [
71
+ process.env.PATH ?? '',
72
+ '/opt/homebrew/bin',
73
+ '/usr/local/bin',
74
+ home ? `${home}/.local/bin` : '',
75
+ home ? `${home}/go/bin` : '',
76
+ ];
77
+ const seen = new Set<string>();
78
+ const parts: string[] = [];
79
+ for (const c of candidates) {
80
+ if (!c) continue;
81
+ for (const dir of c.split(delimiter)) {
82
+ if (!dir || seen.has(dir)) continue;
83
+ seen.add(dir);
84
+ parts.push(dir);
85
+ }
86
+ }
87
+ return parts.join(delimiter);
88
+ }
89
+
29
90
  function formatTimeout(ms: number): string {
30
91
  const seconds = Math.round(ms / 1000);
31
92
  if (seconds >= 60) {
@@ -52,10 +113,8 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
52
113
  const effectiveTimeout = timeout ?? TIMEOUT_MS;
53
114
 
54
115
  return new Promise((resolve, reject) => {
55
- // Strip GOG_ACCESS_TOKEN so gogcli uses stored refresh tokens instead of
56
- // a potentially stale direct access token passed through MCP env config.
57
- const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
58
- const child = spawner(envOrUndefined('GOG_PATH') ?? 'gog', fullArgs, { env: cleanEnv });
116
+ const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
117
+ const child = spawner(envOrUndefined('GOG_PATH') ?? 'gog', fullArgs, { env: childEnv });
59
118
  const stdoutChunks: Buffer[] = [];
60
119
  const stderrChunks: Buffer[] = [];
61
120
  let settled = false;
@@ -82,7 +141,7 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
82
141
  resolve(stdout);
83
142
  }
84
143
  } else {
85
- reject(new Error(stderr || `gog exited with code ${code}`));
144
+ reject(new Error(redactSecrets(stderr || `gog exited with code ${code}`)));
86
145
  }
87
146
  });
88
147
 
@@ -90,6 +149,14 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
90
149
  clearTimeout(timer);
91
150
  if (settled) return;
92
151
  settled = true;
152
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
153
+ reject(new Error(
154
+ 'gog executable not found. Install gogcli (https://github.com/steipete/gogcli) ' +
155
+ 'or set GOG_PATH in your MCP client config to the absolute binary path ' +
156
+ '(run `which gog` in a terminal to find it).',
157
+ ));
158
+ return;
159
+ }
93
160
  reject(err);
94
161
  });
95
162
  });
package/src/tools/auth.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { run } from '../runner.js';
4
- import { toText, toError, runOrDiagnose } from './utils.js';
4
+ import { toText, toError, runOrDiagnose, registerRunTool } from './utils.js';
5
5
 
6
6
  export function registerAuthTools(server: McpServer): void {
7
7
  server.registerTool('gog_auth_list', {
@@ -65,14 +65,10 @@ export function registerAuthTools(server: McpServer): void {
65
65
  }
66
66
  });
67
67
 
68
- server.registerTool('gog_auth_run', {
69
- description: 'Run any gog auth subcommand. Run `gog auth --help` to see all available subcommands and flags. Note: for browser-based authorization, use gog_auth_add instead.',
70
- annotations: { destructiveHint: true },
71
- inputSchema: {
72
- subcommand: z.string().describe('The gog auth subcommand, e.g. "remove", "alias", "tokens"'),
73
- args: z.array(z.string()).describe('Additional positional args and flags'),
74
- },
75
- }, async ({ subcommand, args }) => {
76
- return runOrDiagnose(['auth', subcommand, ...args], {});
68
+ registerRunTool(server, {
69
+ service: 'auth',
70
+ examples: '"remove", "alias", "tokens"',
71
+ omitAccount: true,
72
+ note: 'For browser-based authorization, use gog_auth_add instead.',
77
73
  });
78
74
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerCalendarTools(server: McpServer): void {
6
6
  server.registerTool('gog_calendar_events', {
@@ -114,15 +114,5 @@ export function registerCalendarTools(server: McpServer): void {
114
114
  return runOrDiagnose(args, { account });
115
115
  });
116
116
 
117
- server.registerTool('gog_calendar_run', {
118
- description: 'Run any gog calendar subcommand not covered by the other tools. Run `gog calendar --help` for the full list of subcommands, or `gog calendar <subcommand> --help` for flags on a specific subcommand.',
119
- annotations: { destructiveHint: true },
120
- inputSchema: {
121
- subcommand: z.string().describe('The gog calendar subcommand to run, e.g. "calendars", "freebusy"'),
122
- args: z.array(z.string()).describe('Additional positional args and flags'),
123
- account: accountParam,
124
- },
125
- }, async ({ subcommand, args, account }) => {
126
- return runOrDiagnose(['calendar', subcommand, ...args], { account });
127
- });
117
+ registerRunTool(server, { service: 'calendar', examples: '"calendars", "freebusy"' });
128
118
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerClassroomTools(server: McpServer): void {
6
6
  server.registerTool('gog_classroom_courses_list', {
@@ -387,15 +387,9 @@ export function registerClassroomTools(server: McpServer): void {
387
387
  return runOrDiagnose(args, { account });
388
388
  });
389
389
 
390
- server.registerTool('gog_classroom_run', {
391
- description: 'Run any gog classroom subcommand not covered by the other tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.). Run `gog classroom --help` for the full list, or `gog classroom <subcommand> --help` for flags.',
392
- annotations: { destructiveHint: true },
393
- inputSchema: {
394
- subcommand: z.string().describe('The gog classroom subcommand to run, e.g. "guardians", "materials", "guardian-invitations"'),
395
- args: z.array(z.string()).describe('Additional positional args and flags'),
396
- account: accountParam,
397
- },
398
- }, async ({ subcommand, args, account }) => {
399
- return runOrDiagnose(['classroom', subcommand, ...args], { account });
390
+ registerRunTool(server, {
391
+ service: 'classroom',
392
+ examples: '"guardians", "materials", "guardian-invitations"',
393
+ note: 'Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.).',
400
394
  });
401
395
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerContactsTools(server: McpServer): void {
6
6
  server.registerTool('gog_contacts_search', {
@@ -57,15 +57,5 @@ export function registerContactsTools(server: McpServer): void {
57
57
  return runOrDiagnose(args, { account });
58
58
  });
59
59
 
60
- server.registerTool('gog_contacts_run', {
61
- description: 'Run any gog contacts subcommand not covered by the other tools. Run `gog contacts --help` for the full list of subcommands, or `gog contacts <subcommand> --help` for flags on a specific subcommand.',
62
- annotations: { destructiveHint: true },
63
- inputSchema: {
64
- subcommand: z.string().describe('The gog contacts subcommand to run, e.g. "update", "delete", "directory"'),
65
- args: z.array(z.string()).describe('Additional positional args and flags'),
66
- account: accountParam,
67
- },
68
- }, async ({ subcommand, args, account }) => {
69
- return runOrDiagnose(['contacts', subcommand, ...args], { account });
70
- });
60
+ registerRunTool(server, { service: 'contacts', examples: '"update", "delete", "directory"' });
71
61
  }
package/src/tools/docs.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerDocsTools(server: McpServer): void {
6
6
  server.registerTool('gog_docs_info', {
@@ -74,15 +74,5 @@ export function registerDocsTools(server: McpServer): void {
74
74
  return runOrDiagnose(['docs', 'structure', docId], { account });
75
75
  });
76
76
 
77
- server.registerTool('gog_docs_run', {
78
- description: 'Run any gog docs subcommand not covered by the other tools. Run `gog docs --help` for the full list of subcommands, or `gog docs <subcommand> --help` for flags on a specific subcommand.',
79
- annotations: { destructiveHint: true },
80
- inputSchema: {
81
- subcommand: z.string().describe('The gog docs subcommand to run, e.g. "copy", "clear", "insert", "sed", "export"'),
82
- args: z.array(z.string()).describe('Additional positional args and flags'),
83
- account: accountParam,
84
- },
85
- }, async ({ subcommand, args, account }) => {
86
- return runOrDiagnose(['docs', subcommand, ...args], { account });
87
- });
77
+ registerRunTool(server, { service: 'docs', examples: '"copy", "clear", "insert", "sed", "export"' });
88
78
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerDriveTools(server: McpServer): void {
6
6
  server.registerTool('gog_drive_ls', {
@@ -114,15 +114,5 @@ export function registerDriveTools(server: McpServer): void {
114
114
  return runOrDiagnose(args, { account });
115
115
  });
116
116
 
117
- server.registerTool('gog_drive_run', {
118
- description: 'Run any gog drive subcommand not covered by the other tools. Run `gog drive --help` for the full list of subcommands, or `gog drive <subcommand> --help` for flags on a specific subcommand.',
119
- annotations: { destructiveHint: true },
120
- inputSchema: {
121
- subcommand: z.string().describe('The gog drive subcommand to run, e.g. "copy", "upload", "download", "permissions"'),
122
- args: z.array(z.string()).describe('Additional positional args and flags'),
123
- account: accountParam,
124
- },
125
- }, async ({ subcommand, args, account }) => {
126
- return runOrDiagnose(['drive', subcommand, ...args], { account });
127
- });
117
+ registerRunTool(server, { service: 'drive', examples: '"copy", "upload", "download", "permissions"' });
128
118
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerGmailTools(server: McpServer): void {
6
6
  server.registerTool('gog_gmail_search', {
@@ -53,15 +53,5 @@ export function registerGmailTools(server: McpServer): void {
53
53
  return runOrDiagnose(args, { account });
54
54
  });
55
55
 
56
- server.registerTool('gog_gmail_run', {
57
- description: 'Run any gog gmail subcommand not covered by the other tools. Run `gog gmail --help` for the full list of subcommands, or `gog gmail <subcommand> --help` for flags on a specific subcommand.',
58
- annotations: { destructiveHint: true },
59
- inputSchema: {
60
- subcommand: z.string().describe('The gog gmail subcommand to run, e.g. "archive", "mark-read", "labels"'),
61
- args: z.array(z.string()).describe('Additional positional args and flags'),
62
- account: accountParam,
63
- },
64
- }, async ({ subcommand, args, account }) => {
65
- return runOrDiagnose(['gmail', subcommand, ...args], { account });
66
- });
56
+ registerRunTool(server, { service: 'gmail', examples: '"archive", "mark-read", "labels"' });
67
57
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerSheetsTools(server: McpServer): void {
6
6
  server.registerTool('gog_sheets_get', {
@@ -94,15 +94,5 @@ export function registerSheetsTools(server: McpServer): void {
94
94
  return runOrDiagnose(['sheets', 'find-replace', spreadsheetId, find, replace], { account });
95
95
  });
96
96
 
97
- server.registerTool('gog_sheets_run', {
98
- description: 'Run any gog sheets subcommand not covered by the other tools. Run `gog sheets --help` for the full list of subcommands, or `gog sheets <subcommand> --help` for flags on a specific subcommand.',
99
- annotations: { destructiveHint: true },
100
- inputSchema: {
101
- subcommand: z.string().describe('The gog sheets subcommand to run, e.g. "freeze", "add-tab", "rename-tab"'),
102
- args: z.array(z.string()).describe('Additional positional args and flags, e.g. ["<spreadsheetId>", "--rows=1"]'),
103
- account: accountParam,
104
- },
105
- }, async ({ subcommand, args, account }) => {
106
- return runOrDiagnose(['sheets', subcommand, ...args], { account });
107
- });
97
+ registerRunTool(server, { service: 'sheets', examples: '"freeze", "add-tab", "rename-tab"' });
108
98
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerSlidesTools(server: McpServer): void {
6
6
  server.registerTool('gog_slides_export', {
@@ -82,15 +82,5 @@ export function registerSlidesTools(server: McpServer): void {
82
82
  return runOrDiagnose(['slides', 'read-slide', presentationId, slideId], { account });
83
83
  });
84
84
 
85
- server.registerTool('gog_slides_run', {
86
- description: 'Run any gog slides subcommand not covered by the other tools. Run `gog slides --help` for the full list of subcommands, or `gog slides <subcommand> --help` for flags on a specific subcommand.',
87
- annotations: { destructiveHint: true },
88
- inputSchema: {
89
- subcommand: z.string().describe('The gog slides subcommand to run'),
90
- args: z.array(z.string()).describe('Additional positional args and flags'),
91
- account: accountParam,
92
- },
93
- }, async ({ subcommand, args, account }) => {
94
- return runOrDiagnose(['slides', subcommand, ...args], { account });
95
- });
85
+ registerRunTool(server, { service: 'slides', examples: '"add-slide", "delete-slide", "update-notes"' });
96
86
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from './utils.js';
3
+ import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
4
 
5
5
  export function registerTasksTools(server: McpServer): void {
6
6
  server.registerTool('gog_tasks_lists', {
@@ -77,15 +77,5 @@ export function registerTasksTools(server: McpServer): void {
77
77
  return runOrDiagnose(['tasks', 'delete', tasklistId, taskId], { account });
78
78
  });
79
79
 
80
- server.registerTool('gog_tasks_run', {
81
- description: 'Run any gog tasks subcommand not covered by the other tools. Run `gog tasks --help` for the full list of subcommands, or `gog tasks <subcommand> --help` for flags on a specific subcommand.',
82
- annotations: { destructiveHint: true },
83
- inputSchema: {
84
- subcommand: z.string().describe('The gog tasks subcommand to run, e.g. "update", "undo", "clear"'),
85
- args: z.array(z.string()).describe('Additional positional args and flags'),
86
- account: accountParam,
87
- },
88
- }, async ({ subcommand, args, account }) => {
89
- return runOrDiagnose(['tasks', subcommand, ...args], { account });
90
- });
80
+ registerRunTool(server, { service: 'tasks', examples: '"update", "undo", "clear"' });
91
81
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { run } from '../runner.js';
3
4
 
4
5
  export type ToolResult = { content: [{ type: 'text'; text: string }] };
@@ -7,6 +8,85 @@ export const accountParam = z.string().optional().describe(
7
8
  'Google account email to use (overrides GOG_ACCOUNT env var)',
8
9
  );
9
10
 
11
+ // Canonical ID descriptors. Use these instead of redefining the same
12
+ // `z.string().describe('Course ID')` etc. across multiple tool files —
13
+ // keeps descriptions in lockstep so they don't drift apart.
14
+ export const ids = {
15
+ course: z.string().describe('Course ID'),
16
+ coursework: z.string().describe('Coursework ID'),
17
+ submission: z.string().describe('Submission ID'),
18
+ announcement: z.string().describe('Announcement ID'),
19
+ topic: z.string().describe('Topic ID'),
20
+ invitation: z.string().describe('Invitation ID'),
21
+ spreadsheet: z.string().describe('Spreadsheet ID (from the URL)'),
22
+ doc: z.string().describe('Doc ID (from the URL)'),
23
+ presentation: z.string().describe('Presentation ID'),
24
+ slide: z.string().describe('Slide ID'),
25
+ file: z.string().describe('File ID'),
26
+ message: z.string().describe('Message ID'),
27
+ thread: z.string().describe('Thread ID'),
28
+ draft: z.string().describe('Draft ID'),
29
+ label: z.string().describe('Label ID or name'),
30
+ attachment: z.string().describe('Attachment ID'),
31
+ comment: z.string().describe('Comment ID'),
32
+ meetingCode: z.string().describe('Meeting code (e.g. abc-defg-hij)'),
33
+ permission: z.string().describe('Permission ID'),
34
+ user: z.string().describe('User ID'),
35
+ // People API uses fully-qualified resource names ("people/c123") not bare IDs.
36
+ person: z.string().describe('Person resource name (people/...) or email'),
37
+ };
38
+
39
+ // Pagination param triple — appears in 20+ tools across base + extras.
40
+ export const paginationParams = {
41
+ max: z.number().int().optional().describe('Max results'),
42
+ page: z.string().optional().describe('Page token'),
43
+ all: z.boolean().optional().describe('Fetch all pages'),
44
+ };
45
+
46
+ // Append pagination flags to an argv array. Mirrors the shape of
47
+ // paginationParams above. Use together to keep call sites concise.
48
+ export function pushPaginationFlags(
49
+ args: string[],
50
+ p: { max?: number; page?: string; all?: boolean },
51
+ ): void {
52
+ if (p.max !== undefined) args.push(`--max=${p.max}`);
53
+ if (p.page) args.push(`--page=${p.page}`);
54
+ if (p.all) args.push('--all');
55
+ }
56
+
57
+ // Register a `gog_<service>_run` escape-hatch tool. 11 services currently
58
+ // register an identical-shape tool; this factory keeps them in lockstep.
59
+ // Pass `omitAccount: true` only for auth, which doesn't take --account.
60
+ export function registerRunTool(
61
+ server: McpServer,
62
+ options: {
63
+ service: string;
64
+ examples: string;
65
+ omitAccount?: boolean;
66
+ /** Extra sentence appended to the description (used by auth to point to gog_auth_add). */
67
+ note?: string;
68
+ },
69
+ ): void {
70
+ const { service, examples, omitAccount = false, note } = options;
71
+ const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
72
+ const description = note ? `${baseDescription} ${note}` : baseDescription;
73
+ const inputSchema: Record<string, z.ZodTypeAny> = {
74
+ subcommand: z.string().describe(`The gog ${service} subcommand to run, e.g. ${examples}`),
75
+ args: z.array(z.string()).describe('Additional positional args and flags'),
76
+ };
77
+ if (!omitAccount) {
78
+ inputSchema.account = accountParam;
79
+ }
80
+ server.registerTool(`gog_${service}_run`, {
81
+ description,
82
+ annotations: { destructiveHint: true },
83
+ inputSchema,
84
+ }, async (rawArgs) => {
85
+ const { subcommand, args, account } = rawArgs as { subcommand: string; args: string[]; account?: string };
86
+ return runOrDiagnose([service, subcommand, ...args], { account });
87
+ });
88
+ }
89
+
10
90
  export function toText(output: string): ToolResult {
11
91
  return { content: [{ type: 'text' as const, text: output }] };
12
92
  }
@@ -1,11 +1,7 @@
1
- // Shared test harness for `*-extra.ts` registrars across sub-packages.
1
+ // Shared test harness for tool registrars across base + sub-packages.
2
2
  //
3
- // Each sub-package's extras test file follows the same shape: mock the
4
- // `runOrDiagnose` export from `gogcli-mcp/lib`, register the extras tools onto
5
- // a stub `McpServer`, capture each tool's handler into a Map, and exercise the
6
- // handlers with sample inputs. The `vi.mock(...)` call must stay in the
7
- // caller's test file (vitest hoists it at the module scope), but the
8
- // boilerplate around it can live here.
3
+ // `vi.mock(...)` must stay in the caller's test file because vitest hoists
4
+ // it at module scope, but the boilerplate around it lives here.
9
5
  import { vi } from 'vitest';
10
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
7
 
@@ -17,7 +13,7 @@ export function toText(text: string): { content: Array<{ type: string; text: str
17
13
  return { content: [{ type: 'text', text }] };
18
14
  }
19
15
 
20
- export function setupExtrasHandlers(
16
+ export function setupHandlers(
21
17
  register: (server: McpServer) => void,
22
18
  ): Map<string, ToolHandler> {
23
19
  const server = new McpServer({ name: 'test', version: '0.0.0' });