gogcli-mcp 4.3.0 → 4.5.0

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/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": "4.3.0",
6
+ "version": "4.5.0",
7
7
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp",
5
5
  "description": "MCP server wrapping gogcli for Google service access",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -41,7 +41,7 @@
41
41
  "test:coverage": "vitest run --coverage"
42
42
  },
43
43
  "dependencies": {
44
- "@chrischall/mcp-utils": "^2.4.0",
44
+ "@chrischall/mcp-utils": "^2.6.0",
45
45
  "@modelcontextprotocol/server": "^2.0.0",
46
46
  "zod": "^4.6.1"
47
47
  },
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp"
9
9
  },
10
- "version": "4.3.0",
10
+ "version": "4.5.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "4.3.0",
15
+ "version": "4.5.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -0,0 +1,204 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import type { CallToolResult, InputRequiredResult, ServerContext } from '@modelcontextprotocol/server';
4
+ import {
5
+ CONFIRM_TOKEN_INSTRUCTION,
6
+ confirmationFromEnv,
7
+ readEnvVar,
8
+ requireConfirmationWithFallback,
9
+ type ConfirmSubject,
10
+ } from '@chrischall/mcp-utils';
11
+ import { confirmSpentStore } from './send-confirm-token.js';
12
+
13
+ // ============================================================================
14
+ // THE DISPATCH RAIL, service-neutral: every tool that reaches another person
15
+ // (Gmail sends, Chat posts, guest-visible Calendar changes, Drive shares,
16
+ // Classroom announcements and invitations) asks the user first.
17
+ //
18
+ // Elicitation is primary: the host shows the preview and the model never holds
19
+ // the approval. On a client that declares no elicitation (claude.ai, measured),
20
+ // the call site's `DispatchTokenFallback` runs the two-phase preview +
21
+ // confirmToken flow instead (mcp-utils' confirmationFromEnv +
22
+ // requireConfirmationWithFallback), as the fleet's MCP_CONFIRM_MODE says:
23
+ // ask-user (default), auto, or refuse. A call site with no fallback (the
24
+ // forwarding filter) is always refused on such a client.
25
+ // ============================================================================
26
+
27
+ // Bound on the body text shown in a confirmation prompt. Enough to read what is
28
+ // actually being sent (the point of SEC-5), small enough to keep the prompt a
29
+ // prompt rather than a copy of the message.
30
+ export const BODY_PREVIEW_MAX = 2048;
31
+
32
+ /** The first BODY_PREVIEW_MAX characters of a body, marked when cut. */
33
+ export function bodyPreview(text: string | undefined): string | undefined {
34
+ if (!text) return undefined;
35
+ if (text.length <= BODY_PREVIEW_MAX) return text;
36
+ return `${text.slice(0, BODY_PREVIEW_MAX)}… [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
37
+ }
38
+
39
+ /**
40
+ * Every attachment a dispatch will carry: a server path in full (so the user
41
+ * sees WHICH file on the gog host is leaving), an inline one by its filename.
42
+ */
43
+ export function attachmentNames(
44
+ paths: readonly string[] | undefined,
45
+ inline: ReadonlyArray<{ filename: string }> | undefined,
46
+ ): string[] {
47
+ return [...(paths ?? []), ...(inline ?? []).map((a) => a.filename)];
48
+ }
49
+
50
+ /** One attachment as the fallback preview shows it and its hash binds it. */
51
+ export type AttachmentDetail = { name: string; size: number | null; sha256?: string };
52
+
53
+ /**
54
+ * Name, byte size and content SHA-256 of every file a dispatch carries, for the
55
+ * token fallback's preview and payload hash. A server path is read (it is
56
+ * already confined to GOG_FILE_ROOTS) so a same-size swap between the phases is
57
+ * still a changed payload; an unreadable one reports `size: null` and gog will
58
+ * fail on it anyway.
59
+ */
60
+ export function attachmentDetails(
61
+ paths: readonly string[] | undefined,
62
+ inline: ReadonlyArray<{ filename: string; contentBase64: string }> | undefined,
63
+ ): AttachmentDetail[] {
64
+ const out: AttachmentDetail[] = [];
65
+ for (const path of paths ?? []) {
66
+ let bytes: Buffer | undefined;
67
+ try {
68
+ bytes = readFileSync(path);
69
+ } catch {
70
+ bytes = undefined;
71
+ }
72
+ out.push(bytes
73
+ ? { name: path, size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
74
+ : { name: path, size: null });
75
+ }
76
+ for (const a of inline ?? []) {
77
+ out.push({
78
+ name: a.filename,
79
+ size: Buffer.from(a.contentBase64, 'base64').length,
80
+ sha256: createHash('sha256').update(a.contentBase64).digest('hex'),
81
+ });
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** Who a dispatch goes out as, for the fallback preview: an explicit alias, else the account. */
87
+ export function senderPreview(account: string | undefined, from?: string): string {
88
+ return from ?? account ?? readEnvVar('GOG_ACCOUNT') ?? "the gog account's default address";
89
+ }
90
+
91
+ /** Drop the fingerprint for display: the user needs a name and a size. */
92
+ export function attachmentPreview(details: readonly AttachmentDetail[]): Array<{ name: string; size: number | null }> {
93
+ return details.map(({ name, size }) => ({ name, size }));
94
+ }
95
+
96
+ /** The instruction a mail dispatch's phase 1 carries — verbatim from the spec that introduced the fallback. */
97
+ export const CONFIRM_SEND_INSTRUCTION =
98
+ 'Show this preview to the user verbatim and send only after they explicitly approve in chat. '
99
+ + 'Then call again with confirmToken.';
100
+
101
+ /** The same instruction for a dispatch that is not mail (a share, an invitation, a post). */
102
+ export const CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
103
+
104
+ /** Appended to each gated tool's description. */
105
+ export const CONFIRM_FALLBACK_DESCRIPTION =
106
+ ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai), a two-step flow applies instead: call '
107
+ + 'WITHOUT confirmToken and nothing is sent or changed — the result has status "confirmation-required", the full '
108
+ + 'preview and a confirmToken. Follow its instruction (by default: show the preview to the user verbatim and only '
109
+ + 'after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken). The tool re-reads '
110
+ + 'what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / '
111
+ + 'TOKEN_REUSED / TOKEN_INVALID also do nothing. A server set to MCP_CONFIRM_MODE=refuse refuses instead.';
112
+
113
+ // The schema input every gated tool adds: mcp-utils' own, so its wording and
114
+ // the helper that reads it cannot drift apart.
115
+ export { confirmTokenParam } from '@chrischall/mcp-utils';
116
+
117
+ /** What the fallback binds a token to — recomputed from a fresh read on every call. */
118
+ export type TokenSubject = ConfirmSubject;
119
+
120
+ /**
121
+ * Opt-in second rail for a client that cannot be prompted. `subject` is only
122
+ * called when the fallback actually runs, so a tool may do an extra read there
123
+ * without changing the elicitation path at all. It may return an error result
124
+ * (a failed read), which is passed back unchanged.
125
+ */
126
+ export interface DispatchTokenFallback {
127
+ tool: string;
128
+ account?: string;
129
+ confirmToken?: string;
130
+ /** Phase 1's instruction to the model. Defaults to {@link CONFIRM_ACTION_INSTRUCTION}. */
131
+ instruction?: string;
132
+ subject: () => TokenSubject | CallToolResult | Promise<TokenSubject | CallToolResult>;
133
+ }
134
+
135
+ /**
136
+ * The refusal an escape hatch (`gog_<service>_run`, `gog_api_call`) gives for an
137
+ * action a dedicated tool gates. Without it, the run tool is a way around the
138
+ * rail: the model forwards the same subcommand and nobody is asked (#400).
139
+ */
140
+ export function gatedElsewhere(what: string, via: string, does: string, tool: string): string {
141
+ return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
142
+ }
143
+
144
+ /** True when any forwarded token is one of `words` (kong lets flags precede the command word). */
145
+ export function hasCommandWord(args: readonly string[], words: ReadonlySet<string>): string | undefined {
146
+ return args.find((a) => words.has(a.toLowerCase()));
147
+ }
148
+
149
+ export interface DispatchConfirmationOptions {
150
+ /** Stable id of the dispatch, echoed in every result (`gmail.send`, `drive.share`, …). */
151
+ action: string;
152
+ /** Heading of the elicitation prompt. */
153
+ message: string;
154
+ /** Label beside the prompt's confirmation checkbox. */
155
+ confirmationLabel: string;
156
+ /** What the elicitation prompt shows. */
157
+ details: Record<string, unknown>;
158
+ /** The way through on a client that cannot be prompted, when there is one. */
159
+ unsupportedNote?: string;
160
+ /** Opt-in second rail; omit it and a client that cannot be prompted is always refused. */
161
+ fallback?: DispatchTokenFallback;
162
+ }
163
+
164
+ /**
165
+ * Ask the user before a dispatch. `undefined` means proceed; anything else is
166
+ * the result to return unchanged.
167
+ *
168
+ * Elicitation stays the primary path and is untouched. When the caller
169
+ * declares it cannot be prompted AND a `fallback` is supplied, MCP_CONFIRM_MODE
170
+ * decides: ask-user (default) or auto run the two-phase token flow; refuse
171
+ * refuses and names the switch. Without a fallback it is always refused.
172
+ */
173
+ export async function requireDispatchConfirmation(
174
+ ctx: ServerContext,
175
+ options: DispatchConfirmationOptions,
176
+ ): Promise<InputRequiredResult | CallToolResult | undefined> {
177
+ const { action, fallback } = options;
178
+ const confirmation = {
179
+ action,
180
+ message: options.message,
181
+ details: options.details,
182
+ confirmationLabel: options.confirmationLabel,
183
+ ...(options.unsupportedNote ? { unsupportedNote: options.unsupportedNote } : {}),
184
+ };
185
+ if (!fallback) return requireConfirmationWithFallback(ctx, confirmation);
186
+ return requireConfirmationWithFallback(ctx, confirmationFromEnv({
187
+ ...confirmation,
188
+ tool: fallback.tool,
189
+ account: fallback.account ?? readEnvVar('GOG_ACCOUNT') ?? '',
190
+ confirmToken: fallback.confirmToken,
191
+ subject: fallback.subject,
192
+ instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
193
+ spent: confirmSpentStore(),
194
+ }));
195
+ }
196
+
197
+ // The single place a CallToolResult's text is pulled back out, for the tools
198
+ // here that need to read gog's own JSON before deciding what to preview or
199
+ // log. Mirrors the shape every runOrDiagnose result actually returns
200
+ // (content[0].text); never throws on an unexpected shape.
201
+ export function resultText(result: CallToolResult): string {
202
+ const first = result.content[0];
203
+ return first && first.type === 'text' && typeof first.text === 'string' ? first.text : '{}';
204
+ }
@@ -1,5 +1,22 @@
1
1
  import type { CallToolResult, InputRequiredResult, ServerContext } from '@modelcontextprotocol/server';
2
- import { readEnvVar, requireConfirmation } from '@chrischall/mcp-utils';
2
+ import { readEnvVar } from '@chrischall/mcp-utils';
3
+ import { CONFIRM_SEND_INSTRUCTION, requireDispatchConfirmation, type DispatchTokenFallback } from './dispatch-confirmation.js';
4
+
5
+ // The generic pieces moved to dispatch-confirmation.ts when Chat, Calendar,
6
+ // Drive and Classroom joined the rail; re-exported so Gmail callers keep one import.
7
+ export {
8
+ attachmentDetails,
9
+ attachmentNames,
10
+ BODY_PREVIEW_MAX,
11
+ bodyPreview,
12
+ attachmentPreview,
13
+ CONFIRM_FALLBACK_DESCRIPTION,
14
+ CONFIRM_SEND_INSTRUCTION as CONFIRM_INSTRUCTION,
15
+ confirmTokenParam,
16
+ resultText,
17
+ senderPreview,
18
+ } from './dispatch-confirmation.js';
19
+ export type { AttachmentDetail, DispatchTokenFallback, TokenSubject } from './dispatch-confirmation.js';
3
20
 
4
21
  // ============================================================================
5
22
  // THE SAFETY RAIL. gog_gmail_reply / reply_all / send / forward / autoreply /
@@ -15,6 +32,12 @@ import { readEnvVar, requireConfirmation } from '@chrischall/mcp-utils';
15
32
  // user's accepted confirmation dispatches. The confirmation is never a tool
16
33
  // argument, so a model cannot bypass the user by setting a boolean itself.
17
34
  //
35
+ // The one exception: a client that cannot be prompted gets a two-phase preview
36
+ // + confirmToken instead of a refusal (`DispatchTokenFallback`; mcp-utils'
37
+ // confirmationFromEnv), unless the server sets MCP_CONFIRM_MODE=refuse. There
38
+ // the approval IS a tool argument; see mcp-utils' confirm-token module for
39
+ // exactly what the token does and does not guarantee.
40
+ //
18
41
  // A CLIENT THAT CANNOT SHOW THAT PROMPT gets a sentence rather than a prompt
19
42
  // it will refuse to deliver (`unsupportedNote`, mcp-utils `requireConfirmation`).
20
43
  // Measured on the mcp-host fleet 2026-09-20: claude.ai declares no MCP
@@ -97,41 +120,26 @@ const UNSUPPORTED_NOTE: Partial<Record<GmailDispatchOp, string>> = {
97
120
  'gmail.drafts-send': 'The draft is still saved: ask the user to review it and send it from Gmail.',
98
121
  };
99
122
 
100
- // Bound on the body text shown in a confirmation prompt. Enough to read what is
101
- // actually being sent (the point of SEC-5), small enough to keep the prompt a
102
- // prompt rather than a copy of the message.
103
- export const BODY_PREVIEW_MAX = 2048;
104
-
105
- /** The first BODY_PREVIEW_MAX characters of a body, marked when cut. */
106
- export function bodyPreview(text: string | undefined): string | undefined {
107
- if (!text) return undefined;
108
- if (text.length <= BODY_PREVIEW_MAX) return text;
109
- return `${text.slice(0, BODY_PREVIEW_MAX)}… [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
110
- }
111
-
112
123
  /**
113
- * Every attachment a dispatch will carry: a server path in full (so the user
114
- * sees WHICH file on the gog host is leaving), an inline one by its filename.
124
+ * Apply the shared stateless confirmation flow with Gmail-specific copy.
125
+ *
126
+ * Elicitation stays the primary path and is untouched. When the caller
127
+ * declares it cannot be prompted AND a `fallback` is supplied, MCP_CONFIRM_MODE
128
+ * decides: ask-user (default) or auto run the two-phase token flow; refuse
129
+ * refuses and names the switch.
115
130
  */
116
- export function attachmentNames(
117
- paths: readonly string[] | undefined,
118
- inline: ReadonlyArray<{ filename: string }> | undefined,
119
- ): string[] {
120
- return [...(paths ?? []), ...(inline ?? []).map((a) => a.filename)];
121
- }
122
-
123
- /** Apply the shared stateless confirmation flow with Gmail-specific copy. */
124
- export function requireGmailDispatchConfirmation(
131
+ export async function requireGmailDispatchConfirmation(
125
132
  ctx: ServerContext,
126
133
  op: GmailDispatchOp,
127
134
  details: Record<string, unknown>,
128
- ): InputRequiredResult | CallToolResult | undefined {
135
+ fallback?: DispatchTokenFallback,
136
+ ): Promise<InputRequiredResult | CallToolResult | undefined> {
129
137
  const twin = STAGING_TWIN[op];
130
138
  const note = twin
131
139
  ? `Stage it with ${twin} instead; the user can review the draft and send it from Gmail `
132
140
  + '(gog_gmail_drafts_send also asks for confirmation).'
133
141
  : UNSUPPORTED_NOTE[op];
134
- return requireConfirmation(ctx, {
142
+ return requireDispatchConfirmation(ctx, {
135
143
  action: op,
136
144
  message: op === 'gmail.filter-forward'
137
145
  ? 'Review and confirm this mail-forwarding filter:'
@@ -140,7 +148,8 @@ export function requireGmailDispatchConfirmation(
140
148
  confirmationLabel: op === 'gmail.filter-forward'
141
149
  ? 'Confirm that matching mail should be forwarded automatically from now on.'
142
150
  : 'Confirm that this email should be sent now.',
143
- ...(note ? { unsupportedNote: note } : {}),
151
+ unsupportedNote: note,
152
+ ...(fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}),
144
153
  });
145
154
  }
146
155
 
@@ -210,12 +219,3 @@ export function logGmailDispatch(tool: string, recipients: string[], account?: s
210
219
  };
211
220
  process.stderr.write(`${JSON.stringify(event)}\n`);
212
221
  }
213
-
214
- // The single place a CallToolResult's text is pulled back out, for the tools
215
- // here that need to read gog's own JSON before deciding what to preview or
216
- // log. Mirrors the shape every runOrDiagnose result actually returns
217
- // (content[0].text); never throws on an unexpected shape.
218
- export function resultText(result: CallToolResult): string {
219
- const first = result.content[0];
220
- return first && first.type === 'text' && typeof first.text === 'string' ? first.text : '{}';
221
- }
package/src/lib.ts CHANGED
@@ -26,13 +26,22 @@ export type { ReplyFlags } from './tools/gmail.js';
26
26
  // gmail sub-package's send-side forward/autoreply tools reuse these directly
27
27
  // rather than re-declaring the gate; the draft-side twins never import them.
28
28
  export {
29
+ attachmentDetails,
29
30
  attachmentNames,
31
+ attachmentPreview,
30
32
  bodyPreview,
33
+ CONFIRM_FALLBACK_DESCRIPTION,
34
+ confirmTokenParam,
31
35
  extractEmails,
32
36
  logGmailDispatch,
33
37
  requireGmailDispatchConfirmation,
34
38
  resultText,
39
+ senderPreview,
35
40
  } from './gmail-dispatch-guard.js';
41
+ // The service-neutral rail for every other tool that reaches another person.
42
+ export { requireDispatchConfirmation } from './dispatch-confirmation.js';
43
+ export { readCourse } from './tools/classroom.js';
44
+ export type { AttachmentDetail, DispatchTokenFallback, TokenSubject } from './gmail-dispatch-guard.js';
36
45
  export { run, runBinary, isGogFileArg, MIN_GOG_VERSION } from './runner.js';
37
46
  // Sub-package tools that read gog JSON through bare `run()` (rather than the
38
47
  // `runOrDiagnose` seam) must still apply this, or their timestamps skip the
@@ -0,0 +1,29 @@
1
+ import { createSpentTokenStore, type SpentTokenStore } from '@chrischall/mcp-utils';
2
+
3
+ // ============================================================================
4
+ // THE TOKEN FALLBACK's gogcli-mcp half, which is now only its spent-token store.
5
+ // Everything else is the fleet's shared layer in @chrischall/mcp-utils
6
+ // (`confirmationFromEnv` + `requireConfirmationWithFallback`), configured by the
7
+ // same three variables as every other fleet server:
8
+ //
9
+ // MCP_CONFIRM_MODE ask-user (default) | auto | refuse
10
+ // MCP_CONFIRM_TTL_SECONDS token lifetime, default 600
11
+ // MCP_CONFIRM_SECRET HMAC key; default random per process
12
+ //
13
+ // WHAT THIS DOES AND DOES NOT PROVE: see mcp-utils' confirm-token module. The
14
+ // approval is a tool argument, so under ask-user the gate is the model honouring
15
+ // "show this to the user and wait"; the token guarantees what happens matches
16
+ // what was previewed, once, for one tool, account and target.
17
+ // ============================================================================
18
+
19
+ const spent = createSpentTokenStore();
20
+
21
+ /** This server's spent-token store (one per process). */
22
+ export function confirmSpentStore(): SpentTokenStore {
23
+ return spent;
24
+ }
25
+
26
+ /** Test seam: forget every spent token. */
27
+ export function resetConfirmTokenState(): void {
28
+ spent.clear();
29
+ }
package/src/tools/api.ts CHANGED
@@ -6,6 +6,7 @@ import { assertSafeForwardedArgs } from '../arg-guard.js';
6
6
  import { confineAtFile } from '../file-roots.js';
7
7
  import { pos } from '../argv.js';
8
8
  import type { GogArg } from '../runner.js';
9
+ import { gatedElsewhere } from '../dispatch-confirmation.js';
9
10
 
10
11
  // Gmail methods gog_api_call refuses outright (audit SEC-2). `allowWrite` is a
11
12
  // boolean the MODEL sets, so it cannot stand in for the user's confirmation of
@@ -15,12 +16,32 @@ import type { GogArg } from '../runner.js';
15
16
  // the same reason. --gmail-no-send is pinned on as a runtime backstop too.
16
17
  const GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
17
18
 
19
+ // The same reasoning for every other action the dispatch rail gates (#400):
20
+ // the dedicated tool asks the user, so the raw API method must not be a way
21
+ // around it. Anchored on a `.` or the start so a Discovery id with the API
22
+ // prefix (`chat.spaces.messages.create`) matches too.
23
+ const DISPATCH_API_BLOCKED: Record<string, Array<{ method: RegExp; does: string; tool: string }>> = {
24
+ chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: 'posts a Chat message', tool: 'gog_chat_messages_send / gog_chat_dm_send' }],
25
+ drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: 'grants access to a file', tool: 'gog_drive_share' }],
26
+ classroom: [
27
+ { method: /(?:^|\.)courses\.announcements\.create$/i, does: 'posts to a class', tool: 'gog_classroom_announcements_create' },
28
+ { method: /(?:^|\.)invitations\.create$/i, does: 'invites someone to a class', tool: 'gog_classroom_invitations_create' },
29
+ ],
30
+ calendar: [
31
+ { method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: 'can put an event on guests\' calendars', tool: 'gog_calendar_create' },
32
+ { method: /(?:^|\.)events\.(?:update|patch)$/i, does: 'can change what guests see', tool: 'gog_calendar_update / gog_calendar_respond' },
33
+ ],
34
+ };
35
+
18
36
  export function refusedApiCall(api: string, method: string): string | undefined {
19
- if (api.trim().toLowerCase() === 'gmail' && GMAIL_API_BLOCKED.test(method.trim())) {
37
+ const name = api.trim().toLowerCase();
38
+ const m = method.trim();
39
+ if (name === 'gmail' && GMAIL_API_BLOCKED.test(m)) {
20
40
  return `gmail ${method} is not available through gog_api_call: it sends or forwards mail. `
21
41
  + 'Use gog_gmail_send / gog_gmail_drafts_send (which ask the user to confirm) or the dedicated gog_gmail_* tool.';
22
42
  }
23
- return undefined;
43
+ const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : [])!.find((b) => b.method.test(m));
44
+ return hit ? gatedElsewhere(`${name} ${m}`, 'gog_api_call', hit.does, hit.tool) : undefined;
24
45
  }
25
46
 
26
47
  // Generic Google Discovery API access (gog 0.31). gog_api_list / gog_api_describe
@@ -57,7 +78,7 @@ export function registerApiTools(server: McpServer): void {
57
78
  });
58
79
 
59
80
  server.registerTool('gog_api_call', {
60
- description: 'Call any Discovery-described Google API method directly — an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true — keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused — use the dedicated gog_gmail_* tools, which ask the user to confirm.',
81
+ description: 'Call any Discovery-described Google API method directly — an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true — keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused — use the dedicated gog_gmail_* tools, which ask the user to confirm. So are the other methods a dedicated tool asks about: Chat spaces.messages.create, Drive permissions.create/update, Classroom courses.announcements.create and invitations.create, and Calendar events.insert/import/quickAdd/update/patch.',
61
82
  annotations: { destructiveHint: true },
62
83
  inputSchema: z.object({
63
84
  api: z.string().describe('Discovery API name (e.g. drive, gmail, calendar)'),