gogcli-mcp 2.0.8 → 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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +226 -155
- package/dist/lib.js +235 -155
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/lib.ts +10 -1
- package/src/runner.ts +36 -5
- package/src/tools/auth.ts +6 -10
- package/src/tools/calendar.ts +2 -12
- package/src/tools/classroom.ts +5 -11
- package/src/tools/contacts.ts +2 -12
- package/src/tools/docs.ts +2 -12
- package/src/tools/drive.ts +2 -12
- package/src/tools/gmail.ts +2 -12
- package/src/tools/sheets.ts +2 -12
- package/src/tools/slides.ts +2 -12
- package/src/tools/tasks.ts +2 -12
- package/src/tools/utils.ts +80 -0
- package/tests/helpers/{extras-harness.ts → test-harness.ts} +4 -8
- package/tests/runner.test.ts +59 -0
- package/tests/tools/auth.test.ts +2 -13
- package/tests/tools/calendar.test.ts +2 -13
- package/tests/tools/classroom.test.ts +2 -13
- package/tests/tools/contacts.test.ts +2 -13
- package/tests/tools/docs.test.ts +2 -13
- package/tests/tools/drive.test.ts +2 -13
- package/tests/tools/gmail.test.ts +2 -13
- package/tests/tools/sheets.test.ts +2 -13
- package/tests/tools/slides.test.ts +2 -13
- package/tests/tools/tasks.test.ts +2 -13
- package/tests/tools/utils.test.ts +35 -1
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 {
|
|
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
|
@@ -27,6 +27,40 @@ function envOrUndefined(key: string): string | undefined {
|
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
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
|
+
|
|
30
64
|
// MCP desktop clients often spawn servers with a stripped PATH that excludes
|
|
31
65
|
// Homebrew, user-local, and Go's default install dirs — so even when gog is
|
|
32
66
|
// installed, the spawned server can't find it. Augment the child's PATH with
|
|
@@ -79,10 +113,7 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
79
113
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
80
114
|
|
|
81
115
|
return new Promise((resolve, reject) => {
|
|
82
|
-
|
|
83
|
-
// a potentially stale direct access token passed through MCP env config.
|
|
84
|
-
const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
|
|
85
|
-
const childEnv = { ...cleanEnv, PATH: augmentedPath() };
|
|
116
|
+
const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
|
|
86
117
|
const child = spawner(envOrUndefined('GOG_PATH') ?? 'gog', fullArgs, { env: childEnv });
|
|
87
118
|
const stdoutChunks: Buffer[] = [];
|
|
88
119
|
const stderrChunks: Buffer[] = [];
|
|
@@ -110,7 +141,7 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
110
141
|
resolve(stdout);
|
|
111
142
|
}
|
|
112
143
|
} else {
|
|
113
|
-
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
144
|
+
reject(new Error(redactSecrets(stderr || `gog exited with code ${code}`)));
|
|
114
145
|
}
|
|
115
146
|
});
|
|
116
147
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|
package/src/tools/calendar.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 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
|
|
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
|
}
|
package/src/tools/classroom.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 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
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
}
|
package/src/tools/contacts.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 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
|
|
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
|
|
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
|
}
|
package/src/tools/drive.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 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
|
|
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
|
}
|
package/src/tools/gmail.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 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
|
|
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
|
}
|
package/src/tools/sheets.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 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
|
|
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
|
}
|
package/src/tools/slides.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 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
|
|
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
|
}
|
package/src/tools/tasks.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 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
|
|
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
|
}
|
package/src/tools/utils.ts
CHANGED
|
@@ -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
|
|
1
|
+
// Shared test harness for tool registrars across base + sub-packages.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
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' });
|
package/tests/runner.test.ts
CHANGED
|
@@ -483,6 +483,65 @@ describe('run', () => {
|
|
|
483
483
|
}
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
+
it('strips GOOGLE_APPLICATION_CREDENTIALS and *_TOKEN/*_SECRET/*_API_KEY/*_PRIVATE_KEY vars', async () => {
|
|
487
|
+
const spawner = makeSpawner(0, '{}');
|
|
488
|
+
const snapshot = {
|
|
489
|
+
GOOGLE_APPLICATION_CREDENTIALS: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
490
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
491
|
+
DB_PASSWORD_TOKEN: process.env.DB_PASSWORD_TOKEN,
|
|
492
|
+
AWS_SECRET: process.env.AWS_SECRET,
|
|
493
|
+
MY_PRIVATE_KEY: process.env.MY_PRIVATE_KEY,
|
|
494
|
+
BENIGN_VAR: process.env.BENIGN_VAR,
|
|
495
|
+
};
|
|
496
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/sa.json';
|
|
497
|
+
process.env.ANTHROPIC_API_KEY = 'sk-ant-secret';
|
|
498
|
+
process.env.DB_PASSWORD_TOKEN = 'secret';
|
|
499
|
+
process.env.AWS_SECRET = 'secret';
|
|
500
|
+
process.env.MY_PRIVATE_KEY = 'secret';
|
|
501
|
+
process.env.BENIGN_VAR = 'hello';
|
|
502
|
+
try {
|
|
503
|
+
await run(['docs', 'cat', 'id'], { spawner });
|
|
504
|
+
const envPassed = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
|
|
505
|
+
expect(envPassed.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined();
|
|
506
|
+
expect(envPassed.ANTHROPIC_API_KEY).toBeUndefined();
|
|
507
|
+
expect(envPassed.DB_PASSWORD_TOKEN).toBeUndefined();
|
|
508
|
+
expect(envPassed.AWS_SECRET).toBeUndefined();
|
|
509
|
+
expect(envPassed.MY_PRIVATE_KEY).toBeUndefined();
|
|
510
|
+
expect(envPassed.BENIGN_VAR).toBe('hello');
|
|
511
|
+
} finally {
|
|
512
|
+
for (const [k, v] of Object.entries(snapshot)) {
|
|
513
|
+
if (v === undefined) delete process.env[k];
|
|
514
|
+
else process.env[k] = v;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('redacts Bearer with quoted/encoded characters', async () => {
|
|
520
|
+
const stderrLeak = 'http 401: header was Bearer eyJ.test+slash/equal=padding more text';
|
|
521
|
+
const spawner = makeSpawner(1, '', stderrLeak);
|
|
522
|
+
try {
|
|
523
|
+
await run(['gmail', 'get', 'm1'], { spawner });
|
|
524
|
+
} catch (e) {
|
|
525
|
+
const msg = (e as Error).message;
|
|
526
|
+
expect(msg).not.toContain('eyJ.test+slash/equal=padding');
|
|
527
|
+
expect(msg).toContain('[REDACTED]');
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('redacts bearer/refresh tokens and Google API keys from stderr surfaced to the client', async () => {
|
|
532
|
+
const stderrLeak = 'request failed: Authorization: Bearer ya29.a0Ad52N3-LEAKED-TOKEN-VALUE refresh 1//0eLEAKED-REFRESH key AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI extra';
|
|
533
|
+
const spawner = makeSpawner(1, '', stderrLeak);
|
|
534
|
+
try {
|
|
535
|
+
await run(['gmail', 'get', 'm1'], { spawner });
|
|
536
|
+
} catch (e) {
|
|
537
|
+
const msg = (e as Error).message;
|
|
538
|
+
expect(msg).not.toContain('ya29.a0Ad52N3-LEAKED-TOKEN-VALUE');
|
|
539
|
+
expect(msg).not.toContain('1//0eLEAKED-REFRESH');
|
|
540
|
+
expect(msg).not.toContain('AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI');
|
|
541
|
+
expect(msg).toContain('[REDACTED]');
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
486
545
|
it('ignores timeout if close event already settled the promise', async () => {
|
|
487
546
|
vi.useFakeTimers();
|
|
488
547
|
const spawner = vi.fn(() => {
|
package/tests/tools/auth.test.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import { registerAuthTools } from '../../src/tools/auth.js';
|
|
4
3
|
import * as runner from '../../src/runner.js';
|
|
4
|
+
import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/runner.js');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function setupHandlers(): Map<string, ToolHandler> {
|
|
11
|
-
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
12
|
-
const handlers = new Map<string, ToolHandler>();
|
|
13
|
-
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
14
|
-
handlers.set(name, cb as ToolHandler);
|
|
15
|
-
return undefined as never;
|
|
16
|
-
});
|
|
17
|
-
registerAuthTools(server);
|
|
18
|
-
return handlers;
|
|
19
|
-
}
|
|
8
|
+
const setupHandlers = () => setupHandlersBase(registerAuthTools);
|
|
20
9
|
|
|
21
10
|
beforeEach(() => vi.clearAllMocks());
|
|
22
11
|
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import { registerCalendarTools } from '../../src/tools/calendar.js';
|
|
4
3
|
import * as runner from '../../src/runner.js';
|
|
4
|
+
import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/runner.js');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function setupHandlers(): Map<string, ToolHandler> {
|
|
11
|
-
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
12
|
-
const handlers = new Map<string, ToolHandler>();
|
|
13
|
-
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
14
|
-
handlers.set(name, cb as ToolHandler);
|
|
15
|
-
return undefined as never;
|
|
16
|
-
});
|
|
17
|
-
registerCalendarTools(server);
|
|
18
|
-
return handlers;
|
|
19
|
-
}
|
|
8
|
+
const setupHandlers = () => setupHandlersBase(registerCalendarTools);
|
|
20
9
|
|
|
21
10
|
beforeEach(() => vi.clearAllMocks());
|
|
22
11
|
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import { registerClassroomTools } from '../../src/tools/classroom.js';
|
|
4
3
|
import * as runner from '../../src/runner.js';
|
|
4
|
+
import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/runner.js');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function setupHandlers(): Map<string, ToolHandler> {
|
|
11
|
-
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
12
|
-
const handlers = new Map<string, ToolHandler>();
|
|
13
|
-
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
14
|
-
handlers.set(name, cb as ToolHandler);
|
|
15
|
-
return undefined as never;
|
|
16
|
-
});
|
|
17
|
-
registerClassroomTools(server);
|
|
18
|
-
return handlers;
|
|
19
|
-
}
|
|
8
|
+
const setupHandlers = () => setupHandlersBase(registerClassroomTools);
|
|
20
9
|
|
|
21
10
|
let handlers: Map<string, ToolHandler>;
|
|
22
11
|
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import { registerContactsTools } from '../../src/tools/contacts.js';
|
|
4
3
|
import * as runner from '../../src/runner.js';
|
|
4
|
+
import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/runner.js');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function setupHandlers(): Map<string, ToolHandler> {
|
|
11
|
-
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
12
|
-
const handlers = new Map<string, ToolHandler>();
|
|
13
|
-
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
14
|
-
handlers.set(name, cb as ToolHandler);
|
|
15
|
-
return undefined as never;
|
|
16
|
-
});
|
|
17
|
-
registerContactsTools(server);
|
|
18
|
-
return handlers;
|
|
19
|
-
}
|
|
8
|
+
const setupHandlers = () => setupHandlersBase(registerContactsTools);
|
|
20
9
|
|
|
21
10
|
beforeEach(() => vi.clearAllMocks());
|
|
22
11
|
|