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.0" : "0.0.0";
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 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. Nothing was lost. Run gog_gmail_drafts_list to find the current id, or gog_gmail_drafts_get on ${draftId} to confirm.`
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.0",
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.0",
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>",
@@ -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
- function withRefetchNote(written: CallToolResult, draftId: string): CallToolResult {
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
- 'read-back requested by returnFull could not be performed — the id did not resolve, which on ' +
2919
- 'this mailbox usually means the draft was forked by a mail client between the write and the ' +
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)'));