mcp-google-multi 6.0.0-beta.3 → 6.0.0-beta.4
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/README.md +5 -5
- package/dist/accounts.d.ts +3 -3
- package/dist/accounts.js +18 -31
- package/dist/arg-normalize.js +15 -35
- package/dist/arg-strict.js +8 -28
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +5 -10
- package/dist/boot-gates.d.ts +1 -1
- package/dist/boot-gates.js +3 -5
- package/dist/cimd-cache.js +5 -11
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +8 -14
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +8 -1
- package/dist/compose.js +11 -11
- package/dist/config-file.js +7 -12
- package/dist/discover.js +4 -9
- package/dist/discovery-client.js +14 -33
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +12 -18
- package/dist/env-load.js +2 -5
- package/dist/executor.js +2 -4
- package/dist/fanout.js +8 -17
- package/dist/fs-atomic.d.ts +0 -3
- package/dist/fs-atomic.js +6 -10
- package/dist/hint-provider.js +4 -10
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +3 -7
- package/dist/http-transport.d.ts +5 -5
- package/dist/http-transport.js +39 -92
- package/dist/identity.d.ts +2 -2
- package/dist/identity.js +6 -9
- package/dist/index.js +19 -31
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +11 -22
- package/dist/mcp-token.d.ts +3 -13
- package/dist/mcp-token.js +16 -32
- package/dist/metrics-cli.d.ts +1 -1
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-tap.js +2 -5
- package/dist/migrate-config.d.ts +1 -2
- package/dist/migrate-config.js +3 -5
- package/dist/net-tuning.js +2 -4
- package/dist/oauth-as.d.ts +2 -2
- package/dist/oauth-as.js +32 -73
- package/dist/oauth-consent.js +4 -10
- package/dist/outbound-allowlist.js +4 -9
- package/dist/reauth-hint.js +2 -5
- package/dist/registry-transfer.d.ts +1 -1
- package/dist/registry-transfer.js +5 -13
- package/dist/registry.d.ts +1 -9
- package/dist/registry.js +38 -66
- package/dist/scope-catalog.d.ts +5 -0
- package/dist/scope-catalog.js +17 -7
- package/dist/scope-observability.d.ts +2 -3
- package/dist/scope-observability.js +8 -14
- package/dist/services.d.ts +3 -0
- package/dist/services.js +5 -10
- package/dist/ssrf-guard.js +11 -22
- package/dist/tenant-purge.js +2 -4
- package/dist/token-store.js +0 -0
- package/dist/tools/_coerce.d.ts +4 -4
- package/dist/tools/_errors.js +22 -39
- package/dist/tools/_local-files.js +4 -8
- package/dist/tools/account-wizard.d.ts +4 -3
- package/dist/tools/account-wizard.js +48 -54
- package/dist/tools/accounts-tool.d.ts +1 -1
- package/dist/tools/accounts-tool.js +1 -1
- package/dist/tools/admin.js +2 -3
- package/dist/tools/analytics.js +4 -5
- package/dist/tools/calendar.js +4 -7
- package/dist/tools/chat.js +2 -3
- package/dist/tools/contacts.js +8 -9
- package/dist/tools/docs.js +4 -9
- package/dist/tools/drive.js +18 -27
- package/dist/tools/forms.js +2 -3
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +7 -12
- package/dist/tools/gmail.d.ts +2 -2
- package/dist/tools/gmail.js +26 -40
- package/dist/tools/google-api.js +5 -7
- package/dist/tools/meet.js +2 -3
- package/dist/tools/searchconsole.js +2 -3
- package/dist/tools/sheets.js +2 -3
- package/dist/tools/slides.js +2 -3
- package/dist/tools/tasks.js +2 -3
- package/dist/trim.js +2 -3
- package/dist/usage-metrics.js +5 -12
- package/dist/write-control.d.ts +4 -8
- package/dist/write-control.js +3 -4
- package/package.json +21 -21
package/dist/tools/drive.js
CHANGED
|
@@ -24,9 +24,8 @@ const GOOGLE_WORKSPACE_TYPES = new Set([
|
|
|
24
24
|
'application/vnd.google-apps.presentation',
|
|
25
25
|
'application/vnd.google-apps.drawing',
|
|
26
26
|
]);
|
|
27
|
-
// drive_read inlines only textual content
|
|
28
|
-
//
|
|
29
|
-
// in practice — image/svg+xml was the motivating false "binary" refusal.
|
|
27
|
+
// drive_read inlines only textual content; RFC 6839 +json/+xml/... suffixes and
|
|
28
|
+
// a few bare application/* types count too (image/svg+xml was the motivating case).
|
|
30
29
|
const TEXTUAL_EXACT = new Set([
|
|
31
30
|
'application/json',
|
|
32
31
|
'application/xml',
|
|
@@ -81,17 +80,14 @@ export const DRIVE_QUERY_HINT = "Drive search syntax: a plain keyword is treated
|
|
|
81
80
|
"structured query needs an operator, e.g. \"name contains 'report'\", " +
|
|
82
81
|
"\"mimeType = 'application/pdf'\", or \"'me' in owners\". " +
|
|
83
82
|
'Reference: https://developers.google.com/drive/api/guides/search-files';
|
|
84
|
-
// A bare keyword
|
|
85
|
-
//
|
|
86
|
-
// match. A query already carrying a comparison operator or a quoted literal is a
|
|
87
|
-
// structured query and is passed through untouched.
|
|
83
|
+
// A bare keyword isn't valid Drive `q` syntax (400s "Invalid Value"), so wrap it
|
|
84
|
+
// as a full-text match; a query with a comparison operator is passed through untouched.
|
|
88
85
|
export function normalizeDriveQuery(raw) {
|
|
89
86
|
const q = raw.trim();
|
|
90
87
|
if (q === '')
|
|
91
88
|
return q;
|
|
92
|
-
// Structured iff it
|
|
93
|
-
//
|
|
94
|
-
// count (names like O'Brien are keywords, not query syntax).
|
|
89
|
+
// Structured iff it has a comparison operator or the membership form "<value> in
|
|
90
|
+
// (parents|owners|writers|readers)"; a bare apostrophe (e.g. O'Brien) doesn't count.
|
|
95
91
|
const hasComparison = /(\bcontains\b|!=|<=|>=|[=<>])/i.test(q);
|
|
96
92
|
const hasMembership = /\bin\s+(parents|owners|writers|readers)\b/i.test(q);
|
|
97
93
|
if (hasComparison || hasMembership)
|
|
@@ -99,9 +95,8 @@ export function normalizeDriveQuery(raw) {
|
|
|
99
95
|
const escaped = q.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
100
96
|
return `fullText contains '${escaped}'`;
|
|
101
97
|
}
|
|
102
|
-
// Drive
|
|
103
|
-
//
|
|
104
|
-
// instead of a generic upstream_error.
|
|
98
|
+
// Drive 400s a malformed query with the opaque reason "invalid"/"Invalid Value";
|
|
99
|
+
// detect it so drive_search returns query-syntax help instead of a generic upstream_error.
|
|
105
100
|
export function isDriveInvalidQuery(error) {
|
|
106
101
|
const raw = error?.code ?? error?.status ?? error?.response?.status;
|
|
107
102
|
const status = typeof raw === 'string' ? Number(raw) : raw;
|
|
@@ -122,9 +117,8 @@ export function resolveShareNotification(opts) {
|
|
|
122
117
|
return opts.sendNotification ?? true;
|
|
123
118
|
}
|
|
124
119
|
export function registerDriveTools(server, deps = {}) {
|
|
125
|
-
// Per-registry,
|
|
126
|
-
//
|
|
127
|
-
// custody path is the context's, not the process global.
|
|
120
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
121
|
+
// the registry's account view at parse time, not a process-global default.
|
|
128
122
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
129
123
|
const requiredAccountEnum = accountArgLive(() => server.accountAliases());
|
|
130
124
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
@@ -144,8 +138,8 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
144
138
|
},
|
|
145
139
|
}, async ({ account, query, maxResults, driveId, pageToken }) => {
|
|
146
140
|
try {
|
|
147
|
-
// normalizeDriveQuery trims to '' and Drive reads that as "everything",
|
|
148
|
-
//
|
|
141
|
+
// normalizeDriveQuery trims to '' and Drive reads that as "everything", so
|
|
142
|
+
// a blank query would list the whole drive as a "search" result.
|
|
149
143
|
if (query.trim() === '') {
|
|
150
144
|
return invalidParams(account, '`query` is empty, and Drive reads an empty query as "match everything".', 'Pass a search term, or a structured Drive query such as "mimeType = \'application/pdf\'". To browse a folder instead, use drive_list.');
|
|
151
145
|
}
|
|
@@ -154,9 +148,8 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
154
148
|
const params = {
|
|
155
149
|
q: normalizeDriveQuery(query),
|
|
156
150
|
pageSize: maxResults ?? 10,
|
|
157
|
-
// nextPageToken and incompleteSearch live OUTSIDE files(...): omit
|
|
158
|
-
//
|
|
159
|
-
// indistinguishable from a complete one.
|
|
151
|
+
// nextPageToken and incompleteSearch live OUTSIDE files(...): omit them from
|
|
152
|
+
// the mask and Drive drops them, making a partial result indistinguishable from a complete one.
|
|
160
153
|
fields: 'nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,webViewLink,size,parents,driveId)',
|
|
161
154
|
supportsAllDrives: true,
|
|
162
155
|
includeItemsFromAllDrives: true,
|
|
@@ -182,9 +175,8 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
182
175
|
}
|
|
183
176
|
catch (error) {
|
|
184
177
|
if (isDriveInvalidQuery(error)) {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// non-JSON response landed here uncapped.
|
|
178
|
+
// Goes through safeMessage/stringifyEnvelope like every other envelope
|
|
179
|
+
// so a non-JSON upstream response can't land here uncapped.
|
|
188
180
|
return {
|
|
189
181
|
content: [{ type: 'text', text: stringifyEnvelope({
|
|
190
182
|
error: 'invalid_query',
|
|
@@ -685,9 +677,8 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
685
677
|
},
|
|
686
678
|
}, async ({ account, fileId, type, role, emailAddress, domain, sendNotification, emailMessage, transferOwnership, expirationTime }) => {
|
|
687
679
|
{
|
|
688
|
-
// Outbound allowlist (off unless GOOGLE_OUTBOUND_ALLOWLIST is set):
|
|
689
|
-
//
|
|
690
|
-
// "@domain" entry; "anyone" (link sharing) is refused while active.
|
|
680
|
+
// Outbound allowlist (off unless GOOGLE_OUTBOUND_ALLOWLIST is set): user/group
|
|
681
|
+
// grantees must match, a domain share needs its exact "@domain" entry, "anyone" is refused while active.
|
|
691
682
|
const list = resolveOutboundAllowlist();
|
|
692
683
|
if (list) {
|
|
693
684
|
if ((type === 'user' || type === 'group') && emailAddress) {
|
package/dist/tools/forms.js
CHANGED
|
@@ -5,9 +5,8 @@ import { getClient } from '../client.js';
|
|
|
5
5
|
import { handleGoogleApiError } from './_errors.js';
|
|
6
6
|
import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
|
|
7
7
|
export function registerFormsTools(server, deps = {}) {
|
|
8
|
-
// Per-registry,
|
|
9
|
-
//
|
|
10
|
-
// custody path is the context's, not the process global.
|
|
8
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
9
|
+
// the registry's account view at parse time, not a process-global default.
|
|
11
10
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
12
11
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
13
12
|
server.registerTool('forms_get', {
|
|
@@ -37,7 +37,7 @@ export declare class HeaderInjectionError extends Error {
|
|
|
37
37
|
constructor(field: string);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* The single MIME-assembly seam for gmail_send and gmail_create_draft
|
|
40
|
+
* The single MIME-assembly seam for gmail_send and gmail_create_draft.
|
|
41
41
|
* MailComposer owns header/RFC-2047/address encoding and boundary generation,
|
|
42
42
|
* which closes the CRLF header-injection hole in the old hand-rolled encoders.
|
|
43
43
|
* A deterministic pre-check rejects CR/LF in address/subject headers with a
|
package/dist/tools/gmail-mime.js
CHANGED
|
@@ -3,10 +3,8 @@ import MarkdownIt from 'markdown-it';
|
|
|
3
3
|
import TurndownService from 'turndown';
|
|
4
4
|
// @ts-expect-error turndown-plugin-gfm ships no type declarations
|
|
5
5
|
import { gfm } from 'turndown-plugin-gfm';
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// the body (the XSS-safe default); allowRawHtml swaps to the html:true
|
|
9
|
-
// instance so an audited caller can pass literal HTML (e.g. inline color).
|
|
6
|
+
// Send: body is Markdown; text/plain is the source, text/html is this render.
|
|
7
|
+
// html:false escapes raw HTML in the body (XSS-safe default); allowRawHtml opts into html:true for an audited caller.
|
|
10
8
|
const mdSafe = new MarkdownIt({ html: false, linkify: true });
|
|
11
9
|
const mdRaw = new MarkdownIt({ html: true, linkify: true });
|
|
12
10
|
export function renderMarkdown(body, allowRawHtml = false) {
|
|
@@ -87,10 +85,8 @@ export function htmlToText(html) {
|
|
|
87
85
|
.replace(/\n{3,}/g, '\n\n')
|
|
88
86
|
.trim();
|
|
89
87
|
}
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// bundled @mixmark-io/domino fork does NOT execute scripts; remove() drops
|
|
93
|
-
// script/style/head entirely (turndown otherwise leaks their text content).
|
|
88
|
+
// HTML-only bodies convert to Markdown so the model reads structure, not a flat
|
|
89
|
+
// text dump; turndown's bundled domino fork doesn't execute scripts, and remove() drops script/style/head (which otherwise leak text).
|
|
94
90
|
const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '-' });
|
|
95
91
|
turndownService.use(gfm);
|
|
96
92
|
turndownService.remove(['script', 'style', 'head']);
|
|
@@ -125,7 +121,7 @@ export class HeaderInjectionError extends Error {
|
|
|
125
121
|
}
|
|
126
122
|
}
|
|
127
123
|
/**
|
|
128
|
-
* The single MIME-assembly seam for gmail_send and gmail_create_draft
|
|
124
|
+
* The single MIME-assembly seam for gmail_send and gmail_create_draft.
|
|
129
125
|
* MailComposer owns header/RFC-2047/address encoding and boundary generation,
|
|
130
126
|
* which closes the CRLF header-injection hole in the old hand-rolled encoders.
|
|
131
127
|
* A deterministic pre-check rejects CR/LF in address/subject headers with a
|
|
@@ -152,9 +148,8 @@ export async function composeRaw(input) {
|
|
|
152
148
|
attachments: input.attachments,
|
|
153
149
|
newline: '\r\n',
|
|
154
150
|
textEncoding: 'base64',
|
|
155
|
-
// This server reads
|
|
156
|
-
//
|
|
157
|
-
// file-read defense-in-depth).
|
|
151
|
+
// This server reads attachment files itself and passes a Buffer; MailComposer
|
|
152
|
+
// must never touch the filesystem or network (SSRF / local file-read defense-in-depth).
|
|
158
153
|
disableFileAccess: true,
|
|
159
154
|
disableUrlAccess: true,
|
|
160
155
|
};
|
package/dist/tools/gmail.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface ReplyDerivation {
|
|
|
16
16
|
}
|
|
17
17
|
/** Fetch the reply source once and derive threading headers plus (auto-fill)
|
|
18
18
|
* to/cc/subject. On any fetch failure, degrade to the API id for threading and
|
|
19
|
-
* report sourceFound:false so the caller can decide
|
|
19
|
+
* report sourceFound:false so the caller can decide. */
|
|
20
20
|
export declare function resolveReply(gmail: any, account: string, primaryEmail: string, replyToMessageId: string, replyAll: boolean): Promise<ReplyDerivation>;
|
|
21
21
|
export declare function readAttachments(raw: Array<{
|
|
22
22
|
path: string;
|
|
@@ -27,7 +27,7 @@ export declare function parseMessage(msg: any, bodyCap?: number, opts?: {
|
|
|
27
27
|
rawHtml?: boolean;
|
|
28
28
|
}): GmailMessageFull;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
|
|
31
31
|
* and bound the aggregate output. Order is preserved by input index. A per-id
|
|
32
32
|
* failure becomes a `{ id, error }` entry; an account-wide auth/scope failure
|
|
33
33
|
* is rethrown so the caller maps it to a whole-call isError. Returns the
|
package/dist/tools/gmail.js
CHANGED
|
@@ -38,16 +38,15 @@ function decodeBody(payload, rawHtml = false) {
|
|
|
38
38
|
const plain = [];
|
|
39
39
|
const html = [];
|
|
40
40
|
collectTextParts(payload, plain, html, true);
|
|
41
|
-
// Concatenating every plain leaf keeps forwarded/mixed messages whole;
|
|
42
|
-
//
|
|
43
|
-
// A plain leaf is returned verbatim (byte-identical to v5).
|
|
41
|
+
// Concatenating every plain leaf keeps forwarded/mixed messages whole; plain
|
|
42
|
+
// always wins over HTML since alternatives duplicate the same body.
|
|
44
43
|
if (plain.length > 0)
|
|
45
44
|
return { body: plain.join('\n\n'), bodyFormat: 'plain' };
|
|
46
45
|
if (html.length > 0) {
|
|
47
46
|
const joined = html.join('\n\n');
|
|
48
47
|
if (rawHtml)
|
|
49
48
|
return { body: joined, bodyFormat: 'html' };
|
|
50
|
-
// HTML-only -> Markdown
|
|
49
|
+
// HTML-only -> Markdown. turndown failure degrades to plain text.
|
|
51
50
|
const { text, ok } = htmlToMarkdown(joined);
|
|
52
51
|
return { body: text, bodyFormat: ok ? 'markdown' : 'plain' };
|
|
53
52
|
}
|
|
@@ -91,9 +90,8 @@ function parseAddresses(headerValue) {
|
|
|
91
90
|
return out;
|
|
92
91
|
}
|
|
93
92
|
const normAddr = (a) => a.trim().toLowerCase();
|
|
94
|
-
/**
|
|
95
|
-
* names
|
|
96
|
-
* the same header-injection guard as caller-supplied ones. */
|
|
93
|
+
/** Rebuilds a comma-separated address header; composeRaw/MailComposer re-encodes
|
|
94
|
+
* names and apply the same header-injection guard as caller-supplied recipients. */
|
|
97
95
|
function formatAddresses(list) {
|
|
98
96
|
return list.map((a) => (a.name ? `${a.name} <${a.address}>` : a.address)).join(', ');
|
|
99
97
|
}
|
|
@@ -103,10 +101,8 @@ export function deriveReplySubject(sourceSubject) {
|
|
|
103
101
|
const s = sourceSubject.trim();
|
|
104
102
|
return /^re:/i.test(s) ? s : `Re: ${s}`;
|
|
105
103
|
}
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// process; a stale miss only costs one extra self-copy, never a wrong send.
|
|
109
|
-
// Keyed by alias AND mailbox: two contexts can share an alias name.
|
|
104
|
+
// Own-address set (primary + send-as aliases) for excluding the caller from
|
|
105
|
+
// reply-all; memoized per-process (a stale miss just costs one self-copy), keyed by alias+mailbox.
|
|
110
106
|
const ownAddressCache = new Map();
|
|
111
107
|
function getOwnAddresses(gmail, account, primaryEmail) {
|
|
112
108
|
const key = `${account} ${normAddr(primaryEmail)}`;
|
|
@@ -122,7 +118,7 @@ function getOwnAddresses(gmail, account, primaryEmail) {
|
|
|
122
118
|
}
|
|
123
119
|
}
|
|
124
120
|
catch {
|
|
125
|
-
// insufficient_scope
|
|
121
|
+
// insufficient_scope/5xx: degrade silently
|
|
126
122
|
}
|
|
127
123
|
return set;
|
|
128
124
|
})();
|
|
@@ -132,7 +128,7 @@ function getOwnAddresses(gmail, account, primaryEmail) {
|
|
|
132
128
|
}
|
|
133
129
|
/** Fetch the reply source once and derive threading headers plus (auto-fill)
|
|
134
130
|
* to/cc/subject. On any fetch failure, degrade to the API id for threading and
|
|
135
|
-
* report sourceFound:false so the caller can decide
|
|
131
|
+
* report sourceFound:false so the caller can decide. */
|
|
136
132
|
export async function resolveReply(gmail, account, primaryEmail, replyToMessageId, replyAll) {
|
|
137
133
|
let headers;
|
|
138
134
|
try {
|
|
@@ -145,7 +141,7 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
|
|
|
145
141
|
headers = meta.data.payload?.headers;
|
|
146
142
|
}
|
|
147
143
|
catch {
|
|
148
|
-
// Degrade to the API id
|
|
144
|
+
// Degrade to the API id rather than blocking the send.
|
|
149
145
|
return { inReplyTo: replyToMessageId, references: replyToMessageId, sourceFound: false };
|
|
150
146
|
}
|
|
151
147
|
const threading = buildReplyHeaders(replyToMessageId, getHeader(headers, 'Message-ID'), getHeader(headers, 'References'));
|
|
@@ -181,15 +177,11 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
|
|
|
181
177
|
};
|
|
182
178
|
}
|
|
183
179
|
const BODY_CAP_CHARS = 50_000;
|
|
184
|
-
// gmail_read_batch aggregate ceiling:
|
|
185
|
-
// blow the context window
|
|
186
|
-
// (matches the tool's anthropic/maxResultSizeChars hint). Per-message bodies are
|
|
187
|
-
// still individually capped at BODY_CAP_CHARS first.
|
|
180
|
+
// gmail_read_batch aggregate ceiling: bounds the summed serialized output so a
|
|
181
|
+
// 100-message batch can't blow the context window (per-message bodies are capped at BODY_CAP_CHARS first).
|
|
188
182
|
const BATCH_MAX_RESULT_CHARS = 100_000;
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// ENCODED total there rather than the spec's nominal 35 MB raw (spike/live
|
|
192
|
-
// correction — a 35 MB raw payload is ~47 MB on the wire and Gmail 400s it).
|
|
183
|
+
// messages.send rejects near ~25MB on the wire (base64 adds ~33%), so cap the
|
|
184
|
+
// estimated ENCODED total here rather than the nominal 35MB raw limit, which 400s at ~47MB wire.
|
|
193
185
|
const GMAIL_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
194
186
|
const estimateEncoded = (rawBytes) => Math.ceil((rawBytes * 4) / 3);
|
|
195
187
|
const attachmentSchema = z
|
|
@@ -203,10 +195,8 @@ const attachmentSchema = z
|
|
|
203
195
|
/** Reads attachment files into buffers (THIS server reads them, never
|
|
204
196
|
* MailComposer), enforces absolute-path + total-size guards, and derives the
|
|
205
197
|
* MIME filename via basename so a caller name can't inject path separators. */
|
|
206
|
-
// Deny reading
|
|
207
|
-
//
|
|
208
|
-
// exfil channel (this same server also reads untrusted mail/drive content, so
|
|
209
|
-
// a prompt-injected attach path is a real confused-deputy vector).
|
|
198
|
+
// Deny reading inside the server's own secret dirs; mailing out master.key /
|
|
199
|
+
// <alias>.enc / mcp-jwt.key would be account takeover (also guards against a prompt-injected attach path).
|
|
210
200
|
function isInside(dir, target) {
|
|
211
201
|
let d = path.resolve(dir);
|
|
212
202
|
let t = path.resolve(target);
|
|
@@ -280,9 +270,8 @@ export async function readAttachments(raw, bodyBytes) {
|
|
|
280
270
|
}
|
|
281
271
|
return out;
|
|
282
272
|
}
|
|
283
|
-
/** Tagged compose-time failure
|
|
284
|
-
*
|
|
285
|
-
* auto-fill raises it as not_found when a source message can't be fetched. */
|
|
273
|
+
/** Tagged compose-time failure mapped to an isError envelope; `category`
|
|
274
|
+
* defaults to validation_error (reply auto-fill raises not_found instead). */
|
|
286
275
|
class GmailComposeError extends Error {
|
|
287
276
|
slug;
|
|
288
277
|
category;
|
|
@@ -336,7 +325,7 @@ export function parseMessage(msg, bodyCap, opts) {
|
|
|
336
325
|
};
|
|
337
326
|
}
|
|
338
327
|
/**
|
|
339
|
-
*
|
|
328
|
+
* gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
|
|
340
329
|
* and bound the aggregate output. Order is preserved by input index. A per-id
|
|
341
330
|
* failure becomes a `{ id, error }` entry; an account-wide auth/scope failure
|
|
342
331
|
* is rethrown so the caller maps it to a whole-call isError. Returns the
|
|
@@ -383,9 +372,8 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
|
383
372
|
}
|
|
384
373
|
}
|
|
385
374
|
}
|
|
386
|
-
// Aggregate guard: walk in order
|
|
387
|
-
//
|
|
388
|
-
// later success body so a big batch never blows the context window.
|
|
375
|
+
// Aggregate guard: walk in order accumulating serialized size; once the running
|
|
376
|
+
// total would exceed the budget, cap this and every later success body so the batch never blows the context window.
|
|
389
377
|
let running = 0;
|
|
390
378
|
let truncatedAny = false;
|
|
391
379
|
let exhausted = false;
|
|
@@ -438,9 +426,8 @@ export function compactMessageRow(m) {
|
|
|
438
426
|
};
|
|
439
427
|
}
|
|
440
428
|
export function registerGmailTools(server, deps = {}) {
|
|
441
|
-
// Per-registry,
|
|
442
|
-
//
|
|
443
|
-
// custody path is the context's, not the process global.
|
|
429
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
430
|
+
// the registry's account view at parse time, not a process-global default.
|
|
444
431
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
445
432
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
446
433
|
const localFiles = deps.localFiles ?? true;
|
|
@@ -450,7 +437,7 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
450
437
|
'pass full=true for threadId, to, labelIds and the untruncated snippet.',
|
|
451
438
|
inputSchema: {
|
|
452
439
|
account: accountEnum.describe('Google account alias'),
|
|
453
|
-
query: z.string().describe('Gmail search syntax, e.g. "from:
|
|
440
|
+
query: z.string().describe('Gmail search syntax, e.g. "from:alice is:unread"'),
|
|
454
441
|
maxResults: coerceNumber(z.number().min(1).max(100)).optional()
|
|
455
442
|
.describe('Max results to return (default: 20, max: 100)'),
|
|
456
443
|
full: coerceBoolean.optional().describe('Return the full row shape instead of the compact default'),
|
|
@@ -494,9 +481,8 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
494
481
|
});
|
|
495
482
|
}
|
|
496
483
|
}
|
|
497
|
-
// Search is a pick-one-message step
|
|
498
|
-
// the compact row is the selection signal
|
|
499
|
-
// (p90 14.4k chars per call). full=true restores the pre-6.0 shape.
|
|
484
|
+
// Search is a pick-one-message step almost always followed by gmail_read;
|
|
485
|
+
// the compact row is the selection signal (full=true returns the previous, heavier row).
|
|
500
486
|
const rows = full === true ? results : results.map(compactMessageRow);
|
|
501
487
|
return {
|
|
502
488
|
content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
|
package/dist/tools/google-api.js
CHANGED
|
@@ -64,7 +64,7 @@ function describeMethod(m) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
export function registerEscapeTools(registry, policy, deps = {}) {
|
|
67
|
-
// Live per-registry account validation
|
|
67
|
+
// Live per-registry account validation, same as the curated files.
|
|
68
68
|
const accountEnum = accountArgLive(() => registry.accountAliases()).optional();
|
|
69
69
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
70
70
|
const toolsets = deps.toolsets ?? getToolsets();
|
|
@@ -178,9 +178,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
178
178
|
}
|
|
179
179
|
let method = index.find((m) => m.id === methodId);
|
|
180
180
|
if (!method) {
|
|
181
|
-
// Some discovery docs keep a legacy id prefix (
|
|
182
|
-
//
|
|
183
|
-
// alias, retry under the doc's own prefix before failing.
|
|
181
|
+
// Some discovery docs keep a legacy id prefix (searchconsole's methods are
|
|
182
|
+
// webmasters.*): retry under the doc's own prefix if the caller used our api alias.
|
|
184
183
|
const docPrefix = index[0]?.id.split('.')[0];
|
|
185
184
|
const [head, ...rest] = String(methodId).split('.');
|
|
186
185
|
if (docPrefix && (head === apiKey || head === api) && head !== docPrefix && rest.length > 0) {
|
|
@@ -205,9 +204,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
205
204
|
const cud = cudFromMethod(method);
|
|
206
205
|
const policyService = serviceForAlias(apiKey);
|
|
207
206
|
const lastSegment = method.id.split('.').pop() ?? method.id;
|
|
208
|
-
// The escape hatch self-gates, so it needs the same scope signal the
|
|
209
|
-
//
|
|
210
|
-
// safe-writes here while its generated twin is refused.
|
|
207
|
+
// The escape hatch self-gates, so it needs the same scope signal as the registry
|
|
208
|
+
// wrapper, or a privileged method would slip past safe-writes here while its generated twin is refused.
|
|
211
209
|
const toolRef = { name: `${policyService}_${lastSegment}`, service: policyService, cud, scopes: method.scopes };
|
|
212
210
|
if (cud !== 'read' && !isAllowed(toolRef, policy)) {
|
|
213
211
|
return writeDisabledResult(toolRef, policy, account);
|
package/dist/tools/meet.js
CHANGED
|
@@ -4,9 +4,8 @@ import { accountArgLive } from '../accounts.js';
|
|
|
4
4
|
import { getClient } from '../client.js';
|
|
5
5
|
import { handleGoogleApiError } from './_errors.js';
|
|
6
6
|
export function registerMeetTools(server, deps = {}) {
|
|
7
|
-
// Per-registry,
|
|
8
|
-
//
|
|
9
|
-
// custody path is the context's, not the process global.
|
|
7
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
8
|
+
// the registry's account view at parse time, not a process-global default.
|
|
10
9
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
11
10
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
12
11
|
// ─── Conference records (past meetings) ────────────────────────────────
|
|
@@ -6,9 +6,8 @@ import { accountArgLive } from '../accounts.js';
|
|
|
6
6
|
import { getClient } from '../client.js';
|
|
7
7
|
import { handleGoogleApiError } from './_errors.js';
|
|
8
8
|
export function registerSearchConsoleTools(server, deps = {}) {
|
|
9
|
-
// Per-registry,
|
|
10
|
-
//
|
|
11
|
-
// custody path is the context's, not the process global.
|
|
9
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
10
|
+
// the registry's account view at parse time, not a process-global default.
|
|
12
11
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
13
12
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
14
13
|
// ─── Sites ───────────────────────────────────────────────
|
package/dist/tools/sheets.js
CHANGED
|
@@ -5,9 +5,8 @@ import { accountArgLive } from '../accounts.js';
|
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, invalidParams } from './_errors.js';
|
|
7
7
|
export function registerSheetsTools(server, deps = {}) {
|
|
8
|
-
// Per-registry,
|
|
9
|
-
//
|
|
10
|
-
// custody path is the context's, not the process global.
|
|
8
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
9
|
+
// the registry's account view at parse time, not a process-global default.
|
|
11
10
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
12
11
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
13
12
|
server.registerTool('sheets_create', {
|
package/dist/tools/slides.js
CHANGED
|
@@ -7,9 +7,8 @@ import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
|
|
|
7
7
|
import { sliceClean } from '../trim.js';
|
|
8
8
|
const SLIDE_TEXT_DIGEST_CHARS = 200;
|
|
9
9
|
export function registerSlidesTools(server, deps = {}) {
|
|
10
|
-
// Per-registry,
|
|
11
|
-
//
|
|
12
|
-
// custody path is the context's, not the process global.
|
|
10
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
11
|
+
// the registry's account view at parse time, not a process-global default.
|
|
13
12
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
14
13
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
15
14
|
server.registerTool('slides_create', {
|
package/dist/tools/tasks.js
CHANGED
|
@@ -5,9 +5,8 @@ import { accountArgLive } from '../accounts.js';
|
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, invalidParams } from './_errors.js';
|
|
7
7
|
export function registerTasksTools(server, deps = {}) {
|
|
8
|
-
// Per-registry,
|
|
9
|
-
//
|
|
10
|
-
// custody path is the context's, not the process global.
|
|
8
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
9
|
+
// the registry's account view at parse time, not a process-global default.
|
|
11
10
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
12
11
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
13
12
|
// ─── Tasklists ─────────────────────────────────────────────────────────
|
package/dist/trim.js
CHANGED
|
@@ -11,9 +11,8 @@ export function sliceClean(text, max) {
|
|
|
11
11
|
const sliced = text.slice(0, max);
|
|
12
12
|
return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
|
|
13
13
|
}
|
|
14
|
-
// C0
|
|
15
|
-
//
|
|
16
|
-
// invisible format controls make a line read differently from its bytes.
|
|
14
|
+
// C0/C1/DEL can end a line or drive a terminal; bidi/invisible format
|
|
15
|
+
// controls make a line read differently from its bytes.
|
|
17
16
|
const LOG_UNSAFE = /[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2028\u2029\u2066-\u2069]/gu;
|
|
18
17
|
/** An untrusted value made fit for one log line: never throws, at most `max`
|
|
19
18
|
* characters of it (then `...(len=N)`), each unsafe character as a literal
|
package/dist/usage-metrics.js
CHANGED
|
@@ -4,14 +4,8 @@ import * as os from 'node:os';
|
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { withFileLock, atomicWriteFileSync } from './fs-atomic.js';
|
|
7
|
-
// Local usage metrics
|
|
8
|
-
//
|
|
9
|
-
// Zero network egress is structural: this module's import set is a tested
|
|
10
|
-
// allowlist (fs/path/os/crypto/perf_hooks/fs-atomic), so egress code cannot
|
|
11
|
-
// even be expressed here. Every string persisted is a member of a server-owned
|
|
12
|
-
// closed vocabulary, matches a pinned shape regex, or is other/unknown_*/
|
|
13
|
-
// _overflow; free text is unrepresentable in the file format. No identity
|
|
14
|
-
// dimension of any kind: no aliases, hashes, emails, or exact account counts.
|
|
7
|
+
// Local usage metrics, default OFF; zero network egress is structural (a
|
|
8
|
+
// tested import allowlist). See docs/usage-metrics.md.
|
|
15
9
|
export const METRICS_DIR_NAME = 'metrics';
|
|
16
10
|
const FLUSH_MS = 60_000;
|
|
17
11
|
const RETRY_WINDOW_MS = 10 * 60_000;
|
|
@@ -26,7 +20,7 @@ const WRITE_FAILURE_LIMIT = 3;
|
|
|
26
20
|
const LAT_BOUNDS = [100, 250, 500, 1000, 2500, 5000, 10000, 30000];
|
|
27
21
|
const CHARS_BOUNDS = [64, 256, 1024, 4096, 16384, 65536];
|
|
28
22
|
// methodId backstop shape gate: even a tampered discovery cache cannot turn
|
|
29
|
-
// arbitrary strings into recorded values
|
|
23
|
+
// arbitrary strings into recorded values.
|
|
30
24
|
const METHOD_ID_RE = /^[a-z][a-zA-Z0-9]*(\.[a-zA-Z0-9]+)+$/;
|
|
31
25
|
const METHOD_ID_MAX = 128;
|
|
32
26
|
const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
@@ -366,9 +360,8 @@ export class Metrics {
|
|
|
366
360
|
f.calls += 1;
|
|
367
361
|
addInto(f, fan);
|
|
368
362
|
}
|
|
369
|
-
// retry self-correction chain
|
|
370
|
-
//
|
|
371
|
-
// the process (sequencingEnabled).
|
|
363
|
+
// retry self-correction chain, in-memory only; links consecutive calls,
|
|
364
|
+
// so it only runs when one caller owns the process (sequencingEnabled).
|
|
372
365
|
const nowM = this.mono();
|
|
373
366
|
if (this.sequencing) {
|
|
374
367
|
const prev = this.lastError.get(entry.name);
|
package/dist/write-control.d.ts
CHANGED
|
@@ -7,12 +7,11 @@ export interface Policy {
|
|
|
7
7
|
allow: string[];
|
|
8
8
|
deny: string[];
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* policy for the EE/future seam, but in v6 alpha it does NOT stiffen the
|
|
10
|
+
* Reserved seam (HTTP posture is "annotations only"): the dispatch
|
|
11
|
+
* transport is threaded into the resolved policy for a future seam, but in v6 alpha it does NOT stiffen the
|
|
13
12
|
* profile or alter any `isAllowed` verdict. "Stricter on HTTP" is achieved
|
|
14
13
|
* entirely by the `anthropic/requiresUserInteraction` annotation on the
|
|
15
|
-
* irreversible set
|
|
14
|
+
* irreversible set, emitted identically on both transports.
|
|
16
15
|
* Optional so existing Policy literals (codegen/tests) need no change;
|
|
17
16
|
* resolvePolicy always populates it.
|
|
18
17
|
*/
|
|
@@ -22,12 +21,9 @@ interface ToolRef {
|
|
|
22
21
|
name: string;
|
|
23
22
|
service: string;
|
|
24
23
|
cud: Cud;
|
|
25
|
-
/** The method's required OAuth scopes, when the caller knows them. Absent
|
|
26
|
-
* means "unknown", which is treated as not privileged: a missing scope list
|
|
27
|
-
* must not silently widen the gate OR silently break an ordinary write. */
|
|
28
24
|
scopes?: readonly string[];
|
|
29
25
|
}
|
|
30
|
-
/** The irreversible set (frozen
|
|
26
|
+
/** The irreversible set (frozen): real sends leave the
|
|
31
27
|
* user's (or org's) identity unrecallably; permanent deletes bypass Trash.
|
|
32
28
|
* Includes the callable GENERATED twins of the curated ops — the surface is
|
|
33
29
|
* always callable by name, so a prompt-free twin would defeat the layer.
|
package/dist/write-control.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hints } from './hint-provider.js';
|
|
2
2
|
const PROFILES = ['read-only', 'safe-writes', 'full-writes'];
|
|
3
|
-
/** The irreversible set (frozen
|
|
3
|
+
/** The irreversible set (frozen): real sends leave the
|
|
4
4
|
* user's (or org's) identity unrecallably; permanent deletes bypass Trash.
|
|
5
5
|
* Includes the callable GENERATED twins of the curated ops — the surface is
|
|
6
6
|
* always callable by name, so a prompt-free twin would defeat the layer.
|
|
@@ -64,9 +64,8 @@ function firstMatch(tool, globs) {
|
|
|
64
64
|
function matchesAny(tool, globs) {
|
|
65
65
|
return firstMatch(tool, globs) !== undefined;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* write in any useful sense of the word, whatever its HTTP verb. */
|
|
67
|
+
// Scopes that authorize acting on the whole organization, legal holds,
|
|
68
|
+
// billing, or deployed code: not a "safe" write, whatever its HTTP verb.
|
|
70
69
|
const PRIVILEGED_SCOPE_RE = /\/auth\/(admin\.directory|admin\.datatransfer|cloud-identity|apps\.licensing|apps\.order|apps\.groups\.settings|apps\.groups\.migration|ediscovery|script\.projects|script\.deployments)/;
|
|
71
70
|
/** Registering a push channel hands this account's activity to an external URL.
|
|
72
71
|
* It is shaped like a create and behaves like an export, so the verb hides it. */
|