@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. 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
- return /charset="?([^";\s]+)"?/i.exec(type)?.[1] ?? "utf-8";
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, in the part's declared charset where the runtime knows it. */
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(charsetOf(part)).decode(bytes);
150
+ return { text: new TextDecoder(charset).decode(bytes) };
134
151
  }
135
152
  catch {
136
- return new TextDecoder().decode(bytes);
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 never entered.
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 plain = own.find((part) => mimeOf(part) === "text/plain");
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 { text: format === "html" ? htmlToText(decoded) : decoded, bodyFormat: format };
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({ attachmentId: [asRecord(part["body"])["attachmentId"], ATTACHMENT_ID_BYTES] }),
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 > RESULT_BUDGET_BYTES) {
434
- throw new ConnectorCallError("connector_call_failed", `Gmail's answer to ${tool} is ${size} bytes even after cutting, past connecta's ${RESULT_BUDGET_BYTES / 1024} KiB result limit. Open it in Gmail.`, { retryable: false });
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 names. A long thread pages by cursor.",
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. Never downloads attachments.",
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 trusted pool; other writes use
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 trusted program.
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 names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\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- Every result stays under 192 KiB; `list_labels` pages the same way.\n- Attachments are listed by name, type, and size only; none is downloaded.\n An attached or 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\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",
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 names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\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- Every result stays under 192 KiB; `list_labels` pages the same way.\n- Attachments are listed by name, type, and size only; none is downloaded.\n An attached or 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\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"
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 enforces the configured pool trust policy. Unknown tools without an explicit, uncontradicted read annotation fail closed.";
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
- export interface MixpanelOptions {
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. There is no default: Mixpanel meters its MCP
33
- * server per user per hour, and a per-runtime counter over-counts one runtime
34
- * serving many users and under-counts many runtimes sharing one credential.
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
- /** A maintained Mixpanel hosted-MCP connection. */
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 {};