gogcli-mcp 4.4.0 → 4.5.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +827 -315
- package/dist/lib.js +832 -281
- package/manifest.json +7 -7
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +102 -115
- package/src/gmail-dispatch-guard.ts +28 -21
- package/src/lib.ts +6 -2
- package/src/send-confirm-token.ts +17 -155
- package/src/tools/api.ts +74 -8
- package/src/tools/appscript.ts +63 -2
- package/src/tools/calendar.ts +83 -13
- package/src/tools/chat.ts +35 -4
- package/src/tools/classroom.ts +254 -25
- package/src/tools/docs.ts +7 -1
- package/src/tools/drive.ts +82 -10
- package/src/tools/gmail.ts +37 -1
- package/src/tools/sheets.ts +16 -1
- package/src/tools/utils.ts +10 -4
- package/tests/gmail-dispatch-guard.test.ts +60 -25
- package/tests/send-confirm-token.test.ts +17 -197
- package/tests/tools/api.test.ts +130 -5
- package/tests/tools/appscript.test.ts +6 -2
- package/tests/tools/dispatch-gates-more.test.ts +447 -0
- package/tests/tools/dispatch-gates.test.ts +74 -13
- package/tests/tools/docs.test.ts +5 -6
- package/tests/tools/drive.test.ts +2 -5
- package/tests/tools/gmail-confirm-token.test.ts +13 -11
- package/tests/tools/run-vets-more.test.ts +269 -0
- package/tests/tools/run-vets.test.ts +158 -4
- package/tests/tools/sheets.test.ts +10 -11
- package/tests/tools/utils.test.ts +38 -41
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.
|
|
6
|
+
"version": "4.5.1",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"name": "gog_api_call",
|
|
76
|
-
"description": "Call any Discovery-described Google API method (escape hatch; write
|
|
76
|
+
"description": "Call any Discovery-described Google API method (escape hatch; every write asks the user to confirm; dry-run available)."
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
"name": "gog_auth_add",
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"name": "gog_calendar_delete",
|
|
152
|
-
"description": "Delete a calendar event"
|
|
152
|
+
"description": "Delete a calendar event (asks the user to confirm when it has guests or recurs)"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"name": "gog_calendar_respond",
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
211
|
"name": "gog_classroom_submissions_return",
|
|
212
|
-
"description": "Return a graded submission"
|
|
212
|
+
"description": "Return a graded submission (asks the user to confirm)"
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"name": "gog_classroom_submissions_turn_in",
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
},
|
|
294
294
|
{
|
|
295
295
|
"name": "gog_drive_delete",
|
|
296
|
-
"description": "Move a file to trash"
|
|
296
|
+
"description": "Move a file to trash (a permanent delete asks the user to confirm)"
|
|
297
297
|
},
|
|
298
298
|
{
|
|
299
299
|
"name": "gog_drive_share",
|
|
@@ -449,7 +449,7 @@
|
|
|
449
449
|
},
|
|
450
450
|
{
|
|
451
451
|
"name": "gog_chat_spaces_create",
|
|
452
|
-
"description": "Create a named Chat space, optionally seeding its membership"
|
|
452
|
+
"description": "Create a named Chat space, optionally seeding its membership (with members, asks the user to confirm)"
|
|
453
453
|
},
|
|
454
454
|
{
|
|
455
455
|
"name": "gog_chat_threads_list",
|
|
@@ -517,7 +517,7 @@
|
|
|
517
517
|
},
|
|
518
518
|
{
|
|
519
519
|
"name": "gog_appscript_run_function",
|
|
520
|
-
"description": "Execute a function in a deployed Apps Script project"
|
|
520
|
+
"description": "Execute a function in a deployed Apps Script project (asks the user to confirm)"
|
|
521
521
|
},
|
|
522
522
|
{
|
|
523
523
|
"name": "gog_appscript_run",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
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,12 +41,12 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@chrischall/mcp-utils": "^2.
|
|
44
|
+
"@chrischall/mcp-utils": "^2.6.0",
|
|
45
45
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
46
46
|
"zod": "^4.6.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^26.6.
|
|
49
|
+
"@types/node": "^26.6.2",
|
|
50
50
|
"@vitest/coverage-v8": "^5.0.1",
|
|
51
51
|
"esbuild": "^0.28.2",
|
|
52
52
|
"typescript": "^7.0.2",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.5.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "4.
|
|
15
|
+
"version": "4.5.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import type { CallToolResult, InputRequiredResult, ServerContext } from '@modelcontextprotocol/server';
|
|
4
|
-
import { callerAcceptsFormElicitation, readEnvVar, requireConfirmation, textResult } from '@chrischall/mcp-utils';
|
|
5
|
-
import { z } from 'zod';
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './send-confirm-token.js';
|
|
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';
|
|
15
12
|
|
|
16
13
|
// ============================================================================
|
|
17
14
|
// THE DISPATCH RAIL, service-neutral: every tool that reaches another person
|
|
@@ -20,9 +17,11 @@ import {
|
|
|
20
17
|
//
|
|
21
18
|
// Elicitation is primary: the host shows the preview and the model never holds
|
|
22
19
|
// the approval. On a client that declares no elicitation (claude.ai, measured),
|
|
23
|
-
// the call
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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.
|
|
26
25
|
// ============================================================================
|
|
27
26
|
|
|
28
27
|
// Bound on the body text shown in a confirmation prompt. Enough to read what is
|
|
@@ -100,39 +99,23 @@ export const CONFIRM_SEND_INSTRUCTION =
|
|
|
100
99
|
+ 'Then call again with confirmToken.';
|
|
101
100
|
|
|
102
101
|
/** The same instruction for a dispatch that is not mail (a share, an invitation, a post). */
|
|
103
|
-
export const CONFIRM_ACTION_INSTRUCTION =
|
|
104
|
-
'Show this preview to the user verbatim and proceed only after they explicitly approve in chat. '
|
|
105
|
-
+ 'Then call again with confirmToken.';
|
|
106
|
-
|
|
107
|
-
const FALLBACK_HINT = 'Or set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation.';
|
|
102
|
+
export const CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
|
|
108
103
|
|
|
109
104
|
/** Appended to each gated tool's description. */
|
|
110
105
|
export const CONFIRM_FALLBACK_DESCRIPTION =
|
|
111
|
-
' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai)
|
|
112
|
-
+ '
|
|
113
|
-
+ '
|
|
114
|
-
+ '
|
|
115
|
-
+ '
|
|
116
|
-
+ '
|
|
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.';
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
+ 'has seen that preview and explicitly approved it in chat — never on the first call, never invented, never '
|
|
122
|
-
+ 'reused. Call again with the same arguments. Ignored when the client supports elicitation.',
|
|
123
|
-
);
|
|
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';
|
|
124
116
|
|
|
125
117
|
/** What the fallback binds a token to — recomputed from a fresh read on every call. */
|
|
126
|
-
export
|
|
127
|
-
/** The draftId / messageId / fileId / eventId / query the dispatch acts on. */
|
|
128
|
-
target: string;
|
|
129
|
-
/** A version that rotates on edit: a draft's messageId, an event's etag. */
|
|
130
|
-
revision?: string;
|
|
131
|
-
/** Canonical send payload; its SHA-256 is bound into the token. */
|
|
132
|
-
payload: unknown;
|
|
133
|
-
/** The complete preview shown to the user. */
|
|
134
|
-
preview: Record<string, unknown>;
|
|
135
|
-
}
|
|
118
|
+
export type TokenSubject = ConfirmSubject;
|
|
136
119
|
|
|
137
120
|
/**
|
|
138
121
|
* Opt-in second rail for a client that cannot be prompted. `subject` is only
|
|
@@ -149,73 +132,23 @@ export interface DispatchTokenFallback {
|
|
|
149
132
|
subject: () => TokenSubject | CallToolResult | Promise<TokenSubject | CallToolResult>;
|
|
150
133
|
}
|
|
151
134
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const DRAFT_CHANGED_NOTE = {
|
|
162
|
-
'message-id-rotated': 'Nothing was sent or changed: the target was edited since the user approved it (a draft\'s '
|
|
163
|
-
+ 'messageId or an event\'s version rotated), so what would happen is not what they saw.',
|
|
164
|
-
'payload-changed': 'Nothing was sent or changed: what would happen no longer matches what the user approved.',
|
|
165
|
-
} as const;
|
|
166
|
-
|
|
167
|
-
function isToolResult(value: TokenSubject | CallToolResult): value is CallToolResult {
|
|
168
|
-
return Array.isArray((value as CallToolResult).content);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function rejection(data: Record<string, unknown>): CallToolResult {
|
|
172
|
-
return { ...textResult({ status: 'confirmation-rejected', confirmed: false, dispatched: false, ...data }), isError: true };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function tokenConfirmation(op: string, fallback: DispatchTokenFallback): Promise<CallToolResult | undefined> {
|
|
176
|
-
const subject = await fallback.subject();
|
|
177
|
-
if (isToolResult(subject)) return subject;
|
|
178
|
-
const binding: ConfirmBinding = {
|
|
179
|
-
tool: fallback.tool,
|
|
180
|
-
account: fallback.account ?? readEnvVar('GOG_ACCOUNT') ?? '',
|
|
181
|
-
target: subject.target,
|
|
182
|
-
...(subject.revision === undefined ? {} : { revision: subject.revision }),
|
|
183
|
-
payloadHash: hashSendPayload(subject.payload),
|
|
184
|
-
};
|
|
185
|
-
const phaseOne = () => {
|
|
186
|
-
const { token, expiresAt } = issueConfirmToken(binding);
|
|
187
|
-
return {
|
|
188
|
-
action: op,
|
|
189
|
-
preview: subject.preview,
|
|
190
|
-
confirmToken: token,
|
|
191
|
-
expiresAt,
|
|
192
|
-
ttlSeconds: confirmTokenTtlSeconds(),
|
|
193
|
-
instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
|
-
if (!fallback.confirmToken) {
|
|
197
|
-
return textResult({ status: 'confirmation-required', confirmed: false, dispatched: false, ...phaseOne() });
|
|
198
|
-
}
|
|
199
|
-
const verdict = verifyConfirmToken(fallback.confirmToken, binding);
|
|
200
|
-
if (verdict.ok) return undefined;
|
|
201
|
-
if (verdict.error === 'DRAFT_CHANGED') {
|
|
202
|
-
return rejection({
|
|
203
|
-
error: 'DRAFT_CHANGED',
|
|
204
|
-
reason: verdict.reason,
|
|
205
|
-
note: `${DRAFT_CHANGED_NOTE[verdict.reason!]} The current preview and a fresh confirmToken are below.`,
|
|
206
|
-
...phaseOne(),
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
return rejection({ error: verdict.error, action: op, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
135
|
+
/**
|
|
136
|
+
* The refusal an escape hatch (`gog_<service>_run`, `gog_api_call`) gives for an
|
|
137
|
+
* action it must not perform: the run tool cannot see the guests, the class or
|
|
138
|
+
* the folder tree a change reaches, so it cannot preview it. `instead` names
|
|
139
|
+
* the way through — the dedicated tool, or the user doing it in the product.
|
|
140
|
+
*/
|
|
141
|
+
export function refusedInRun(what: string, via: string, does: string, instead: string): string {
|
|
142
|
+
return `${what} ${does} and is not available through ${via}. ${instead}`;
|
|
210
143
|
}
|
|
211
144
|
|
|
212
145
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
146
|
+
* {@link refusedInRun} for an action a dedicated tool gates. Without it, the
|
|
147
|
+
* run tool is a way around the rail: the model forwards the same subcommand
|
|
148
|
+
* and nobody is asked (#400).
|
|
216
149
|
*/
|
|
217
150
|
export function gatedElsewhere(what: string, via: string, does: string, tool: string): string {
|
|
218
|
-
return
|
|
151
|
+
return refusedInRun(what, via, does, `Use ${tool}, which asks the user to confirm.`);
|
|
219
152
|
}
|
|
220
153
|
|
|
221
154
|
/** True when any forwarded token is one of `words` (kong lets flags precede the command word). */
|
|
@@ -223,6 +156,56 @@ export function hasCommandWord(args: readonly string[], words: ReadonlySet<strin
|
|
|
223
156
|
return args.find((a) => words.has(a.toLowerCase()));
|
|
224
157
|
}
|
|
225
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The value of `--name=value` or `--name value` among forwarded args, `''` for
|
|
161
|
+
* a bare `--name`, undefined when the flag is absent. Kong accepts both
|
|
162
|
+
* spellings, so a vet that read only one of them would be a way around itself.
|
|
163
|
+
* Over-inclusive on a bare flag followed by another flag (it reads that flag
|
|
164
|
+
* as the value): a vet only ever asks whether the flag is present or names a
|
|
165
|
+
* specific value, and refusing is the safe direction of error.
|
|
166
|
+
*/
|
|
167
|
+
export function flagValue(args: readonly string[], name: string): string | undefined {
|
|
168
|
+
const flag = `--${name}`;
|
|
169
|
+
for (let i = 0; i < args.length; i++) {
|
|
170
|
+
const a = args[i]!;
|
|
171
|
+
if (a === flag) return args[i + 1] ?? '';
|
|
172
|
+
if (a.startsWith(`${flag}=`)) return a.slice(flag.length + 1);
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// gog's spellings of `comments create|reply` under drive and docs (`gog schema` 0.41.0).
|
|
178
|
+
const COMMENT_ADD_WORDS = new Set(['create', 'add', 'new']);
|
|
179
|
+
const COMMENT_REPLY_WORDS = new Set(['reply', 'respond']);
|
|
180
|
+
|
|
181
|
+
/** True when a forwarded flag list asks for `--<flag>` (bare, or =anything but false). */
|
|
182
|
+
export function hasTrueFlag(args: readonly string[], flag: string): boolean {
|
|
183
|
+
const name = `--${flag}`;
|
|
184
|
+
return args.some((a) => {
|
|
185
|
+
const lower = a.toLowerCase();
|
|
186
|
+
if (lower === name) return true;
|
|
187
|
+
return lower.startsWith(`${name}=`) && lower.slice(name.length + 1) !== 'false';
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* gog_{drive,docs}_run must not post the comments gog_*_comments_add / _reply
|
|
193
|
+
* would ask about: a comment notifies the file's owner and everyone it +mentions.
|
|
194
|
+
*/
|
|
195
|
+
export function vetCommentsRun(service: 'drive' | 'docs', subcommand: string, args: readonly string[]): string | undefined {
|
|
196
|
+
if (subcommand.toLowerCase() !== 'comments') return undefined;
|
|
197
|
+
const reply = hasCommandWord(args, COMMENT_REPLY_WORDS);
|
|
198
|
+
if (reply) {
|
|
199
|
+
return gatedElsewhere(`gog ${service} comments ${reply.toLowerCase()}`, `gog_${service}_run`,
|
|
200
|
+
'notifies the comment thread', `gog_${service}_comments_reply`);
|
|
201
|
+
}
|
|
202
|
+
const add = hasCommandWord(args, COMMENT_ADD_WORDS);
|
|
203
|
+
return add
|
|
204
|
+
? gatedElsewhere(`gog ${service} comments ${add.toLowerCase()}`, `gog_${service}_run`,
|
|
205
|
+
'notifies the file\'s owner and anyone it mentions', `gog_${service}_comments_add`)
|
|
206
|
+
: undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
226
209
|
export interface DispatchConfirmationOptions {
|
|
227
210
|
/** Stable id of the dispatch, echoed in every result (`gmail.send`, `drive.share`, …). */
|
|
228
211
|
action: string;
|
|
@@ -242,29 +225,33 @@ export interface DispatchConfirmationOptions {
|
|
|
242
225
|
* Ask the user before a dispatch. `undefined` means proceed; anything else is
|
|
243
226
|
* the result to return unchanged.
|
|
244
227
|
*
|
|
245
|
-
* Elicitation stays the primary path and is untouched.
|
|
246
|
-
* declares it cannot be prompted AND a `fallback` is supplied
|
|
247
|
-
*
|
|
248
|
-
*
|
|
228
|
+
* Elicitation stays the primary path and is untouched. When the caller
|
|
229
|
+
* declares it cannot be prompted AND a `fallback` is supplied, MCP_CONFIRM_MODE
|
|
230
|
+
* decides: ask-user (default) or auto run the two-phase token flow; refuse
|
|
231
|
+
* refuses and names the switch. Without a fallback it is always refused.
|
|
249
232
|
*/
|
|
250
233
|
export async function requireDispatchConfirmation(
|
|
251
234
|
ctx: ServerContext,
|
|
252
235
|
options: DispatchConfirmationOptions,
|
|
253
236
|
): Promise<InputRequiredResult | CallToolResult | undefined> {
|
|
254
237
|
const { action, fallback } = options;
|
|
255
|
-
|
|
256
|
-
return tokenConfirmation(action, fallback);
|
|
257
|
-
}
|
|
258
|
-
const note = fallback
|
|
259
|
-
? [options.unsupportedNote, FALLBACK_HINT].filter(Boolean).join(' ')
|
|
260
|
-
: options.unsupportedNote;
|
|
261
|
-
return requireConfirmation(ctx, {
|
|
238
|
+
const confirmation = {
|
|
262
239
|
action,
|
|
263
240
|
message: options.message,
|
|
264
241
|
details: options.details,
|
|
265
242
|
confirmationLabel: options.confirmationLabel,
|
|
266
|
-
...(
|
|
267
|
-
}
|
|
243
|
+
...(options.unsupportedNote ? { unsupportedNote: options.unsupportedNote } : {}),
|
|
244
|
+
};
|
|
245
|
+
if (!fallback) return requireConfirmationWithFallback(ctx, confirmation);
|
|
246
|
+
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
247
|
+
...confirmation,
|
|
248
|
+
tool: fallback.tool,
|
|
249
|
+
account: fallback.account ?? readEnvVar('GOG_ACCOUNT') ?? '',
|
|
250
|
+
confirmToken: fallback.confirmToken,
|
|
251
|
+
subject: fallback.subject,
|
|
252
|
+
instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
|
|
253
|
+
spent: confirmSpentStore(),
|
|
254
|
+
}));
|
|
268
255
|
}
|
|
269
256
|
|
|
270
257
|
// The single place a CallToolResult's text is pulled back out, for the tools
|
|
@@ -32,11 +32,11 @@ export type { AttachmentDetail, DispatchTokenFallback, TokenSubject } from './di
|
|
|
32
32
|
// user's accepted confirmation dispatches. The confirmation is never a tool
|
|
33
33
|
// argument, so a model cannot bypass the user by setting a boolean itself.
|
|
34
34
|
//
|
|
35
|
-
// The one exception
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// IS a tool argument; see
|
|
39
|
-
// not guarantee.
|
|
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
40
|
//
|
|
41
41
|
// A CLIENT THAT CANNOT SHOW THAT PROMPT gets a sentence rather than a prompt
|
|
42
42
|
// it will refuse to deliver (`unsupportedNote`, mcp-utils `requireConfirmation`).
|
|
@@ -123,10 +123,10 @@ const UNSUPPORTED_NOTE: Partial<Record<GmailDispatchOp, string>> = {
|
|
|
123
123
|
/**
|
|
124
124
|
* Apply the shared stateless confirmation flow with Gmail-specific copy.
|
|
125
125
|
*
|
|
126
|
-
* Elicitation stays the primary path and is untouched.
|
|
127
|
-
* declares it cannot be prompted AND a `fallback` is supplied
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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.
|
|
130
130
|
*/
|
|
131
131
|
export async function requireGmailDispatchConfirmation(
|
|
132
132
|
ctx: ServerContext,
|
|
@@ -196,25 +196,32 @@ function trustedDomains(account: string | undefined): Set<string> {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// A distinguishable, greppable event for every mail dispatch — recipient
|
|
199
|
-
// count plus whichever recipients fall outside the trusted
|
|
200
|
-
// unexpected external send (outside counsel, a wrong-number alias) can
|
|
201
|
-
// caught after the fact even if the confirmation step above is somehow
|
|
202
|
-
// bypassed by a future caller.
|
|
203
|
-
//
|
|
199
|
+
// count plus the DOMAINS of whichever recipients fall outside the trusted list
|
|
200
|
+
// — so an unexpected external send (outside counsel, a wrong-number alias) can
|
|
201
|
+
// be caught after the fact even if the confirmation step above is somehow
|
|
202
|
+
// bypassed by a future caller. Never the addresses (PRIV-1, fleet-audit
|
|
203
|
+
// #1012): on mcp-host this line is host log data the user never sees or
|
|
204
|
+
// controls, and a third party's address is their identity where their domain
|
|
205
|
+
// is the audit signal. stdout is the JSON-RPC channel, so this goes to stderr
|
|
206
|
+
// like every other diagnostic in this repo.
|
|
204
207
|
export function logGmailDispatch(tool: string, recipients: string[], account?: string): void {
|
|
205
208
|
const domains = trustedDomains(account);
|
|
206
|
-
const
|
|
209
|
+
const externalDomains = new Set<string>();
|
|
210
|
+
let externalRecipientCount = 0;
|
|
211
|
+
for (const recipient of recipients) {
|
|
207
212
|
const at = recipient.indexOf('@');
|
|
208
|
-
const domain = at > -1 ? recipient.slice(at + 1) : '';
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
const domain = at > -1 ? recipient.slice(at + 1).toLowerCase() : '';
|
|
214
|
+
if (domain && domains.has(domain)) continue;
|
|
215
|
+
externalRecipientCount += 1;
|
|
216
|
+
externalDomains.add(domain || '(no domain)');
|
|
217
|
+
}
|
|
211
218
|
const event = {
|
|
212
219
|
event: 'gmail_dispatch',
|
|
213
220
|
tool,
|
|
214
221
|
recipientCount: recipients.length,
|
|
215
|
-
externalRecipientCount
|
|
216
|
-
hasExternalRecipients:
|
|
217
|
-
|
|
222
|
+
externalRecipientCount,
|
|
223
|
+
hasExternalRecipients: externalRecipientCount > 0,
|
|
224
|
+
externalDomains: [...externalDomains].sort(),
|
|
218
225
|
timestamp: new Date().toISOString(),
|
|
219
226
|
};
|
|
220
227
|
process.stderr.write(`${JSON.stringify(event)}\n`);
|
package/src/lib.ts
CHANGED
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
// The reply/reply-all schema and flag builder live in the base package so the
|
|
20
20
|
// gmail sub-package's draft-side twins reuse ONE definition — registering the
|
|
21
21
|
// same tool name from both registrar lists would be a duplicate-name error.
|
|
22
|
-
export { replySchema, appendReplyFlags } from './tools/gmail.js';
|
|
22
|
+
export { replySchema, appendReplyFlags, parseMetadataHeaders } from './tools/gmail.js';
|
|
23
23
|
export type { ReplyFlags } from './tools/gmail.js';
|
|
24
24
|
// The gmail confirmation gate — gog_gmail_reply/send/forward/autoreply are
|
|
25
25
|
// the only tools that dispatch mail irreversibly on the first call. The
|
|
@@ -40,7 +40,11 @@ export {
|
|
|
40
40
|
} from './gmail-dispatch-guard.js';
|
|
41
41
|
// The service-neutral rail for every other tool that reaches another person.
|
|
42
42
|
export { requireDispatchConfirmation } from './dispatch-confirmation.js';
|
|
43
|
-
export { readCourse } from './tools/classroom.js';
|
|
43
|
+
export { readCourse, readCoursework, readClassroomWork, studentLabel } from './tools/classroom.js';
|
|
44
|
+
export type { ClassroomWork, ClassroomWorkKind } from './tools/classroom.js';
|
|
45
|
+
// Pure parsers a sub-package's confirmation prompt reuses on its own reads.
|
|
46
|
+
export { eventSnapshot } from './tools/calendar.js';
|
|
47
|
+
export { commentMentions, commentSnapshot, shareTargetMeta } from './tools/drive.js';
|
|
44
48
|
export type { AttachmentDetail, DispatchTokenFallback, TokenSubject } from './gmail-dispatch-guard.js';
|
|
45
49
|
export { run, runBinary, isGogFileArg, MIN_GOG_VERSION } from './runner.js';
|
|
46
50
|
// Sub-package tools that read gog JSON through bare `run()` (rather than the
|