@zackbart/connecta 0.31.0 → 0.33.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/CHANGELOG.md +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -26,7 +26,7 @@ import { skill } from "./skill.generated.js";
|
|
|
26
26
|
*/
|
|
27
27
|
import { apiConnector as api, defined } from "../../connectors/api-connector.js";
|
|
28
28
|
import { ConnectorCallError } from "../../errors.js";
|
|
29
|
-
import { GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, } from "../_shared/google/workspace.js";
|
|
29
|
+
import { GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, workspaceConnector, } from "../_shared/google/workspace.js";
|
|
30
30
|
import { RESULT_BUDGET_BYTES, clampText, jsonBytes } from "../_shared/google/result-size.js";
|
|
31
31
|
import { asProviderFactory } from "../../provider.js";
|
|
32
32
|
/**
|
|
@@ -58,6 +58,11 @@ const SUMMARY_CONCURRENCY = 5;
|
|
|
58
58
|
/** A full thread without attachment bodies; Gmail's message cap is 25 MB. */
|
|
59
59
|
const GMAIL_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
60
60
|
const MAX_RECIPIENTS = 100;
|
|
61
|
+
/** Gmail cannot range-read attachment JSON, so text and binary downloads are bounded. */
|
|
62
|
+
const MAX_ATTACHMENT_BYTES = 1024 * 1024;
|
|
63
|
+
/** Same direct-call ceiling and minimum result budget as Drive content reads. */
|
|
64
|
+
const MAX_ATTACHMENT_RESULT_BYTES = 4 * 1024 * 1024;
|
|
65
|
+
const MIN_ATTACHMENT_RESULT_BYTES = 64 * 1024;
|
|
61
66
|
function asRecord(value) {
|
|
62
67
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
63
68
|
}
|
|
@@ -118,22 +123,37 @@ function base64UrlBytes(data) {
|
|
|
118
123
|
}
|
|
119
124
|
function charsetOf(part) {
|
|
120
125
|
const type = header(part, "Content-Type") ?? "";
|
|
121
|
-
|
|
126
|
+
// Consume complete parameters so a semicolon inside another quoted value
|
|
127
|
+
// cannot masquerade as a charset. MIME allows whitespace between tokens.
|
|
128
|
+
const parameters = /;\s*([^=;\s]+)\s*=\s*(?:"((?:\\.|[^"\\])*)"|([^;\s]*))/g;
|
|
129
|
+
for (const match of type.matchAll(parameters)) {
|
|
130
|
+
if (match[1].toLowerCase() === "charset") {
|
|
131
|
+
return (match[2]?.replace(/\\(.)/g, "$1") ?? match[3] ?? "").trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return "utf-8";
|
|
122
135
|
}
|
|
123
|
-
/** Bytes as text,
|
|
136
|
+
/** Bytes as text, with an explicit notice when the declared charset is unsupported. */
|
|
124
137
|
function decodeIn(part, data) {
|
|
125
138
|
let bytes;
|
|
126
139
|
try {
|
|
127
140
|
bytes = base64UrlBytes(data);
|
|
128
141
|
}
|
|
129
142
|
catch {
|
|
130
|
-
return "";
|
|
143
|
+
return { text: "" };
|
|
131
144
|
}
|
|
145
|
+
return decodeBytes(part, bytes);
|
|
146
|
+
}
|
|
147
|
+
function decodeBytes(part, bytes) {
|
|
148
|
+
const charset = charsetOf(part);
|
|
132
149
|
try {
|
|
133
|
-
return new TextDecoder(
|
|
150
|
+
return { text: new TextDecoder(charset).decode(bytes) };
|
|
134
151
|
}
|
|
135
152
|
catch {
|
|
136
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
text: new TextDecoder().decode(bytes),
|
|
155
|
+
note: `Gmail declared unsupported charset "${clipped(charset, FIELD_BYTES)}"; decoded as UTF-8.`,
|
|
156
|
+
};
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
function mimeOf(part) {
|
|
@@ -161,13 +181,13 @@ const BODY_TYPES = new Set(["text/plain", "text/html"]);
|
|
|
161
181
|
const MAX_PART_DEPTH = 20;
|
|
162
182
|
/**
|
|
163
183
|
* Walk a payload keeping ancestry: the message's own parts, in order, and the
|
|
164
|
-
* attachment roots, whose subtrees are
|
|
184
|
+
* attachment roots, whose subtrees are entered only for a resolved forwarded root.
|
|
165
185
|
*/
|
|
166
|
-
function walk(root) {
|
|
186
|
+
function walk(root, enterAttachmentRoot = false) {
|
|
167
187
|
const own = [];
|
|
168
188
|
const attachments = [];
|
|
169
189
|
const visit = (part, depth) => {
|
|
170
|
-
if (isAttachment(part)) {
|
|
190
|
+
if (isAttachment(part) && !(enterAttachmentRoot && depth === 0)) {
|
|
171
191
|
attachments.push(part);
|
|
172
192
|
return;
|
|
173
193
|
}
|
|
@@ -180,6 +200,10 @@ function walk(root) {
|
|
|
180
200
|
visit(root, 0);
|
|
181
201
|
return { own, attachments };
|
|
182
202
|
}
|
|
203
|
+
/** The same plain-first selection for messages and parsed forwarded attachments. */
|
|
204
|
+
function ownBody(own) {
|
|
205
|
+
return own.find((part) => mimeOf(part) === "text/plain") ?? own.find((part) => mimeOf(part) === "text/html");
|
|
206
|
+
}
|
|
183
207
|
/** Gmail stores a large body apart from the message, behind an attachment id. */
|
|
184
208
|
const MAX_STORED_BODY_BYTES = 1024 * 1024;
|
|
185
209
|
/**
|
|
@@ -190,11 +214,10 @@ const MAX_STORED_BODY_BYTES = 1024 * 1024;
|
|
|
190
214
|
*/
|
|
191
215
|
async function readBody(client, ctx, messageId, payload, max) {
|
|
192
216
|
const own = walk(payload).own;
|
|
193
|
-
const
|
|
194
|
-
const chosen = plain ?? own.find((part) => mimeOf(part) === "text/html");
|
|
217
|
+
const chosen = ownBody(own);
|
|
195
218
|
if (!chosen)
|
|
196
219
|
return { body: "", bodyTruncated: false, bodyFormat: "none" };
|
|
197
|
-
const format = plain ? "text" : "html";
|
|
220
|
+
const format = mimeOf(chosen) === "text/plain" ? "text" : "html";
|
|
198
221
|
const stored = asRecord(chosen["body"]);
|
|
199
222
|
let data = text(stored["data"]);
|
|
200
223
|
const attachmentId = text(stored["attachmentId"]);
|
|
@@ -216,9 +239,13 @@ async function readBody(client, ctx, messageId, payload, max) {
|
|
|
216
239
|
}, ctx));
|
|
217
240
|
data = text(fetched["data"]);
|
|
218
241
|
}
|
|
219
|
-
const decoded = data ? decodeIn(chosen, data) : "";
|
|
242
|
+
const decoded = data ? decodeIn(chosen, data) : { text: "" };
|
|
220
243
|
// Uncut: the caller caps it, by characters and by the result's bytes.
|
|
221
|
-
return {
|
|
244
|
+
return {
|
|
245
|
+
text: format === "html" ? htmlToText(decoded.text) : decoded.text,
|
|
246
|
+
bodyFormat: format,
|
|
247
|
+
...(decoded.note ? { bodyNote: decoded.note } : {}),
|
|
248
|
+
};
|
|
222
249
|
}
|
|
223
250
|
/**
|
|
224
251
|
* Gmail's ids are short opaque tokens — message and thread ids are sixteen
|
|
@@ -243,7 +270,7 @@ function ids(fields) {
|
|
|
243
270
|
const kept = {};
|
|
244
271
|
const omitted = [];
|
|
245
272
|
for (const [name, [value, maxBytes]] of Object.entries(fields)) {
|
|
246
|
-
const id = idOf(value, maxBytes);
|
|
273
|
+
const id = name === "partId" ? partIdOf(value) : idOf(value, maxBytes);
|
|
247
274
|
if (id !== undefined)
|
|
248
275
|
kept[name] = id;
|
|
249
276
|
else if (text(value) !== undefined)
|
|
@@ -269,11 +296,180 @@ function attachmentsOf(payload) {
|
|
|
269
296
|
filename: clipped(text(part["filename"]) ?? "", FIELD_BYTES),
|
|
270
297
|
mimeType: clipped(text(part["mimeType"]), FIELD_BYTES),
|
|
271
298
|
size: typeof asRecord(part["body"])["size"] === "number" ? asRecord(part["body"])["size"] : undefined,
|
|
272
|
-
...ids({
|
|
299
|
+
...ids({
|
|
300
|
+
attachmentId: [asRecord(part["body"])["attachmentId"], ATTACHMENT_ID_BYTES],
|
|
301
|
+
partId: [part["partId"]],
|
|
302
|
+
}),
|
|
273
303
|
})),
|
|
274
304
|
omitted: Math.max(0, all.length - MAX_LISTED_ATTACHMENTS),
|
|
275
305
|
};
|
|
276
306
|
}
|
|
307
|
+
/** Unlike opaque ids, Gmail's root part id may be empty. */
|
|
308
|
+
function partIdOf(value) {
|
|
309
|
+
return typeof value === "string" && jsonBytes(value) <= ID_BYTES ? value : undefined;
|
|
310
|
+
}
|
|
311
|
+
function declaredSize(body) {
|
|
312
|
+
const size = body["size"];
|
|
313
|
+
return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : undefined;
|
|
314
|
+
}
|
|
315
|
+
/** Include every descendant, even carried files that body selection will skip. */
|
|
316
|
+
function forwardedSize(part, depth = 0) {
|
|
317
|
+
if (depth > MAX_PART_DEPTH)
|
|
318
|
+
return undefined;
|
|
319
|
+
const body = asRecord(part["body"]);
|
|
320
|
+
const children = asArray(part["parts"]);
|
|
321
|
+
let total = declaredSize(body);
|
|
322
|
+
if (total === undefined) {
|
|
323
|
+
if (children.length === 0 || body["size"] !== undefined || body["attachmentId"] || body["data"])
|
|
324
|
+
return undefined;
|
|
325
|
+
total = 0;
|
|
326
|
+
}
|
|
327
|
+
for (const child of children) {
|
|
328
|
+
const size = forwardedSize(asRecord(child), depth + 1);
|
|
329
|
+
if (size === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
total += size;
|
|
332
|
+
if (!Number.isSafeInteger(total))
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return total;
|
|
336
|
+
}
|
|
337
|
+
/** Validate both Gmail byte counts before content can be returned as complete. */
|
|
338
|
+
async function attachmentBytes(client, ctx, messageId, part) {
|
|
339
|
+
const stored = asRecord(part["body"]);
|
|
340
|
+
let source = stored;
|
|
341
|
+
const freshId = text(stored["attachmentId"]);
|
|
342
|
+
if (freshId) {
|
|
343
|
+
source = asRecord(await client.json({
|
|
344
|
+
method: "GET",
|
|
345
|
+
path: `/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(freshId)}`,
|
|
346
|
+
}, ctx));
|
|
347
|
+
}
|
|
348
|
+
const data = source["data"];
|
|
349
|
+
if (typeof data !== "string")
|
|
350
|
+
return undefined;
|
|
351
|
+
let bytes;
|
|
352
|
+
try {
|
|
353
|
+
bytes = base64UrlBytes(data);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
throw new ConnectorCallError("connector_call_failed", "Gmail returned invalid base64url attachment data.", {
|
|
357
|
+
retryable: false,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (bytes.length !== declaredSize(stored) ||
|
|
361
|
+
(source["size"] !== undefined && bytes.length !== declaredSize(source))) {
|
|
362
|
+
throw new ConnectorCallError("connector_call_failed", "Gmail attachment data does not match its declared byte size.", { retryable: false });
|
|
363
|
+
}
|
|
364
|
+
return bytes;
|
|
365
|
+
}
|
|
366
|
+
/** Read only a carried part of a fresh message, never an arbitrary download id. */
|
|
367
|
+
async function readAttachment(client, ctx, args) {
|
|
368
|
+
const messageId = String(args["messageId"]);
|
|
369
|
+
const partId = args["partId"];
|
|
370
|
+
const attachmentId = text(args["attachmentId"]);
|
|
371
|
+
if (partId === undefined && attachmentId === undefined) {
|
|
372
|
+
throw new ConnectorCallError("invalid_args", "Supply partId from the attachment listing, or attachmentId.");
|
|
373
|
+
}
|
|
374
|
+
const message = asRecord(await client.json({ method: "GET", path: `/messages/${encodeURIComponent(messageId)}`, query: { format: "full" } }, ctx));
|
|
375
|
+
const attachments = walk(asRecord(message["payload"])).attachments;
|
|
376
|
+
// partId is immutable and authoritative when supplied. An attachmentId
|
|
377
|
+
// from the previous listing may have rotated since that messages.get.
|
|
378
|
+
const part = attachments.find((entry) => partId !== undefined ? entry["partId"] === partId : asRecord(entry["body"])["attachmentId"] === attachmentId);
|
|
379
|
+
if (!part) {
|
|
380
|
+
throw new ConnectorCallError("not_found", "This attachment is not among the message's carried parts. Read get_message again and use its immutable partId.");
|
|
381
|
+
}
|
|
382
|
+
const stored = asRecord(part["body"]);
|
|
383
|
+
const mime = mimeOf(part).split(";")[0].trim();
|
|
384
|
+
const parsedForward = mime === "message/rfc822" &&
|
|
385
|
+
!text(stored["attachmentId"]) &&
|
|
386
|
+
!text(stored["data"]) &&
|
|
387
|
+
asArray(part["parts"]).length > 0;
|
|
388
|
+
const size = parsedForward ? forwardedSize(part) : declaredSize(stored);
|
|
389
|
+
const maxBytes = typeof args["maxBytes"] === "number" ? args["maxBytes"] : RESULT_BUDGET_BYTES;
|
|
390
|
+
const maxChars = typeof args["maxChars"] === "number" ? args["maxChars"] : DEFAULT_MESSAGE_BODY_CHARS;
|
|
391
|
+
const base = compact({
|
|
392
|
+
messageId,
|
|
393
|
+
...ids({ partId: [part["partId"]] }),
|
|
394
|
+
filename: clipped(text(part["filename"]) ?? "", FIELD_BYTES),
|
|
395
|
+
mimeType: clipped(text(part["mimeType"]) ?? "application/octet-stream", FIELD_BYTES),
|
|
396
|
+
size,
|
|
397
|
+
});
|
|
398
|
+
const unavailable = (note) => ({ ...base, format: "unavailable", contentTruncated: false, note });
|
|
399
|
+
const isText = mime.startsWith("text/") ||
|
|
400
|
+
["application/json", "application/xml", "message/rfc822"].includes(mime) ||
|
|
401
|
+
mime.endsWith("+json") ||
|
|
402
|
+
mime.endsWith("+xml");
|
|
403
|
+
let envelope = { ...base, format: isText ? "text" : "base64", content: "", contentTruncated: false };
|
|
404
|
+
const fitting = Math.floor(Math.max(0, maxBytes - jsonBytes(envelope)) / 4) * 3;
|
|
405
|
+
const limit = isText ? MAX_ATTACHMENT_BYTES : Math.min(MAX_ATTACHMENT_BYTES, fitting);
|
|
406
|
+
const tooLarge = () => unavailable(limit < MAX_ATTACHMENT_BYTES
|
|
407
|
+
? `This binary attachment does not fit maxBytes (${maxBytes}), which holds ${limit} attachment bytes. Raise maxBytes, up to ${MAX_ATTACHMENT_RESULT_BYTES} for a direct call_tool only, to read up to ${MAX_ATTACHMENT_BYTES} bytes. No content is returned.`
|
|
408
|
+
: `This attachment exceeds the ${MAX_ATTACHMENT_BYTES}-byte download and inline cap. No content is returned; open it in Gmail.`);
|
|
409
|
+
// Declared size is checked before downloading, but actual bytes are checked
|
|
410
|
+
// too: inconsistent metadata must never bypass the cap or whole-result budget.
|
|
411
|
+
if (size === undefined)
|
|
412
|
+
return unavailable("Gmail did not declare a valid attachment size; no content is downloaded. Open it in Gmail.");
|
|
413
|
+
if (size > limit)
|
|
414
|
+
return tooLarge();
|
|
415
|
+
let decoded;
|
|
416
|
+
if (parsedForward) {
|
|
417
|
+
const own = walk(part, true).own;
|
|
418
|
+
const chosen = ownBody(own);
|
|
419
|
+
if (!chosen)
|
|
420
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
421
|
+
const bytes = await attachmentBytes(client, ctx, messageId, chosen);
|
|
422
|
+
if (!bytes)
|
|
423
|
+
return unavailable("Gmail returned no readable forwarded message body data; open it in Gmail.");
|
|
424
|
+
const rendered = decodeBytes(chosen, bytes);
|
|
425
|
+
if (!rendered.text.trim())
|
|
426
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
427
|
+
const headers = ["From", "To", "Date", "Subject"].flatMap((name) => {
|
|
428
|
+
const value = own.map((entry) => header(entry, name)).find((entry) => entry !== undefined);
|
|
429
|
+
return value === undefined ? [] : [`${name}: ${value}`];
|
|
430
|
+
});
|
|
431
|
+
const body = mimeOf(chosen) === "text/html" ? htmlToText(rendered.text) : rendered.text;
|
|
432
|
+
if (!body.trim())
|
|
433
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
434
|
+
decoded = headers.length > 0 ? `${headers.join("\n")}\n\n${body}` : body;
|
|
435
|
+
envelope = { ...envelope, ...compact({ note: rendered.note }) };
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const bytes = await attachmentBytes(client, ctx, messageId, part);
|
|
439
|
+
if (!bytes)
|
|
440
|
+
return unavailable("Gmail returned no attachment data; open it in Gmail.");
|
|
441
|
+
if (bytes.length > limit)
|
|
442
|
+
return tooLarge();
|
|
443
|
+
if (!isText) {
|
|
444
|
+
let binary = "";
|
|
445
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
446
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
|
447
|
+
}
|
|
448
|
+
return { ...envelope, content: btoa(binary) };
|
|
449
|
+
}
|
|
450
|
+
const rendered = decodeBytes(part, bytes);
|
|
451
|
+
decoded = rendered.text;
|
|
452
|
+
if (mime === "message/rfc822" && !decoded.trim()) {
|
|
453
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
454
|
+
}
|
|
455
|
+
envelope = { ...envelope, ...compact({ note: rendered.note }) };
|
|
456
|
+
}
|
|
457
|
+
const characters = Array.from(decoded);
|
|
458
|
+
const marker = (dropped) => `\n[… ${dropped} more characters truncated; raise maxChars or maxBytes for a direct call_tool, or open it in Gmail]`;
|
|
459
|
+
const byChars = characters.length > maxChars
|
|
460
|
+
? characters.slice(0, maxChars).join("") + marker(characters.length - maxChars)
|
|
461
|
+
: decoded;
|
|
462
|
+
if (jsonBytes({ ...envelope, content: byChars }) <= maxBytes) {
|
|
463
|
+
return { ...envelope, content: byChars, contentTruncated: characters.length > maxChars };
|
|
464
|
+
}
|
|
465
|
+
// Reserve the longest marker, then clamp only the original prefix. A text
|
|
466
|
+
// cut by both limits still has one marker, counted in the result's budget.
|
|
467
|
+
const prefix = characters.slice(0, maxChars).join("");
|
|
468
|
+
const contentBudget = maxBytes - jsonBytes(envelope) + jsonBytes("");
|
|
469
|
+
const kept = clampText(prefix, contentBudget - jsonBytes(marker(characters.length)) + 2, () => "");
|
|
470
|
+
const content = kept + marker(characters.length - Array.from(kept).length);
|
|
471
|
+
return { ...envelope, content, contentTruncated: true };
|
|
472
|
+
}
|
|
277
473
|
/**
|
|
278
474
|
* Per-field byte bounds, so every result is bounded by construction: a page
|
|
279
475
|
* of 25 summaries, or one message beside its body, stays under
|
|
@@ -334,6 +530,7 @@ async function projectMessage(client, ctx, value, maxBodyChars, budget = RESULT_
|
|
|
334
530
|
body: "",
|
|
335
531
|
bodyTruncated: false,
|
|
336
532
|
bodyFormat: read.bodyFormat,
|
|
533
|
+
bodyNote: read.bodyNote,
|
|
337
534
|
attachments: attachments.listed.length > 0 ? attachments.listed : undefined,
|
|
338
535
|
attachmentsOmitted: attachments.omitted > 0 ? attachments.omitted : undefined,
|
|
339
536
|
omittedIds,
|
|
@@ -428,10 +625,10 @@ function resultLimitMarker(dropped) {
|
|
|
428
625
|
* all, against the budget. The bounds above make a refusal here a
|
|
429
626
|
* should-not-happen; it exists so nothing oversized is ever handed on.
|
|
430
627
|
*/
|
|
431
|
-
function deliverable(tool, result) {
|
|
628
|
+
function deliverable(tool, result, budget = RESULT_BUDGET_BYTES) {
|
|
432
629
|
const size = jsonBytes(result);
|
|
433
|
-
if (size >
|
|
434
|
-
throw new ConnectorCallError("connector_call_failed", `Gmail's answer to ${tool} is ${size} bytes even after cutting, past connecta's ${
|
|
630
|
+
if (size > budget) {
|
|
631
|
+
throw new ConnectorCallError("connector_call_failed", `Gmail's answer to ${tool} is ${size} bytes even after cutting, past connecta's ${budget / 1024} KiB result limit. Open it in Gmail.`, { retryable: false });
|
|
435
632
|
}
|
|
436
633
|
return result;
|
|
437
634
|
}
|
|
@@ -870,6 +1067,7 @@ const ATTACHMENT_SCHEMA = {
|
|
|
870
1067
|
mimeType: { type: "string" },
|
|
871
1068
|
size: { type: "integer" },
|
|
872
1069
|
attachmentId: { type: "string" },
|
|
1070
|
+
partId: { type: "string" },
|
|
873
1071
|
omittedIds: OMITTED_IDS_SCHEMA,
|
|
874
1072
|
},
|
|
875
1073
|
};
|
|
@@ -892,6 +1090,7 @@ const MESSAGE_PROPERTIES = {
|
|
|
892
1090
|
body: { type: "string" },
|
|
893
1091
|
bodyTruncated: { type: "boolean" },
|
|
894
1092
|
bodyFormat: { type: "string", enum: ["text", "html", "none", "unavailable"] },
|
|
1093
|
+
bodyNote: { type: "string", description: "Decoding notice, preserved when the body is truncated." },
|
|
895
1094
|
attachments: { type: "array", items: ATTACHMENT_SCHEMA },
|
|
896
1095
|
attachmentsOmitted: { type: "integer" },
|
|
897
1096
|
};
|
|
@@ -1018,7 +1217,7 @@ function tools(client) {
|
|
|
1018
1217
|
},
|
|
1019
1218
|
{
|
|
1020
1219
|
name: "get_thread",
|
|
1021
|
-
description: "Get a Gmail thread's messages, oldest first, with headers, plain-text bodies capped by maxBodyChars, and attachment
|
|
1220
|
+
description: "Get a Gmail thread's messages, oldest first, with headers, plain-text bodies capped by maxBodyChars, and attachment metadata. Read attachments with get_attachment using partId. A long thread pages by cursor.",
|
|
1022
1221
|
annotations: readOnly,
|
|
1023
1222
|
inputSchema: input({
|
|
1024
1223
|
threadId: idProperty("Thread id from search_threads or a message's threadId."),
|
|
@@ -1079,7 +1278,7 @@ function tools(client) {
|
|
|
1079
1278
|
},
|
|
1080
1279
|
{
|
|
1081
1280
|
name: "get_message",
|
|
1082
|
-
description: "Get one Gmail message with headers, its plain-text body (HTML converted only when no text part exists), and attachment metadata.
|
|
1281
|
+
description: "Get one Gmail message with headers, its plain-text body (HTML converted only when no text part exists), and attachment metadata. Read attachments with get_attachment using partId.",
|
|
1083
1282
|
annotations: readOnly,
|
|
1084
1283
|
inputSchema: input({
|
|
1085
1284
|
messageId: idProperty("Message id from get_thread, or a draft's messageId."),
|
|
@@ -1108,6 +1307,58 @@ function tools(client) {
|
|
|
1108
1307
|
return message;
|
|
1109
1308
|
},
|
|
1110
1309
|
},
|
|
1310
|
+
{
|
|
1311
|
+
name: "get_attachment",
|
|
1312
|
+
description: "Read a Gmail attachment by immutable partId: text with its charset, or small binaries as standard base64 for Drive create_file. Refuses oversized downloads; text is capped by maxChars and the whole result by maxBytes.",
|
|
1313
|
+
annotations: readOnly,
|
|
1314
|
+
inputSchema: {
|
|
1315
|
+
...input({
|
|
1316
|
+
messageId: idProperty("Message id from get_message, get_thread, or get_draft."),
|
|
1317
|
+
partId: {
|
|
1318
|
+
type: "string",
|
|
1319
|
+
maxLength: ID_BYTES,
|
|
1320
|
+
description: "Immutable partId from the attachment listing, including an empty root id. Preferred; authoritative when attachmentId is also supplied.",
|
|
1321
|
+
},
|
|
1322
|
+
attachmentId: {
|
|
1323
|
+
type: "string",
|
|
1324
|
+
minLength: 1,
|
|
1325
|
+
maxLength: ATTACHMENT_ID_BYTES,
|
|
1326
|
+
pattern: ID_PATTERN,
|
|
1327
|
+
description: "Alternative attachmentId from the listing. Gmail can rotate it between reads; prefer partId.",
|
|
1328
|
+
},
|
|
1329
|
+
maxChars: {
|
|
1330
|
+
type: "integer",
|
|
1331
|
+
minimum: 1,
|
|
1332
|
+
maximum: MAX_BODY_CHARS,
|
|
1333
|
+
description: `Text code points kept; defaults to ${DEFAULT_MESSAGE_BODY_CHARS}. Longer text ends with a truncation marker. Ignored for binaries.`,
|
|
1334
|
+
},
|
|
1335
|
+
maxBytes: {
|
|
1336
|
+
type: "integer",
|
|
1337
|
+
minimum: MIN_ATTACHMENT_RESULT_BYTES,
|
|
1338
|
+
maximum: MAX_ATTACHMENT_RESULT_BYTES,
|
|
1339
|
+
description: `Whole-result UTF-8 JSON byte budget; defaults to ${RESULT_BUDGET_BYTES}. Raise it only for a direct call_tool; execute_code cannot receive large results.`,
|
|
1340
|
+
},
|
|
1341
|
+
}, ["messageId"]),
|
|
1342
|
+
anyOf: [{ required: ["partId"] }, { required: ["attachmentId"] }],
|
|
1343
|
+
},
|
|
1344
|
+
outputSchema: {
|
|
1345
|
+
type: "object",
|
|
1346
|
+
properties: {
|
|
1347
|
+
messageId: { type: "string" },
|
|
1348
|
+
partId: { type: "string" },
|
|
1349
|
+
omittedIds: OMITTED_IDS_SCHEMA,
|
|
1350
|
+
filename: { type: "string" },
|
|
1351
|
+
mimeType: { type: "string" },
|
|
1352
|
+
size: { type: "integer" },
|
|
1353
|
+
format: { type: "string", enum: ["text", "base64", "unavailable"] },
|
|
1354
|
+
content: { type: "string" },
|
|
1355
|
+
contentTruncated: { type: "boolean" },
|
|
1356
|
+
note: { type: "string" },
|
|
1357
|
+
},
|
|
1358
|
+
required: ["messageId", "filename", "mimeType", "format", "contentTruncated"],
|
|
1359
|
+
},
|
|
1360
|
+
handler: async (args, ctx) => await readAttachment(client, ctx, args),
|
|
1361
|
+
},
|
|
1111
1362
|
{
|
|
1112
1363
|
name: "list_labels",
|
|
1113
1364
|
description: "List the user's Gmail labels, system and user-created, with the ids messages carry; no message counts. Search by name with label: in search_threads.",
|
|
@@ -1271,7 +1522,7 @@ function tools(client) {
|
|
|
1271
1522
|
name: "create_draft",
|
|
1272
1523
|
description: "Create a Gmail draft, optionally as a reply in an existing thread. It is saved, never sent: the user reviews and sends it in Gmail.",
|
|
1273
1524
|
// Additive: a new draft changes nothing that existed. Not read-only, so
|
|
1274
|
-
// program calls require a
|
|
1525
|
+
// program calls require a writable endpoint; other writes use
|
|
1275
1526
|
// call_destructive_tool. The host controls approval.
|
|
1276
1527
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1277
1528
|
inputSchema: input({
|
|
@@ -1384,7 +1635,7 @@ function usageGuide(purpose, instructions) {
|
|
|
1384
1635
|
*
|
|
1385
1636
|
* `gmail.compose` technically allows sending; this connection never does,
|
|
1386
1637
|
* because no tool reaches a send method. Drafts are additive writes the host
|
|
1387
|
-
* approves through call_destructive_tool or a
|
|
1638
|
+
* approves through call_destructive_tool or a program on a writable endpoint.
|
|
1388
1639
|
*/
|
|
1389
1640
|
export const gmail = asProviderFactory({
|
|
1390
1641
|
name: "gmail",
|
|
@@ -1413,7 +1664,7 @@ function gmailConnector(id, options) {
|
|
|
1413
1664
|
notFound: "absent",
|
|
1414
1665
|
connection,
|
|
1415
1666
|
});
|
|
1416
|
-
return api(id, {
|
|
1667
|
+
return workspaceConnector(connection, api(id, {
|
|
1417
1668
|
...defined({
|
|
1418
1669
|
callAdmission: options.callAdmission,
|
|
1419
1670
|
maxResultBytes: options.maxResultBytes,
|
|
@@ -1430,7 +1681,9 @@ function gmailConnector(id, options) {
|
|
|
1430
1681
|
// Every tool's result passes the whole-size postcondition on its way out.
|
|
1431
1682
|
tools: tools(client).map((tool) => ({
|
|
1432
1683
|
...tool,
|
|
1433
|
-
handler: async (args, ctx) => deliverable(tool.name, await tool.handler(args, ctx)
|
|
1684
|
+
handler: async (args, ctx) => deliverable(tool.name, await tool.handler(args, ctx), tool.name === "get_attachment" && typeof args["maxBytes"] === "number"
|
|
1685
|
+
? args["maxBytes"]
|
|
1686
|
+
: RESULT_BUDGET_BYTES),
|
|
1434
1687
|
})),
|
|
1435
|
-
});
|
|
1688
|
+
}));
|
|
1436
1689
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: \n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument
|
|
3
|
+
"content": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: \n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared mailbox that every caller, agent or person, acts as. No argument\nnames a mailbox. A call with none mapped, or from an agent whose pool has no\nowner, fails `auth_required`; only an operator can change either.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n- Results default to at most 192 KiB; `list_labels` pages the same way.\n- `get_message` and `get_thread` list attachment metadata. An attached or\n forwarded email is an attachment, not the body.\n- `bodyFormat: \"unavailable\"` means Gmail stored the body apart from the\n message and it is too large to read here; say so rather than summarizing.\n- `get_message`, each `get_thread` message, and `get_draft` return `bodyNote`\n when decoding falls back to UTF-8 for an unsupported charset. The note\n survives character and result-byte truncation, including `maxBodyChars: 0`\n when inline body data is decoded. It counts toward the result's byte limit.\n\n## Attachments\n\n- `get_attachment` takes `messageId` and the listing's immutable `partId`.\n Prefer `partId`, which stays stable across message reads, including an\n empty id for a root attachment. `attachmentId` is an alternative that\n Gmail can rotate between reads. When both are supplied, `partId` wins and\n the fresh message's download id is used. Read the listing again after\n an unknown-id refusal; never store an attachmentId for later use.\n- Small attachments can have inline data and no attachmentId. Use their\n partId too. Only carried attachment parts can be read, never arbitrary\n message body parts or blind download ids.\n- Text, JSON, XML, calendar invites, CSV, and `message/rfc822` return\n `format: \"text\"` and `content` decoded in the declared charset. An unsupported\n charset produces an explicit `note` that decoding used UTF-8. Message bodies\n return the notice separately as `bodyNote`.\n Parsed forwarded emails render From, To, Date, Subject, and their own\n plain body, else HTML converted to text. Raw forwarded data is preferred\n when present. A forwarded email without readable text returns an\n `unavailable` note.\n `maxChars` defaults to 12,000 code points, at most 100,000. A character\n or byte cut ends with a truncation marker and `contentTruncated: true`.\n Reduce attachment text inside `execute_code` before returning it.\n- Binary attachments return `format: \"base64\"` and standard base64 in\n `content`. Pass it unchanged to Drive `create_file` as `contentBase64`,\n with `name: filename` and `mimeType`. File creation is a write: use\n `call_destructive_tool` or a program on a writable endpoint for that step.\n- Downloads, text and binary, are limited to 1 MiB. Declared size is\n checked before a separate download. Parsed forwarded emails check the sum\n of descendant sizes before downloading any stored body, and return that\n sum as `size`. Attachment data must match both the message part's declared\n size and the download response's size when supplied; inconsistent payloads\n fail rather than return incomplete files. Binary content must also fit the\n whole-result `maxBytes`, which defaults to 192 KiB. `format: \"unavailable\"`\n returns a note and no content when it cannot fit. Raising `maxChars`\n or `maxBytes` cannot bypass the download cap.\n- Raise `maxBytes` only for a direct `call_tool` read, from 64 KiB up to\n 4 MiB, as with Drive `get_file_content`. `execute_code` cannot receive\n large results; keep the default there. Direct-call results may be paged.\n\n## Drafting\n\n- `create_draft` saves a draft and never sends it; tell the person it is\n waiting in Drafts. There is no send, delete, or label tool.\n- To reply, pass `replyToMessageId`: the draft joins that thread with\n In-Reply-To and References set, and subject (Re: …) and `to` default\n from that message's Reply-To, else its From. Keep the Re: subject or Gmail\n may start a new thread.\n- `update_draft` replaces the body entirely; send the full new text.\n From, To, Cc, Bcc, Reply-To, Subject, the thread, and reply headers are\n kept unless restated; no other header is. A draft with attachments or\n inline images is refused, unchanged — Gmail's update would delete them.\n- Gmail sets From to the mailbox itself; there is no From argument.\n",
|
|
4
4
|
"instructionsHeading": "Connection instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"guide_0": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: ",
|
|
7
|
-
"guide_1": "\n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument
|
|
7
|
+
"guide_1": "\n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared mailbox that every caller, agent or person, acts as. No argument\nnames a mailbox. A call with none mapped, or from an agent whose pool has no\nowner, fails `auth_required`; only an operator can change either.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n- Results default to at most 192 KiB; `list_labels` pages the same way.\n- `get_message` and `get_thread` list attachment metadata. An attached or\n forwarded email is an attachment, not the body.\n- `bodyFormat: \"unavailable\"` means Gmail stored the body apart from the\n message and it is too large to read here; say so rather than summarizing.\n- `get_message`, each `get_thread` message, and `get_draft` return `bodyNote`\n when decoding falls back to UTF-8 for an unsupported charset. The note\n survives character and result-byte truncation, including `maxBodyChars: 0`\n when inline body data is decoded. It counts toward the result's byte limit.\n\n## Attachments\n\n- `get_attachment` takes `messageId` and the listing's immutable `partId`.\n Prefer `partId`, which stays stable across message reads, including an\n empty id for a root attachment. `attachmentId` is an alternative that\n Gmail can rotate between reads. When both are supplied, `partId` wins and\n the fresh message's download id is used. Read the listing again after\n an unknown-id refusal; never store an attachmentId for later use.\n- Small attachments can have inline data and no attachmentId. Use their\n partId too. Only carried attachment parts can be read, never arbitrary\n message body parts or blind download ids.\n- Text, JSON, XML, calendar invites, CSV, and `message/rfc822` return\n `format: \"text\"` and `content` decoded in the declared charset. An unsupported\n charset produces an explicit `note` that decoding used UTF-8. Message bodies\n return the notice separately as `bodyNote`.\n Parsed forwarded emails render From, To, Date, Subject, and their own\n plain body, else HTML converted to text. Raw forwarded data is preferred\n when present. A forwarded email without readable text returns an\n `unavailable` note.\n `maxChars` defaults to 12,000 code points, at most 100,000. A character\n or byte cut ends with a truncation marker and `contentTruncated: true`.\n Reduce attachment text inside `execute_code` before returning it.\n- Binary attachments return `format: \"base64\"` and standard base64 in\n `content`. Pass it unchanged to Drive `create_file` as `contentBase64`,\n with `name: filename` and `mimeType`. File creation is a write: use\n `call_destructive_tool` or a program on a writable endpoint for that step.\n- Downloads, text and binary, are limited to 1 MiB. Declared size is\n checked before a separate download. Parsed forwarded emails check the sum\n of descendant sizes before downloading any stored body, and return that\n sum as `size`. Attachment data must match both the message part's declared\n size and the download response's size when supplied; inconsistent payloads\n fail rather than return incomplete files. Binary content must also fit the\n whole-result `maxBytes`, which defaults to 192 KiB. `format: \"unavailable\"`\n returns a note and no content when it cannot fit. Raising `maxChars`\n or `maxBytes` cannot bypass the download cap.\n- Raise `maxBytes` only for a direct `call_tool` read, from 64 KiB up to\n 4 MiB, as with Drive `get_file_content`. `execute_code` cannot receive\n large results; keep the default there. Direct-call results may be paged.\n\n## Drafting\n\n- `create_draft` saves a draft and never sends it; tell the person it is\n waiting in Drafts. There is no send, delete, or label tool.\n- To reply, pass `replyToMessageId`: the draft joins that thread with\n In-Reply-To and References set, and subject (Re: …) and `to` default\n from that message's Reply-To, else its From. Keep the Re: subject or Gmail\n may start a new thread.\n- `update_draft` replaces the body entirely; send the full new text.\n From, To, Cc, Bcc, Reply-To, Subject, the thread, and reply headers are\n kept unless restated; no other header is. A draft with attachments or\n inline images is refused, unchanged — Gmail's update would delete them.\n- Gmail sets From to the mailbox itself; there is no From argument.\n"
|
|
8
8
|
}
|
|
9
9
|
};
|
|
@@ -302,7 +302,7 @@ export const linear = defineProvider({
|
|
|
302
302
|
// the summary without the description.
|
|
303
303
|
const accessNote = access === "read-only"
|
|
304
304
|
? "Read-only connection: bound to Linear's read-only endpoint, whose token is scope-limited downstream, so every write fails at Linear regardless of arguments. Route writes to a connector configured for read-write access."
|
|
305
|
-
: "Read-write connection: treat every `save_`, `create_`, `delete_`, `resolve_`, `submit_`, and `merge_` operation as a write. Connecta classifies the maintained writes explicitly and
|
|
305
|
+
: "Read-write connection: treat every `save_`, `create_`, `delete_`, `resolve_`, `submit_`, and `merge_` operation as a write. Connecta classifies the maintained writes explicitly, and a read-only endpoint refuses them. Unknown tools without an explicit, uncontradicted read annotation fail closed.";
|
|
306
306
|
return remoteMcp(id, {
|
|
307
307
|
url: LINEAR_MCP_ENDPOINTS[access],
|
|
308
308
|
...provider.connectorOptions,
|
|
@@ -2,7 +2,7 @@ import { type RemoteMcpAuth } from "../../connectors/remote-mcp.js";
|
|
|
2
2
|
import type { ConnectorCallAdmissionPolicy } from "../../types.js";
|
|
3
3
|
export type MixpanelRegion = "us" | "eu" | "in";
|
|
4
4
|
export declare const MIXPANEL_MCP_ENDPOINTS: Readonly<Record<MixpanelRegion, string>>;
|
|
5
|
-
|
|
5
|
+
interface MixpanelCommonOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Human-readable display name; defaults to "Mixpanel (<region>)". The region
|
|
8
8
|
* rides the title because a project lives in exactly one residency and
|
|
@@ -18,25 +18,40 @@ export interface MixpanelOptions {
|
|
|
18
18
|
* region answers empty rather than wrong, which reads as an empty project.
|
|
19
19
|
*/
|
|
20
20
|
region?: MixpanelRegion;
|
|
21
|
-
/**
|
|
22
|
-
* OAuth by default; static headers support Mixpanel service accounts, and
|
|
23
|
-
* `{ type: "credential" }` takes the same service account as an
|
|
24
|
-
* operator-managed `username:secret` that Connecta frames for the endpoint.
|
|
25
|
-
*/
|
|
26
|
-
auth?: RemoteMcpAuth;
|
|
27
21
|
/** Account-specific conventions appended to the maintained provider guide. */
|
|
28
22
|
instructions?: string;
|
|
29
23
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
30
24
|
maxResultBytes?: number;
|
|
31
25
|
/**
|
|
32
|
-
* Optional per-runtime policy.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* Only an operator who knows the account can pick a number worth enforcing.
|
|
26
|
+
* Optional per-runtime policy. API mode defaults to 60 calls/hour and five
|
|
27
|
+
* concurrent calls across all tools and projects. MCP mode has no default:
|
|
28
|
+
* its per-user hourly quota cannot be modeled by one per-runtime counter.
|
|
36
29
|
*/
|
|
37
30
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
38
31
|
}
|
|
39
|
-
/**
|
|
32
|
+
/** The default hosted MCP connection. Existing auth configurations still work. */
|
|
33
|
+
export interface MixpanelMcpOptions extends MixpanelCommonOptions {
|
|
34
|
+
via?: "mcp";
|
|
35
|
+
/**
|
|
36
|
+
* OAuth by default; static headers support Mixpanel service accounts, and
|
|
37
|
+
* `{ type: "credential" }` takes the same service account as an
|
|
38
|
+
* operator-managed `username:secret` that Connecta frames for the endpoint.
|
|
39
|
+
*/
|
|
40
|
+
auth?: RemoteMcpAuth;
|
|
41
|
+
defaultProjectId?: never;
|
|
42
|
+
}
|
|
43
|
+
/** REST APIs using an operator-managed username:secret service account. */
|
|
44
|
+
export interface MixpanelApiOptions extends MixpanelCommonOptions {
|
|
45
|
+
via: "api";
|
|
46
|
+
auth: {
|
|
47
|
+
type: "credential";
|
|
48
|
+
};
|
|
49
|
+
/** Fills an omitted project_id. Explicit ids still reach other granted projects. */
|
|
50
|
+
defaultProjectId?: number;
|
|
51
|
+
}
|
|
52
|
+
export type MixpanelOptions = MixpanelMcpOptions | MixpanelApiOptions;
|
|
53
|
+
/** Mixpanel hosted MCP by default, or service-account REST with via: "api". */
|
|
40
54
|
export declare const mixpanel: import("../../provider.js").ProviderFactory<MixpanelOptions>;
|
|
41
55
|
/** @deprecated Read `mixpanel.definition.classify` instead. Kept for existing imports. */
|
|
42
56
|
export declare const MIXPANEL_VETTED_CATALOG: import("../../catalog-drift.js").VettedCatalog;
|
|
57
|
+
export {};
|