gogcli-mcp 1.0.3 → 1.0.5
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/.mcp.json +1 -2
- package/CLAUDE.md +1 -1
- package/dist/index.js +93 -17
- package/{gogcli-mcp-1.0.3.skill → gogcli-mcp-1.0.5.skill} +0 -0
- package/manifest.json +26 -10
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/runner.ts +12 -7
- package/src/tools/auth.ts +2 -6
- package/src/tools/docs.ts +83 -1
- package/src/tools/utils.ts +11 -4
- package/tests/runner.test.ts +17 -0
- package/tests/tools/auth.test.ts +2 -2
- package/tests/tools/docs.test.ts +187 -0
- package/tests/tools/utils.test.ts +79 -0
package/.mcp.json
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -31,7 +31,7 @@ Main is always one version ahead of the latest tag. To release, run the **Tag &
|
|
|
31
31
|
4. Rebuilds, commits, and pushes main + tag
|
|
32
32
|
5. The tag push triggers the **Release** workflow (CI + npm publish + .mcpb + .skill + GitHub release)
|
|
33
33
|
|
|
34
|
-
Do NOT manually bump versions or create tags unless the user explicitly asks.
|
|
34
|
+
Do NOT manually bump versions or create tags unless the user explicitly asks. Always prefer the Tag & Bump action: `gh workflow run tag-and-bump.yml --ref main`. The action handles all four version files, tagging, and triggering the release.
|
|
35
35
|
|
|
36
36
|
## Architecture
|
|
37
37
|
|
package/dist/index.js
CHANGED
|
@@ -30137,9 +30137,10 @@ async function run(args, options = {}) {
|
|
|
30137
30137
|
fullArgs.push(...args);
|
|
30138
30138
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
30139
30139
|
return new Promise((resolve, reject) => {
|
|
30140
|
-
const
|
|
30141
|
-
|
|
30142
|
-
|
|
30140
|
+
const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
|
|
30141
|
+
const child = spawner(process.env.GOG_PATH ?? "gog", fullArgs, { env: cleanEnv });
|
|
30142
|
+
const stdoutChunks = [];
|
|
30143
|
+
const stderrChunks = [];
|
|
30143
30144
|
let settled = false;
|
|
30144
30145
|
const timer = setTimeout(() => {
|
|
30145
30146
|
settled = true;
|
|
@@ -30147,23 +30148,25 @@ async function run(args, options = {}) {
|
|
|
30147
30148
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
30148
30149
|
}, effectiveTimeout);
|
|
30149
30150
|
child.stdout.on("data", (chunk) => {
|
|
30150
|
-
|
|
30151
|
+
stdoutChunks.push(chunk);
|
|
30151
30152
|
});
|
|
30152
30153
|
child.stderr.on("data", (chunk) => {
|
|
30153
|
-
|
|
30154
|
+
stderrChunks.push(chunk);
|
|
30154
30155
|
});
|
|
30155
30156
|
child.on("close", (code) => {
|
|
30156
30157
|
clearTimeout(timer);
|
|
30157
30158
|
if (settled) return;
|
|
30158
30159
|
settled = true;
|
|
30160
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
30161
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
30159
30162
|
if (code === 0) {
|
|
30160
|
-
if (interactive && stderr
|
|
30163
|
+
if (interactive && stderr) {
|
|
30161
30164
|
resolve(stdout + "\n" + stderr);
|
|
30162
30165
|
} else {
|
|
30163
30166
|
resolve(stdout);
|
|
30164
30167
|
}
|
|
30165
30168
|
} else {
|
|
30166
|
-
reject(new Error(stderr
|
|
30169
|
+
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
30167
30170
|
}
|
|
30168
30171
|
});
|
|
30169
30172
|
child.on("error", (err) => {
|
|
@@ -30185,19 +30188,24 @@ function toText(output) {
|
|
|
30185
30188
|
function toError(err) {
|
|
30186
30189
|
return toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
30187
30190
|
}
|
|
30191
|
+
var AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
|
|
30192
|
+
var AUTH_HINT = "\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. Ask the user if they would like to re-authenticate.";
|
|
30188
30193
|
async function runOrDiagnose(args, options) {
|
|
30189
30194
|
try {
|
|
30190
30195
|
return toText(await run(args, options));
|
|
30191
30196
|
} catch (err) {
|
|
30192
30197
|
const base = toError(err);
|
|
30198
|
+
const errText = base.content[0].text;
|
|
30199
|
+
const isAuthError = AUTH_ERROR_PATTERN.test(errText);
|
|
30200
|
+
const hint = isAuthError ? AUTH_HINT : "";
|
|
30193
30201
|
try {
|
|
30194
30202
|
const accounts = await run(["auth", "list"]);
|
|
30195
|
-
return toText(`${
|
|
30203
|
+
return toText(`${errText}
|
|
30196
30204
|
|
|
30197
30205
|
Configured accounts:
|
|
30198
|
-
${accounts}`);
|
|
30206
|
+
${accounts}${hint}`);
|
|
30199
30207
|
} catch {
|
|
30200
|
-
return
|
|
30208
|
+
return toText(`${errText}${hint}`);
|
|
30201
30209
|
}
|
|
30202
30210
|
}
|
|
30203
30211
|
}
|
|
@@ -30264,11 +30272,7 @@ function registerAuthTools(server2) {
|
|
|
30264
30272
|
args: external_exports3.array(external_exports3.string()).describe("Additional positional args and flags")
|
|
30265
30273
|
}
|
|
30266
30274
|
}, async ({ subcommand, args }) => {
|
|
30267
|
-
|
|
30268
|
-
return toText(await run(["auth", subcommand, ...args]));
|
|
30269
|
-
} catch (err) {
|
|
30270
|
-
return toError(err);
|
|
30271
|
-
}
|
|
30275
|
+
return runOrDiagnose(["auth", subcommand, ...args], {});
|
|
30272
30276
|
});
|
|
30273
30277
|
}
|
|
30274
30278
|
|
|
@@ -30481,7 +30485,6 @@ function registerDocsTools(server2) {
|
|
|
30481
30485
|
});
|
|
30482
30486
|
server2.registerTool("gog_docs_create", {
|
|
30483
30487
|
description: "Create a new Google Doc. Returns JSON with the new docId and URL.",
|
|
30484
|
-
annotations: { destructiveHint: false },
|
|
30485
30488
|
inputSchema: {
|
|
30486
30489
|
title: external_exports3.string().describe("Title for the new document"),
|
|
30487
30490
|
account: accountParam
|
|
@@ -30525,6 +30528,79 @@ function registerDocsTools(server2) {
|
|
|
30525
30528
|
}, async ({ docId, account }) => {
|
|
30526
30529
|
return runOrDiagnose(["docs", "structure", docId], { account });
|
|
30527
30530
|
});
|
|
30531
|
+
server2.registerTool("gog_docs_comments_list", {
|
|
30532
|
+
description: "List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.",
|
|
30533
|
+
annotations: { readOnlyHint: true },
|
|
30534
|
+
inputSchema: {
|
|
30535
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30536
|
+
includeResolved: external_exports3.boolean().optional().describe("Include resolved comments (default: false, open only)"),
|
|
30537
|
+
account: accountParam
|
|
30538
|
+
}
|
|
30539
|
+
}, async ({ docId, includeResolved, account }) => {
|
|
30540
|
+
const args = ["docs", "comments", "list", docId];
|
|
30541
|
+
if (includeResolved) args.push("--include-resolved");
|
|
30542
|
+
return runOrDiagnose(args, { account });
|
|
30543
|
+
});
|
|
30544
|
+
server2.registerTool("gog_docs_comments_get", {
|
|
30545
|
+
description: "Get a single comment by ID, including its replies.",
|
|
30546
|
+
annotations: { readOnlyHint: true },
|
|
30547
|
+
inputSchema: {
|
|
30548
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30549
|
+
commentId: external_exports3.string().describe("Comment ID"),
|
|
30550
|
+
account: accountParam
|
|
30551
|
+
}
|
|
30552
|
+
}, async ({ docId, commentId, account }) => {
|
|
30553
|
+
return runOrDiagnose(["docs", "comments", "get", docId, commentId], { account });
|
|
30554
|
+
});
|
|
30555
|
+
server2.registerTool("gog_docs_comments_add", {
|
|
30556
|
+
description: "Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.",
|
|
30557
|
+
inputSchema: {
|
|
30558
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30559
|
+
content: external_exports3.string().describe("Comment text"),
|
|
30560
|
+
quoted: external_exports3.string().optional().describe("Quoted text to attach to the comment (shown in UIs when available)"),
|
|
30561
|
+
account: accountParam
|
|
30562
|
+
}
|
|
30563
|
+
}, async ({ docId, content, quoted, account }) => {
|
|
30564
|
+
const args = ["docs", "comments", "add", docId, content];
|
|
30565
|
+
if (quoted) args.push(`--quoted=${quoted}`);
|
|
30566
|
+
return runOrDiagnose(args, { account });
|
|
30567
|
+
});
|
|
30568
|
+
server2.registerTool("gog_docs_comments_reply", {
|
|
30569
|
+
description: "Reply to an existing comment on a Google Doc.",
|
|
30570
|
+
inputSchema: {
|
|
30571
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30572
|
+
commentId: external_exports3.string().describe("Comment ID to reply to"),
|
|
30573
|
+
content: external_exports3.string().describe("Reply text"),
|
|
30574
|
+
account: accountParam
|
|
30575
|
+
}
|
|
30576
|
+
}, async ({ docId, commentId, content, account }) => {
|
|
30577
|
+
return runOrDiagnose(["docs", "comments", "reply", docId, commentId, content], { account });
|
|
30578
|
+
});
|
|
30579
|
+
server2.registerTool("gog_docs_comments_resolve", {
|
|
30580
|
+
description: "Resolve a comment (mark as done). Optionally include a closing message.",
|
|
30581
|
+
annotations: { destructiveHint: true },
|
|
30582
|
+
inputSchema: {
|
|
30583
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30584
|
+
commentId: external_exports3.string().describe("Comment ID to resolve"),
|
|
30585
|
+
message: external_exports3.string().optional().describe("Optional message to include when resolving"),
|
|
30586
|
+
account: accountParam
|
|
30587
|
+
}
|
|
30588
|
+
}, async ({ docId, commentId, message, account }) => {
|
|
30589
|
+
const args = ["docs", "comments", "resolve", docId, commentId];
|
|
30590
|
+
if (message) args.push(`--message=${message}`);
|
|
30591
|
+
return runOrDiagnose(args, { account });
|
|
30592
|
+
});
|
|
30593
|
+
server2.registerTool("gog_docs_comments_delete", {
|
|
30594
|
+
description: "Delete a comment from a Google Doc. This action is permanent.",
|
|
30595
|
+
annotations: { destructiveHint: true },
|
|
30596
|
+
inputSchema: {
|
|
30597
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30598
|
+
commentId: external_exports3.string().describe("Comment ID to delete"),
|
|
30599
|
+
account: accountParam
|
|
30600
|
+
}
|
|
30601
|
+
}, async ({ docId, commentId, account }) => {
|
|
30602
|
+
return runOrDiagnose(["docs", "comments", "delete", docId, commentId], { account });
|
|
30603
|
+
});
|
|
30528
30604
|
server2.registerTool("gog_docs_run", {
|
|
30529
30605
|
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.",
|
|
30530
30606
|
annotations: { destructiveHint: true },
|
|
@@ -30890,7 +30966,7 @@ function registerTasksTools(server2) {
|
|
|
30890
30966
|
}
|
|
30891
30967
|
|
|
30892
30968
|
// src/index.ts
|
|
30893
|
-
var server = new McpServer({ name: "gogcli", version: "1.0.
|
|
30969
|
+
var server = new McpServer({ name: "gogcli", version: "1.0.5" });
|
|
30894
30970
|
registerAuthTools(server);
|
|
30895
30971
|
registerCalendarTools(server);
|
|
30896
30972
|
registerContactsTools(server);
|
|
Binary file
|
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": "1.0.
|
|
6
|
+
"version": "1.0.5",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"env": {
|
|
39
39
|
"GOG_ACCOUNT": "${user_config.gog_account}",
|
|
40
|
-
"GOG_PATH": "${user_config.gog_path}"
|
|
41
|
-
"GOG_ACCESS_TOKEN": "${user_config.gog_access_token}"
|
|
40
|
+
"GOG_PATH": "${user_config.gog_path}"
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
},
|
|
@@ -54,13 +53,6 @@
|
|
|
54
53
|
"title": "gog Executable Path",
|
|
55
54
|
"description": "Path to the gog executable (optional — defaults to 'gog' on your PATH)",
|
|
56
55
|
"required": false
|
|
57
|
-
},
|
|
58
|
-
"gog_access_token": {
|
|
59
|
-
"type": "string",
|
|
60
|
-
"title": "Google Access Token",
|
|
61
|
-
"description": "Short-lived OAuth access token (optional — bypasses stored refresh tokens, expires in ~1h). Useful for CI or automated contexts.",
|
|
62
|
-
"required": false,
|
|
63
|
-
"sensitive": true
|
|
64
56
|
}
|
|
65
57
|
},
|
|
66
58
|
"tools": [
|
|
@@ -236,6 +228,30 @@
|
|
|
236
228
|
"name": "gog_docs_structure",
|
|
237
229
|
"description": "Show document structure with numbered paragraphs"
|
|
238
230
|
},
|
|
231
|
+
{
|
|
232
|
+
"name": "gog_docs_comments_list",
|
|
233
|
+
"description": "List comments on a Google Doc"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "gog_docs_comments_get",
|
|
237
|
+
"description": "Get a single comment by ID with replies"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"name": "gog_docs_comments_add",
|
|
241
|
+
"description": "Add a comment to a Google Doc"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "gog_docs_comments_reply",
|
|
245
|
+
"description": "Reply to a comment on a Google Doc"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "gog_docs_comments_resolve",
|
|
249
|
+
"description": "Resolve (mark as done) a comment on a Google Doc"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "gog_docs_comments_delete",
|
|
253
|
+
"description": "Delete a comment from a Google Doc"
|
|
254
|
+
},
|
|
239
255
|
{
|
|
240
256
|
"name": "gog_docs_run",
|
|
241
257
|
"description": "Run any gog docs subcommand (escape hatch)"
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { registerGmailTools } from './tools/gmail.js';
|
|
|
10
10
|
import { registerSheetsTools } from './tools/sheets.js';
|
|
11
11
|
import { registerTasksTools } from './tools/tasks.js';
|
|
12
12
|
|
|
13
|
-
const server = new McpServer({ name: 'gogcli', version: '1.0.
|
|
13
|
+
const server = new McpServer({ name: 'gogcli', version: '1.0.5' });
|
|
14
14
|
|
|
15
15
|
registerAuthTools(server);
|
|
16
16
|
registerCalendarTools(server);
|
package/src/runner.ts
CHANGED
|
@@ -42,9 +42,12 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
42
42
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
43
43
|
|
|
44
44
|
return new Promise((resolve, reject) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Strip GOG_ACCESS_TOKEN so gogcli uses stored refresh tokens instead of
|
|
46
|
+
// a potentially stale direct access token passed through MCP env config.
|
|
47
|
+
const { GOG_ACCESS_TOKEN: _, ...cleanEnv } = process.env;
|
|
48
|
+
const child = spawner(process.env.GOG_PATH ?? 'gog', fullArgs, { env: cleanEnv });
|
|
49
|
+
const stdoutChunks: Buffer[] = [];
|
|
50
|
+
const stderrChunks: Buffer[] = [];
|
|
48
51
|
let settled = false;
|
|
49
52
|
|
|
50
53
|
const timer = setTimeout(() => {
|
|
@@ -53,21 +56,23 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
53
56
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
54
57
|
}, effectiveTimeout);
|
|
55
58
|
|
|
56
|
-
child.stdout!.on('data', (chunk: Buffer) => {
|
|
57
|
-
child.stderr!.on('data', (chunk: Buffer) => {
|
|
59
|
+
child.stdout!.on('data', (chunk: Buffer) => { stdoutChunks.push(chunk); });
|
|
60
|
+
child.stderr!.on('data', (chunk: Buffer) => { stderrChunks.push(chunk); });
|
|
58
61
|
|
|
59
62
|
child.on('close', (code: number | null) => {
|
|
60
63
|
clearTimeout(timer);
|
|
61
64
|
if (settled) return;
|
|
62
65
|
settled = true;
|
|
66
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
67
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
63
68
|
if (code === 0) {
|
|
64
|
-
if (interactive && stderr
|
|
69
|
+
if (interactive && stderr) {
|
|
65
70
|
resolve(stdout + '\n' + stderr);
|
|
66
71
|
} else {
|
|
67
72
|
resolve(stdout);
|
|
68
73
|
}
|
|
69
74
|
} else {
|
|
70
|
-
reject(new Error(stderr
|
|
75
|
+
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
71
76
|
}
|
|
72
77
|
});
|
|
73
78
|
|
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 } from './utils.js';
|
|
4
|
+
import { toText, toError, runOrDiagnose } from './utils.js';
|
|
5
5
|
|
|
6
6
|
export function registerAuthTools(server: McpServer): void {
|
|
7
7
|
server.registerTool('gog_auth_list', {
|
|
@@ -73,10 +73,6 @@ export function registerAuthTools(server: McpServer): void {
|
|
|
73
73
|
args: z.array(z.string()).describe('Additional positional args and flags'),
|
|
74
74
|
},
|
|
75
75
|
}, async ({ subcommand, args }) => {
|
|
76
|
-
|
|
77
|
-
return toText(await run(['auth', subcommand, ...args]));
|
|
78
|
-
} catch (err) {
|
|
79
|
-
return toError(err);
|
|
80
|
-
}
|
|
76
|
+
return runOrDiagnose(['auth', subcommand, ...args], {});
|
|
81
77
|
});
|
|
82
78
|
}
|
package/src/tools/docs.ts
CHANGED
|
@@ -27,7 +27,6 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
27
27
|
|
|
28
28
|
server.registerTool('gog_docs_create', {
|
|
29
29
|
description: 'Create a new Google Doc. Returns JSON with the new docId and URL.',
|
|
30
|
-
annotations: { destructiveHint: false },
|
|
31
30
|
inputSchema: {
|
|
32
31
|
title: z.string().describe('Title for the new document'),
|
|
33
32
|
account: accountParam,
|
|
@@ -75,6 +74,89 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
75
74
|
return runOrDiagnose(['docs', 'structure', docId], { account });
|
|
76
75
|
});
|
|
77
76
|
|
|
77
|
+
// --- Comments tools ---
|
|
78
|
+
|
|
79
|
+
server.registerTool('gog_docs_comments_list', {
|
|
80
|
+
description:
|
|
81
|
+
'List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.',
|
|
82
|
+
annotations: { readOnlyHint: true },
|
|
83
|
+
inputSchema: {
|
|
84
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
85
|
+
includeResolved: z.boolean().optional().describe('Include resolved comments (default: false, open only)'),
|
|
86
|
+
account: accountParam,
|
|
87
|
+
},
|
|
88
|
+
}, async ({ docId, includeResolved, account }) => {
|
|
89
|
+
const args = ['docs', 'comments', 'list', docId];
|
|
90
|
+
if (includeResolved) args.push('--include-resolved');
|
|
91
|
+
return runOrDiagnose(args, { account });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
server.registerTool('gog_docs_comments_get', {
|
|
95
|
+
description: 'Get a single comment by ID, including its replies.',
|
|
96
|
+
annotations: { readOnlyHint: true },
|
|
97
|
+
inputSchema: {
|
|
98
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
99
|
+
commentId: z.string().describe('Comment ID'),
|
|
100
|
+
account: accountParam,
|
|
101
|
+
},
|
|
102
|
+
}, async ({ docId, commentId, account }) => {
|
|
103
|
+
return runOrDiagnose(['docs', 'comments', 'get', docId, commentId], { account });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.registerTool('gog_docs_comments_add', {
|
|
107
|
+
description:
|
|
108
|
+
'Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
111
|
+
content: z.string().describe('Comment text'),
|
|
112
|
+
quoted: z.string().optional().describe('Quoted text to attach to the comment (shown in UIs when available)'),
|
|
113
|
+
account: accountParam,
|
|
114
|
+
},
|
|
115
|
+
}, async ({ docId, content, quoted, account }) => {
|
|
116
|
+
const args = ['docs', 'comments', 'add', docId, content];
|
|
117
|
+
if (quoted) args.push(`--quoted=${quoted}`);
|
|
118
|
+
return runOrDiagnose(args, { account });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.registerTool('gog_docs_comments_reply', {
|
|
122
|
+
description: 'Reply to an existing comment on a Google Doc.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
125
|
+
commentId: z.string().describe('Comment ID to reply to'),
|
|
126
|
+
content: z.string().describe('Reply text'),
|
|
127
|
+
account: accountParam,
|
|
128
|
+
},
|
|
129
|
+
}, async ({ docId, commentId, content, account }) => {
|
|
130
|
+
return runOrDiagnose(['docs', 'comments', 'reply', docId, commentId, content], { account });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
server.registerTool('gog_docs_comments_resolve', {
|
|
134
|
+
description: 'Resolve a comment (mark as done). Optionally include a closing message.',
|
|
135
|
+
annotations: { destructiveHint: true },
|
|
136
|
+
inputSchema: {
|
|
137
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
138
|
+
commentId: z.string().describe('Comment ID to resolve'),
|
|
139
|
+
message: z.string().optional().describe('Optional message to include when resolving'),
|
|
140
|
+
account: accountParam,
|
|
141
|
+
},
|
|
142
|
+
}, async ({ docId, commentId, message, account }) => {
|
|
143
|
+
const args = ['docs', 'comments', 'resolve', docId, commentId];
|
|
144
|
+
if (message) args.push(`--message=${message}`);
|
|
145
|
+
return runOrDiagnose(args, { account });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.registerTool('gog_docs_comments_delete', {
|
|
149
|
+
description: 'Delete a comment from a Google Doc. This action is permanent.',
|
|
150
|
+
annotations: { destructiveHint: true },
|
|
151
|
+
inputSchema: {
|
|
152
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
153
|
+
commentId: z.string().describe('Comment ID to delete'),
|
|
154
|
+
account: accountParam,
|
|
155
|
+
},
|
|
156
|
+
}, async ({ docId, commentId, account }) => {
|
|
157
|
+
return runOrDiagnose(['docs', 'comments', 'delete', docId, commentId], { account });
|
|
158
|
+
});
|
|
159
|
+
|
|
78
160
|
server.registerTool('gog_docs_run', {
|
|
79
161
|
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.',
|
|
80
162
|
annotations: { destructiveHint: true },
|
package/src/tools/utils.ts
CHANGED
|
@@ -15,8 +15,12 @@ export function toError(err: unknown): ToolResult {
|
|
|
15
15
|
return toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
|
|
19
|
+
|
|
20
|
+
const AUTH_HINT =
|
|
21
|
+
'\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. ' +
|
|
22
|
+
'Ask the user if they would like to re-authenticate.';
|
|
23
|
+
|
|
20
24
|
export async function runOrDiagnose(
|
|
21
25
|
args: string[],
|
|
22
26
|
options: { account?: string },
|
|
@@ -25,11 +29,14 @@ export async function runOrDiagnose(
|
|
|
25
29
|
return toText(await run(args, options));
|
|
26
30
|
} catch (err) {
|
|
27
31
|
const base = toError(err);
|
|
32
|
+
const errText = base.content[0].text;
|
|
33
|
+
const isAuthError = AUTH_ERROR_PATTERN.test(errText);
|
|
34
|
+
const hint = isAuthError ? AUTH_HINT : '';
|
|
28
35
|
try {
|
|
29
36
|
const accounts = await run(['auth', 'list']);
|
|
30
|
-
return toText(`${
|
|
37
|
+
return toText(`${errText}\n\nConfigured accounts:\n${accounts}${hint}`);
|
|
31
38
|
} catch {
|
|
32
|
-
return
|
|
39
|
+
return toText(`${errText}${hint}`);
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
}
|
package/tests/runner.test.ts
CHANGED
|
@@ -300,6 +300,23 @@ describe('run', () => {
|
|
|
300
300
|
vi.useRealTimers();
|
|
301
301
|
});
|
|
302
302
|
|
|
303
|
+
it('strips GOG_ACCESS_TOKEN from child environment to force refresh-token auth', async () => {
|
|
304
|
+
const spawner = makeSpawner(0, '{}');
|
|
305
|
+
const originalToken = process.env.GOG_ACCESS_TOKEN;
|
|
306
|
+
process.env.GOG_ACCESS_TOKEN = 'stale-token-from-mcp-config';
|
|
307
|
+
try {
|
|
308
|
+
await run(['docs', 'comments', 'list', 'docId'], { spawner });
|
|
309
|
+
const envPassed = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv;
|
|
310
|
+
expect(envPassed.GOG_ACCESS_TOKEN).toBeUndefined();
|
|
311
|
+
} finally {
|
|
312
|
+
if (originalToken === undefined) {
|
|
313
|
+
delete process.env.GOG_ACCESS_TOKEN;
|
|
314
|
+
} else {
|
|
315
|
+
process.env.GOG_ACCESS_TOKEN = originalToken;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
303
320
|
it('ignores timeout if close event already settled the promise', async () => {
|
|
304
321
|
vi.useFakeTimers();
|
|
305
322
|
const spawner = vi.fn(() => {
|
package/tests/tools/auth.test.ts
CHANGED
|
@@ -121,7 +121,7 @@ describe('gog_auth_run', () => {
|
|
|
121
121
|
vi.mocked(runner.run).mockResolvedValue('removed user@gmail.com');
|
|
122
122
|
const handlers = setupHandlers();
|
|
123
123
|
const result = await handlers.get('gog_auth_run')!({ subcommand: 'remove', args: ['user@gmail.com'] });
|
|
124
|
-
expect(runner.run).toHaveBeenCalledWith(['auth', 'remove', 'user@gmail.com']);
|
|
124
|
+
expect(runner.run).toHaveBeenCalledWith(['auth', 'remove', 'user@gmail.com'], {});
|
|
125
125
|
expect(result.content[0].text).toBe('removed user@gmail.com');
|
|
126
126
|
});
|
|
127
127
|
|
|
@@ -129,7 +129,7 @@ describe('gog_auth_run', () => {
|
|
|
129
129
|
vi.mocked(runner.run).mockResolvedValue('token info');
|
|
130
130
|
const handlers = setupHandlers();
|
|
131
131
|
await handlers.get('gog_auth_run')!({ subcommand: 'tokens', args: [] });
|
|
132
|
-
expect(runner.run).toHaveBeenCalledWith(['auth', 'tokens']);
|
|
132
|
+
expect(runner.run).toHaveBeenCalledWith(['auth', 'tokens'], {});
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('returns error text on failure', async () => {
|
package/tests/tools/docs.test.ts
CHANGED
|
@@ -161,6 +161,193 @@ describe('gog_docs_structure', () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
// --- Comments tools ---
|
|
165
|
+
|
|
166
|
+
describe('gog_docs_comments_list', () => {
|
|
167
|
+
it('calls run with correct args for open comments', async () => {
|
|
168
|
+
vi.mocked(runner.run).mockResolvedValue('[{"id":"c1","content":"Fix this"}]');
|
|
169
|
+
const handlers = setupHandlers();
|
|
170
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'abc' });
|
|
171
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
172
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('includes --include-resolved when set', async () => {
|
|
176
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
177
|
+
const handlers = setupHandlers();
|
|
178
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: true });
|
|
179
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
180
|
+
['docs', 'comments', 'list', 'abc', '--include-resolved'],
|
|
181
|
+
{ account: undefined },
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('omits --include-resolved when false', async () => {
|
|
186
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
187
|
+
const handlers = setupHandlers();
|
|
188
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: false });
|
|
189
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('forwards account override', async () => {
|
|
193
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
194
|
+
const handlers = setupHandlers();
|
|
195
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', account: 'other@gmail.com' });
|
|
196
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
197
|
+
['docs', 'comments', 'list', 'abc'],
|
|
198
|
+
{ account: 'other@gmail.com' },
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns error text on failure', async () => {
|
|
203
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
|
|
204
|
+
const handlers = setupHandlers();
|
|
205
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'bad' });
|
|
206
|
+
expect(result.content[0].text).toContain('Error: List failed');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('gog_docs_comments_get', () => {
|
|
211
|
+
it('calls run with correct args', async () => {
|
|
212
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c1","content":"Fix this","replies":[]}');
|
|
213
|
+
const handlers = setupHandlers();
|
|
214
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'c1' });
|
|
215
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'get', 'abc', 'c1'], { account: undefined });
|
|
216
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns error text on failure', async () => {
|
|
220
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
|
|
221
|
+
const handlers = setupHandlers();
|
|
222
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'bad' });
|
|
223
|
+
expect(result.content[0].text).toContain('Error: Not found');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('gog_docs_comments_add', () => {
|
|
228
|
+
it('calls run with content', async () => {
|
|
229
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
230
|
+
const handlers = setupHandlers();
|
|
231
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Please review' });
|
|
232
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
233
|
+
['docs', 'comments', 'add', 'abc', 'Please review'],
|
|
234
|
+
{ account: undefined },
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('includes --quoted when provided', async () => {
|
|
239
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
240
|
+
const handlers = setupHandlers();
|
|
241
|
+
await handlers.get('gog_docs_comments_add')!({
|
|
242
|
+
docId: 'abc',
|
|
243
|
+
content: 'Typo here',
|
|
244
|
+
quoted: 'teh',
|
|
245
|
+
});
|
|
246
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
247
|
+
['docs', 'comments', 'add', 'abc', 'Typo here', '--quoted=teh'],
|
|
248
|
+
{ account: undefined },
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('omits --quoted when not provided', async () => {
|
|
253
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
254
|
+
const handlers = setupHandlers();
|
|
255
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Nice' });
|
|
256
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
257
|
+
['docs', 'comments', 'add', 'abc', 'Nice'],
|
|
258
|
+
{ account: undefined },
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('returns error text on failure', async () => {
|
|
263
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Add failed'));
|
|
264
|
+
const handlers = setupHandlers();
|
|
265
|
+
const result = await handlers.get('gog_docs_comments_add')!({ docId: 'bad', content: 'x' });
|
|
266
|
+
expect(result.content[0].text).toContain('Error: Add failed');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('gog_docs_comments_reply', () => {
|
|
271
|
+
it('calls run with correct args', async () => {
|
|
272
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"r1"}');
|
|
273
|
+
const handlers = setupHandlers();
|
|
274
|
+
await handlers.get('gog_docs_comments_reply')!({ docId: 'abc', commentId: 'c1', content: 'Done' });
|
|
275
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
276
|
+
['docs', 'comments', 'reply', 'abc', 'c1', 'Done'],
|
|
277
|
+
{ account: undefined },
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns error text on failure', async () => {
|
|
282
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Reply failed'));
|
|
283
|
+
const handlers = setupHandlers();
|
|
284
|
+
const result = await handlers.get('gog_docs_comments_reply')!({
|
|
285
|
+
docId: 'abc', commentId: 'c1', content: 'x',
|
|
286
|
+
});
|
|
287
|
+
expect(result.content[0].text).toContain('Error: Reply failed');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('gog_docs_comments_resolve', () => {
|
|
292
|
+
it('calls run with correct args', async () => {
|
|
293
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
294
|
+
const handlers = setupHandlers();
|
|
295
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
296
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
297
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
298
|
+
{ account: undefined },
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('includes --message when provided', async () => {
|
|
303
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
304
|
+
const handlers = setupHandlers();
|
|
305
|
+
await handlers.get('gog_docs_comments_resolve')!({
|
|
306
|
+
docId: 'abc', commentId: 'c1', message: 'Fixed in v2',
|
|
307
|
+
});
|
|
308
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
309
|
+
['docs', 'comments', 'resolve', 'abc', 'c1', '--message=Fixed in v2'],
|
|
310
|
+
{ account: undefined },
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('omits --message when not provided', async () => {
|
|
315
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
316
|
+
const handlers = setupHandlers();
|
|
317
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
318
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
319
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
320
|
+
{ account: undefined },
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns error text on failure', async () => {
|
|
325
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Resolve failed'));
|
|
326
|
+
const handlers = setupHandlers();
|
|
327
|
+
const result = await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
328
|
+
expect(result.content[0].text).toContain('Error: Resolve failed');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('gog_docs_comments_delete', () => {
|
|
333
|
+
it('calls run with correct args', async () => {
|
|
334
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
335
|
+
const handlers = setupHandlers();
|
|
336
|
+
await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
337
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
338
|
+
['docs', 'comments', 'delete', 'abc', 'c1'],
|
|
339
|
+
{ account: undefined },
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('returns error text on failure', async () => {
|
|
344
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Delete failed'));
|
|
345
|
+
const handlers = setupHandlers();
|
|
346
|
+
const result = await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
347
|
+
expect(result.content[0].text).toContain('Error: Delete failed');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
164
351
|
describe('gog_docs_run', () => {
|
|
165
352
|
it('passes raw subcommand and args to runner', async () => {
|
|
166
353
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as runner from '../../src/runner.js';
|
|
3
|
+
import { runOrDiagnose } from '../../src/tools/utils.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../src/runner.js');
|
|
6
|
+
|
|
7
|
+
beforeEach(() => vi.clearAllMocks());
|
|
8
|
+
|
|
9
|
+
describe('runOrDiagnose', () => {
|
|
10
|
+
it('returns output text on success', async () => {
|
|
11
|
+
vi.mocked(runner.run).mockResolvedValue('{"ok":true}');
|
|
12
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
13
|
+
expect(result.content[0].text).toBe('{"ok":true}');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('appends auth list on non-auth failure', async () => {
|
|
17
|
+
vi.mocked(runner.run)
|
|
18
|
+
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
19
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
20
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
21
|
+
expect(result.content[0].text).toBe(
|
|
22
|
+
'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
|
|
23
|
+
);
|
|
24
|
+
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('appends re-auth hint on 401 error', async () => {
|
|
28
|
+
vi.mocked(runner.run)
|
|
29
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
30
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
31
|
+
const result = await runOrDiagnose(['docs', 'comments', 'list', 'abc'], {});
|
|
32
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
33
|
+
expect(result.content[0].text).toContain('Configured accounts:\nuser@gmail.com');
|
|
34
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('appends re-auth hint on "unauthorized" error', async () => {
|
|
38
|
+
vi.mocked(runner.run)
|
|
39
|
+
.mockRejectedValueOnce(new Error('unauthorized access'))
|
|
40
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
41
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
42
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('appends re-auth hint on "token expired" error', async () => {
|
|
46
|
+
vi.mocked(runner.run)
|
|
47
|
+
.mockRejectedValueOnce(new Error('token has been expired or revoked'))
|
|
48
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
49
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
50
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('appends re-auth hint on "invalid_grant" error', async () => {
|
|
54
|
+
vi.mocked(runner.run)
|
|
55
|
+
.mockRejectedValueOnce(new Error('invalid_grant'))
|
|
56
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
57
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
58
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns plain error with auth hint when auth list also fails on auth error', async () => {
|
|
62
|
+
vi.mocked(runner.run)
|
|
63
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
64
|
+
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
65
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
66
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
67
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
68
|
+
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns plain error when auth list also fails on non-auth error', async () => {
|
|
72
|
+
vi.mocked(runner.run)
|
|
73
|
+
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
74
|
+
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
75
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
76
|
+
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
77
|
+
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
78
|
+
});
|
|
79
|
+
});
|