gogcli-mcp-gmail 2.23.0 → 2.23.1
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/dist/index.js
CHANGED
|
@@ -31896,7 +31896,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
31896
31896
|
);
|
|
31897
31897
|
|
|
31898
31898
|
// ../gogcli-mcp/src/server.ts
|
|
31899
|
-
var VERSION = true ? "2.23.
|
|
31899
|
+
var VERSION = true ? "2.23.1" : "0.0.0";
|
|
31900
31900
|
|
|
31901
31901
|
// ../gogcli-mcp/src/auth-log.ts
|
|
31902
31902
|
var FAILURES = /* @__PURE__ */ new Set([
|
|
@@ -33875,14 +33875,17 @@ function registerExtraGmailTools(server) {
|
|
|
33875
33875
|
if (f.from) args.push(`--from=${f.from}`);
|
|
33876
33876
|
args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
|
|
33877
33877
|
}
|
|
33878
|
-
function withRefetchNote(written, draftId) {
|
|
33878
|
+
function withRefetchNote(written, draftId, refetch) {
|
|
33879
|
+
const detail = resultText(refetch)?.trim();
|
|
33880
|
+
const looksForked = detail !== void 0 && DRAFT_NOT_FOUND_PATTERN.test(detail);
|
|
33881
|
+
const because = looksForked ? "the id did not resolve, which on this mailbox usually means the draft was forked by a mail client between the write and the read. Run gog_gmail_drafts_list to find the current id" : `the read failed for a different reason, reported verbatim here: ${detail ?? "(no detail supplied)"}. That is a failure of the READ ONLY`;
|
|
33879
33882
|
return {
|
|
33880
33883
|
...written,
|
|
33881
33884
|
content: [
|
|
33882
33885
|
...written.content,
|
|
33883
33886
|
{
|
|
33884
33887
|
type: "text",
|
|
33885
|
-
text: `Note: the write to draft ${draftId} SUCCEEDED and is acknowledged above. The follow-up read-back requested by returnFull could not be performed \u2014
|
|
33888
|
+
text: `Note: the write to draft ${draftId} SUCCEEDED and is acknowledged above. The follow-up read-back requested by returnFull could not be performed \u2014 ${because}. Nothing was lost; run gog_gmail_drafts_get on ${draftId} to confirm the saved content.`
|
|
33886
33889
|
}
|
|
33887
33890
|
]
|
|
33888
33891
|
};
|
|
@@ -33904,7 +33907,7 @@ function registerExtraGmailTools(server) {
|
|
|
33904
33907
|
if (draftId) {
|
|
33905
33908
|
const refetched = await runOrDiagnose(["gmail", "drafts", "get", draftId, "--use-indexed-attachment-ids=false"], { account });
|
|
33906
33909
|
if (refetched.isError !== true) final = refetched;
|
|
33907
|
-
else final = withRefetchNote(result, draftId);
|
|
33910
|
+
else final = withRefetchNote(result, draftId, refetched);
|
|
33908
33911
|
}
|
|
33909
33912
|
}
|
|
33910
33913
|
if (!verification) return final;
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-gmail",
|
|
5
5
|
"display_name": "gogcli (Gmail)",
|
|
6
|
-
"version": "2.23.
|
|
6
|
+
"version": "2.23.1",
|
|
7
7
|
"description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-gmail",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-gmail",
|
|
5
5
|
"description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/src/tools/gmail-extra.ts
CHANGED
|
@@ -2905,8 +2905,22 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
2905
2905
|
// the raw write result if the id can't be determined.
|
|
2906
2906
|
/** The write succeeded; only the `returnFull` re-read did not. Hand back the
|
|
2907
2907
|
* acknowledgement, and say plainly which half failed — a caller that cannot
|
|
2908
|
-
* tell those apart will either re-send or delete the wrong copy.
|
|
2909
|
-
|
|
2908
|
+
* tell those apart will either re-send or delete the wrong copy.
|
|
2909
|
+
*
|
|
2910
|
+
* A missing id and any OTHER read failure are different stories and get
|
|
2911
|
+
* different text. Gating on DRAFT_NOT_FOUND_PATTERN — the same test
|
|
2912
|
+
* forkAwareDraftFailure uses — keeps the fork explanation for the case that
|
|
2913
|
+
* actually looks like one; a permission error, a timeout or a transport fault
|
|
2914
|
+
* keeps its own message instead of being retold as a fork, which would both
|
|
2915
|
+
* mislead and discard the only text saying what really went wrong. */
|
|
2916
|
+
function withRefetchNote(written: CallToolResult, draftId: string, refetch: CallToolResult): CallToolResult {
|
|
2917
|
+
const detail = resultText(refetch)?.trim();
|
|
2918
|
+
const looksForked = detail !== undefined && DRAFT_NOT_FOUND_PATTERN.test(detail);
|
|
2919
|
+
const because = looksForked
|
|
2920
|
+
? 'the id did not resolve, which on this mailbox usually means the draft was forked by a mail client ' +
|
|
2921
|
+
'between the write and the read. Run gog_gmail_drafts_list to find the current id'
|
|
2922
|
+
: `the read failed for a different reason, reported verbatim here: ${detail ?? '(no detail supplied)'}. ` +
|
|
2923
|
+
'That is a failure of the READ ONLY';
|
|
2910
2924
|
return {
|
|
2911
2925
|
...written,
|
|
2912
2926
|
content: [
|
|
@@ -2915,10 +2929,8 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
2915
2929
|
type: 'text' as const,
|
|
2916
2930
|
text:
|
|
2917
2931
|
`Note: the write to draft ${draftId} SUCCEEDED and is acknowledged above. The follow-up ` +
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
`read. Nothing was lost. Run gog_gmail_drafts_list to find the current id, or ` +
|
|
2921
|
-
`gog_gmail_drafts_get on ${draftId} to confirm.`,
|
|
2932
|
+
`read-back requested by returnFull could not be performed — ${because}. Nothing was lost; ` +
|
|
2933
|
+
`run gog_gmail_drafts_get on ${draftId} to confirm the saved content.`,
|
|
2922
2934
|
},
|
|
2923
2935
|
],
|
|
2924
2936
|
};
|
|
@@ -2969,7 +2981,7 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
2969
2981
|
// most destructive thing this tool can say to someone about to tidy up
|
|
2970
2982
|
// the sibling copy.
|
|
2971
2983
|
if (refetched.isError !== true) final = refetched;
|
|
2972
|
-
else final = withRefetchNote(result, draftId);
|
|
2984
|
+
else final = withRefetchNote(result, draftId, refetched);
|
|
2973
2985
|
}
|
|
2974
2986
|
}
|
|
2975
2987
|
if (!verification) return final;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { registerExtraGmailTools } from '../../src/tools/gmail-extra.js';
|
|
3
|
+
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
+
import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { rawTextResult, errorResult } from '@chrischall/mcp-utils';
|
|
6
|
+
|
|
7
|
+
vi.mock('../../../gogcli-mcp/src/lib.js', async (o) => ({ ...(await o<typeof lib>()), run: vi.fn(), runOrDiagnose: vi.fn(), diagnose: vi.fn() }));
|
|
8
|
+
|
|
9
|
+
let harness: TestHarness;
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
13
|
+
vi.mocked(lib.diagnose).mockResolvedValue(errorResult('diagnosed'));
|
|
14
|
+
harness = await createTestHarness(registerExtraGmailTools);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const b64 = (t: string) => Buffer.from(t, 'utf8').toString('base64url');
|
|
18
|
+
const draft = (id: string, body: string) => JSON.stringify({
|
|
19
|
+
draft: {
|
|
20
|
+
id,
|
|
21
|
+
message: { id: `m${id}`, threadId: `t${id}`, payload: { mimeType: 'text/plain', headers: [{ name: 'Subject', value: 'S' }], body: { data: b64(body) } } },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* #264: bodyLineCount counted RAW normalized lines while diffBodyLines compares
|
|
27
|
+
* Set members, so onlyInACount + sharedLineCount === bodyLineCount stopped
|
|
28
|
+
* holding the moment a body repeated a line. Dividers repeat constantly in real
|
|
29
|
+
* mail, so this is the ordinary case, not an edge one.
|
|
30
|
+
*
|
|
31
|
+
* Asserted through the TOOL, against the emitted payload, because
|
|
32
|
+
* describeDraftSide is module-private — and because the payload is what a
|
|
33
|
+
* caller actually does arithmetic on.
|
|
34
|
+
*/
|
|
35
|
+
const A = ['Intro paragraph.', '---', 'Middle paragraph.', '---', 'Closing paragraph.'].join('\n');
|
|
36
|
+
const B = ['Intro paragraph.', '---', 'A different middle.', '---', 'Closing paragraph.'].join('\n');
|
|
37
|
+
|
|
38
|
+
describe('gog_gmail_drafts_diff count arithmetic', () => {
|
|
39
|
+
it('onlyInACount + sharedLineCount === bodyLineCount when a body repeats a line', async () => {
|
|
40
|
+
vi.mocked(lib.run).mockImplementation(async (args: readonly string[]) =>
|
|
41
|
+
draft(String(args[3]), String(args[3]) === 'a1' ? A : B));
|
|
42
|
+
|
|
43
|
+
const res = await harness.callTool('gog_gmail_drafts_diff', { draftIdA: 'a1', draftIdB: 'b1' });
|
|
44
|
+
const raw = res.content.map((c: any) => c.text).join('\n');
|
|
45
|
+
const payload = JSON.parse(raw);
|
|
46
|
+
|
|
47
|
+
const sideA = payload.drafts.a;
|
|
48
|
+
const diff = payload.bodyDiff;
|
|
49
|
+
|
|
50
|
+
expect(sideA.bodyLineCount).toBeDefined();
|
|
51
|
+
expect(diff.onlyInACount).toBeDefined();
|
|
52
|
+
expect(diff.sharedLineCount).toBeDefined();
|
|
53
|
+
// The body has 5 raw lines but only 4 distinct ones — that gap is the bug.
|
|
54
|
+
expect(sideA.bodyLineCount).toBe(4);
|
|
55
|
+
expect(diff.onlyInACount + diff.sharedLineCount).toBe(sideA.bodyLineCount);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -53,6 +53,43 @@ describe('a failed returnFull re-read does not erase a successful write', () =>
|
|
|
53
53
|
expect(res.content.map((c: any) => c.text).join('\n')).toContain('payload');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('does NOT retell a non-404 read failure as a fork, and keeps its text', async () => {
|
|
57
|
+
// #264: any refetch failure used to get the fork explanation, discarding the
|
|
58
|
+
// only text saying what actually went wrong. A permission error is not a fork.
|
|
59
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: readonly string[]) =>
|
|
60
|
+
args[2] === 'update' ? rawTextResult(ack) : errorResult('Google API error (403 forbidden): insufficient permission'));
|
|
61
|
+
|
|
62
|
+
const res = await harness.callTool('gog_gmail_drafts_update', { draftId: 'r123', subject: 'S', body: 'x', returnFull: true });
|
|
63
|
+
const text = res.content.map((c: any) => c.text).join('\n');
|
|
64
|
+
|
|
65
|
+
expect(res.isError).not.toBe(true);
|
|
66
|
+
expect(text).toContain('SUCCEEDED');
|
|
67
|
+
expect(text).toContain('403 forbidden'); // the real cause survives
|
|
68
|
+
expect(text).not.toMatch(/forked by a mail client/); // and is not retold as a fork
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('still gives the fork explanation on a genuine 404', async () => {
|
|
72
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: readonly string[]) =>
|
|
73
|
+
args[2] === 'update' ? rawTextResult(ack) : errorResult('Google API error (404 notFound)'));
|
|
74
|
+
const res = await harness.callTool('gog_gmail_drafts_update', { draftId: 'r123', subject: 'S', body: 'x', returnFull: true });
|
|
75
|
+
expect(res.content.map((c: any) => c.text).join('\n')).toMatch(/forked by a mail client/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('still says the write succeeded when the read failure carries no text', async () => {
|
|
79
|
+
// An error result with no content at all: the note must not interpolate
|
|
80
|
+
// `undefined` into the sentence a caller reads to decide what to do next.
|
|
81
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: readonly string[]) =>
|
|
82
|
+
args[2] === 'update' ? rawTextResult(ack) : ({ isError: true, content: [] } as any));
|
|
83
|
+
|
|
84
|
+
const res = await harness.callTool('gog_gmail_drafts_update', { draftId: 'r123', subject: 'S', body: 'x', returnFull: true });
|
|
85
|
+
const text = res.content.map((c: any) => c.text).join('\n');
|
|
86
|
+
|
|
87
|
+
expect(res.isError).not.toBe(true);
|
|
88
|
+
expect(text).toContain('SUCCEEDED');
|
|
89
|
+
expect(text).toContain('(no detail supplied)');
|
|
90
|
+
expect(text).not.toContain('undefined');
|
|
91
|
+
});
|
|
92
|
+
|
|
56
93
|
it('still reports a genuinely failed WRITE as an error', async () => {
|
|
57
94
|
// The guard must not swallow a real failure: here the update itself fails.
|
|
58
95
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(errorResult('Google API error (404 notFound)'));
|