@zackbart/connecta 0.24.2 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
@@ -1,4 +1,14 @@
1
- /** See documentation/cloudflare.md#no-sdk-on-purpose. */
1
+ /**
2
+ * No official `cloudflare` SDK on purpose. What the SDK sells — typed request
3
+ * wrappers and pagination helpers — is what this connection replaces: an agent
4
+ * gets a projected result and a `page.hasMore` boolean, so the SDK's types
5
+ * would be re-projected away at the boundary, and the dependency would cost an
6
+ * optional peer, an install step, and an import that never belongs in the root
7
+ * graph. Cloudflare's v4 API is authenticated `fetch` over a uniform
8
+ * `{ success, errors, messages, result, result_info }` envelope, so Web APIs
9
+ * alone keep this provider Workers-clean. `test/package-surface.test.ts` pins
10
+ * it: no `cloudflare` package in any dependency field, every import relative.
11
+ */
2
12
  import { api, defined } from "../connectors/api.js";
3
13
  import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
4
14
  import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
@@ -8,7 +18,11 @@ import { ConnectorCallError } from "../errors.js";
8
18
  export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
9
19
  /** Cloudflare's official whole-API hosted MCP endpoint. */
10
20
  export const CLOUDFLARE_MCP_ENDPOINT = "https://mcp.cloudflare.com/mcp";
11
- /** See documentation/cloudflare.md#dns-record-types. */
21
+ /**
22
+ * The 21 record types Cloudflare accepts. Eight carry a single `content`
23
+ * string; the other thirteen carry a per-type structured `data` object.
24
+ * `list_dns_records` filters on all 21 because reading them costs nothing.
25
+ */
12
26
  export const CLOUDFLARE_DNS_RECORD_TYPES = [
13
27
  "A",
14
28
  "AAAA",
@@ -32,7 +46,15 @@ export const CLOUDFLARE_DNS_RECORD_TYPES = [
32
46
  "TXT",
33
47
  "URI",
34
48
  ];
35
- /** Content-valued types only; see documentation/cloudflare.md#dns-record-types. */
49
+ /**
50
+ * Content-valued types only, and the only types `create_dns_record` and
51
+ * `update_dns_record` accept. Covering the thirteen structured-`data` types
52
+ * would mean either a free-form `data` passthrough — the untyped body this
53
+ * connection exists to avoid — or thirteen more hand-written schemas for record
54
+ * types that are rare in day-to-day zone administration. They stay fully
55
+ * readable; writing one goes through the approval-gated raw mutation tool with
56
+ * Cloudflare's documented per-type `data` body.
57
+ */
36
58
  export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
37
59
  "A",
38
60
  "AAAA",
@@ -43,7 +65,17 @@ export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
43
65
  "PTR",
44
66
  "TXT",
45
67
  ];
46
- /** See documentation/cloudflare.md#rate-limits. */
68
+ /**
69
+ * Cloudflare documents a global limit of 1,200 requests per five minutes per
70
+ * user, counted cumulatively across the dashboard, API keys, and API tokens
71
+ * (developers.cloudflare.com/fundamentals/api/reference/limits/). The matching
72
+ * rolling window here is a best-effort approximation, not an enforcement: each
73
+ * runtime keeps its own counter, so N isolates or processes can each admit
74
+ * 1,200, and a human's dashboard traffic counts for Cloudflare but not for us.
75
+ * `maxConcurrency` is the bound that actually protects a shared credential,
76
+ * because one `execute_code` program can fan out faster than the window
77
+ * notices.
78
+ */
47
79
  function admissionPolicy(maxConcurrency) {
48
80
  return {
49
81
  rules: [
@@ -155,7 +187,17 @@ function errorCodes(errors) {
155
187
  * would risk telling an agent its token was broken when its arguments were.
156
188
  * Genuine 10000 auth failures arrive with 401 or 403 and are caught by status.
157
189
  *
158
- * See documentation/cloudflare.md#typed-failures for provenance and routing.
190
+ * Provenance worth knowing before editing this set: Cloudflare publishes no
191
+ * official table mapping error codes to causes, so these six — and the 10000
192
+ * observation above — come from community reports and probing rather than
193
+ * documentation. They are well-supported readings Cloudflare could invalidate
194
+ * without notice; prefer `verify_api_token` or `verify_global_api_key` when a
195
+ * diagnosis actually matters.
196
+ *
197
+ * Cloudflare also rate-limits authentication failures separately from the
198
+ * global limit: a few requests with a bad token return 429 with code 10502.
199
+ * That is one more reason to diagnose a broken token once with a verify tool
200
+ * rather than by retrying real calls.
159
201
  */
160
202
  const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
161
203
  /**
@@ -169,7 +211,9 @@ const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
169
211
  function failureFor(status, headers, errors) {
170
212
  const detail = describeErrors(errors);
171
213
  const codes = errorCodes(errors);
172
- // Ordering rationale: documentation/cloudflare.md#typed-failures.
214
+ // 429 is checked before the authentication codes on purpose: Cloudflare
215
+ // reuses the generic 10000 code on throttled responses, and reading a rate
216
+ // limit as an auth failure would tell an agent to stop when it should wait.
173
217
  if (status === 429) {
174
218
  const wait = retryAfterMs(headers);
175
219
  return new ConnectorCallError("rate_limited", `Cloudflare rate limit reached (HTTP 429). ${detail} The documented limit is 1,200 requests per five minutes per user, counted across the dashboard and every token.`,
@@ -184,7 +228,10 @@ function failureFor(status, headers, errors) {
184
228
  if (status === 400 || status === 409 || status === 422) {
185
229
  return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
186
230
  }
187
- // 404 rationale: documentation/cloudflare.md#typed-failures.
231
+ // Cloudflare refuses a token that may not touch a resource with 401 or 403,
232
+ // so a 404 here is a real absence rather than a permission gap wearing a
233
+ // miss — the unambiguous case `not_found` exists for, and something an agent
234
+ // can act on by re-running discovery for the id.
188
235
  if (status === 404) {
189
236
  return new ConnectorCallError("not_found", `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`);
190
237
  }
@@ -576,7 +623,17 @@ const RAW_INPUT_PROPERTY = {
576
623
  type: "boolean",
577
624
  description: "Return Cloudflare's unprojected result instead of the lean shape. Use only when a field the projection drops is genuinely needed; the raw shape is much larger.",
578
625
  };
579
- /** Bound provenance: documentation/cloudflare.md#where-the-perpage-bounds-come-from. */
626
+ /**
627
+ * Local enforcement of a `perPage` range is only a favor when the bound is
628
+ * really Cloudflare's, so `bounds` decides what the description admits to.
629
+ * `list_accounts`/`list_zones` (5–50) and `list_kv_namespaces` (1–1000) are
630
+ * Cloudflare's documented bounds. `list_dns_records` takes Cloudflare's
631
+ * minimum but is `clamped` at 1,000: Cloudflare's schema documents `per_page`
632
+ * on `/zones/{id}/dns_records` up to 5,000,000, a nominal ceiling no listing
633
+ * will honor, and a local cap an agent is told about beats a page size that
634
+ * fails somewhere inside Cloudflare. `list_pages_projects` is `undocumented` —
635
+ * Cloudflare publishes no bounds and no default for it, so 1–100 is ours.
636
+ */
580
637
  function pagingInputProperties(minPerPage, maxPerPage, options = {}) {
581
638
  const { defaultPerPage, bounds = "cloudflare" } = options;
582
639
  const defaultNote = defaultPerPage === undefined
@@ -601,7 +658,12 @@ function pagingInputProperties(minPerPage, maxPerPage, options = {}) {
601
658
  },
602
659
  };
603
660
  }
604
- /** Cursor convention: documentation/cloudflare.md#results. */
661
+ /**
662
+ * Four endpoints — `list_zone_rulesets`, `list_r2_buckets`, `list_r2_objects`,
663
+ * `list_kv_keys` — page by cursor and get no `page` object. Both the argument
664
+ * and the result say so, so the loop condition is legible from either end of
665
+ * one tool rather than only to a reader who compared all of them.
666
+ */
605
667
  const CURSOR_INPUT_PROPERTY = {
606
668
  type: "string",
607
669
  description: "Opaque cursor from a previous call's nextCursor. This endpoint pages by cursor, not page number.",
@@ -1013,7 +1075,12 @@ const QUERY_INPUT_PROPERTY = {
1013
1075
  additionalProperties: false,
1014
1076
  },
1015
1077
  };
1016
- // Header boundary: documentation/cloudflare.md#the-whole-v4-escape-hatch.
1078
+ // Connector-owned and refused: Authorization, Cookie, Host, Content-Length,
1079
+ // Content-Type, Transfer-Encoding — authentication, host selection, content
1080
+ // type, and request framing are not the caller's to set. That list stays out of
1081
+ // this description on purpose: the compact renderer inlines a shared property
1082
+ // description once per tool, and spelling the six out three times pushed
1083
+ // cloudflare_api_upload's compact input past the 1,024-byte discovery budget.
1017
1084
  const HEADERS_INPUT_PROPERTY = {
1018
1085
  type: "array",
1019
1086
  description: "Endpoint headers as name/value pairs, e.g. cf-r2-jurisdiction or Range. Connector-owned headers are refused.",
@@ -1412,7 +1479,11 @@ function buildTools(scope, authentication) {
1412
1479
  path: "/zones",
1413
1480
  query: {
1414
1481
  name: optionalString(args, "name"),
1415
- // Undefaulted on purpose see documentation/cloudflare.md#scoping.
1482
+ // Undefaulted on purpose: list_zones is the discovery step,
1483
+ // and quietly filtering it by a configured accountId would
1484
+ // be a restriction in all but name — one with no argument
1485
+ // that escapes it, since an empty accountId would fall back
1486
+ // to the default again.
1416
1487
  "account.id": optionalString(args, "accountId"),
1417
1488
  status: optionalString(args, "status"),
1418
1489
  page: optionalNumber(args, "page"),
@@ -1424,7 +1495,13 @@ function buildTools(scope, authentication) {
1424
1495
  cfTool("get_zone", "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.", readOnly, "zoneId", scope.zoneId, {
1425
1496
  raw: RAW_INPUT_PROPERTY
1426
1497
  }, [], ZONE_SCHEMA, getResult(send, (args) => ({ method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }), (result, args) => args["raw"] === true ? result : projectZone(result))),
1427
- // Removed tools: documentation/cloudflare.md#what-the-named-surface-deliberately-leaves-out.
1498
+ // No bulk `list_zone_settings` on purpose (#361): Cloudflare's published
1499
+ // document marks GET /zones/{zoneId}/settings and its PATCH sibling
1500
+ // deprecated with no bulk replacement, and the tool projected nothing while
1501
+ // *growing* the payload 22.7% by wrapping an unpaginated settings array in a
1502
+ // page object. Read one setting here; an operator who genuinely wants the
1503
+ // whole set names /zones/{zoneId}/settings through cloudflare_api_get, at
1504
+ // the caller's risk rather than promised by connecta's catalog.
1428
1505
  cfTool("get_zone_setting", "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.", readOnly, "zoneId", scope.zoneId, {
1429
1506
  settingId: SETTING_ID_PROPERTY,
1430
1507
  raw: RAW_INPUT_PROPERTY
@@ -2019,8 +2096,18 @@ function buildTools(scope, authentication) {
2019
2096
  path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
2020
2097
  headers: r2Headers(args),
2021
2098
  }))),
2022
- // No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` on purpose (#350)
2023
- // see documentation/cloudflare.md#what-the-named-surface-deliberately-leaves-out.
2099
+ // No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` on purpose (#350).
2100
+ // A named tool is a permanent line item in every deployment's catalog, and
2101
+ // these lost the comparison against the escape hatches: `get_r2_metrics`
2102
+ // took an account id, put it in a path, and returned the response untouched,
2103
+ // which `cloudflare_api_get` at /accounts/{accountId}/r2/metrics already
2104
+ // does. `set_r2_cors` declared its rule list as free-form objects — the
2105
+ // untyped body refused everywhere else — and returned Cloudflare's response
2106
+ // unprojected, so it beat the raw route on nothing. `delete_r2_cors`
2107
+ // validated fine and went anyway, because naming only the delete would mean
2108
+ // one CORS policy is set through the raw route and cleared through a named
2109
+ // tool. Read with `get_r2_cors`; write with `cloudflare_api_mutate` at
2110
+ // PUT/DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors.
2024
2111
  cfTool("get_r2_cors", "Get the browser CORS rules configured on an R2 bucket.", readOnly, "accountId", scope.accountId, {
2025
2112
  bucketName: R2_BUCKET_NAME_PROPERTY,
2026
2113
  jurisdiction: R2_JURISDICTION_PROPERTY
@@ -2342,8 +2429,12 @@ function buildTools(scope, authentication) {
2342
2429
  const zoneId = zoneArg(args);
2343
2430
  const targeted = ["files", "tags", "hosts", "prefixes"].filter((key) => Array.isArray(args[key]) && args[key].length > 0);
2344
2431
  const everything = args["everything"] === true;
2345
- // Cloudflare's purge body accepts exactly one variant. Refusing here
2346
- // turns a confusing provider 400 into a schema-shaped failure.
2432
+ // One variant per call is this connection's contract, not a
2433
+ // documented API restriction: Cloudflare's schema models the body as
2434
+ // `anyOf`, which does not forbid combining. Refusing locally gives an
2435
+ // agent `invalid_args` naming the conflict instead of a purge whose
2436
+ // actual scope is ambiguous. Combined purging would be a deliberate
2437
+ // change here.
2347
2438
  if (everything && targeted.length > 0) {
2348
2439
  throw new ConnectorCallError("invalid_args", "purge_cache takes either everything: true or one targeted list, never both.", {
2349
2440
  validation: {
@@ -19,18 +19,40 @@ export interface LinearOptions {
19
19
  authScope?: "shared" | "personal";
20
20
  /** Which workspace this is and what decisions it answers. */
21
21
  purpose: string;
22
- /** Required endpoint selection; see `documentation/linear.md`. */
22
+ /**
23
+ * Endpoint selection, required with no default. Linear's read-only endpoint
24
+ * advertises the `read` scope alone, so its token cannot reach the write APIs
25
+ * — a stronger guarantee than any annotation. Neither default is safe:
26
+ * read-write hands out access nobody asked for, and read-only breaks a
27
+ * writing deployment at runtime, where no agent can repair it.
28
+ */
23
29
  access: LinearAccess;
24
- /** OAuth or a personal API key; see `documentation/linear.md`. */
30
+ /**
31
+ * OAuth 2.1 with dynamic client registration by default. Linear's MCP server
32
+ * also accepts a personal API key in `Authorization: Bearer` — note this is
33
+ * the MCP endpoint's own documented contract, not the GraphQL API convention.
34
+ * A personal key carries the acting user's full permissions, so pair it with
35
+ * `access: "read-only"` unless the deployment genuinely writes.
36
+ */
25
37
  auth?: RemoteMcpAuth;
26
38
  /** Workspace-specific conventions appended to the maintained provider guide. */
27
39
  instructions?: string;
28
40
  /** Connector-specific inline result limit; omit to inherit the deployment. */
29
41
  maxResultBytes?: number;
30
- /** Optional per-runtime policy; see `documentation/linear.md#rate-limits`. */
42
+ /**
43
+ * Optional per-runtime policy. There is no default: Linear documents no
44
+ * MCP-specific limit, the GraphQL limits it rides are metered per user per
45
+ * hour (and its own page disagrees with itself on the API-key figure), and
46
+ * Connecta's counter is per runtime. Only the operator knows the workspace.
47
+ */
31
48
  callAdmission?: ConnectorCallAdmissionPolicy;
32
49
  }
33
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
50
+ /**
51
+ * One release-reviewed manifest, used both to classify a live tool and as the
52
+ * baseline the drift check compares against, so the annotation a caller gets
53
+ * and the verdict a check reads can never disagree. Drift must surface as an
54
+ * unclassified tool on the approval path (P5), never a quiet re-guess.
55
+ */
34
56
  export declare const LINEAR_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
35
57
  /** A maintained Linear hosted-MCP connection. */
36
58
  export declare function linear(id: string, options: LinearOptions): Connector;
@@ -11,7 +11,11 @@ export const LINEAR_MCP_ENDPOINTS = {
11
11
  "read-write": "https://mcp.linear.app/mcp",
12
12
  "read-only": "https://mcp.linear.app/mcp/readonly",
13
13
  };
14
- /** Reviewed reads; see `documentation/linear.md` and provider convention P5. */
14
+ /**
15
+ * Reviewed reads, listed by name (P5). The list is a superset: Linear's hosted
16
+ * `tools/list` varies by plan and enabled features, so a name this workspace
17
+ * never returns costs nothing while an unlisted new one fails closed.
18
+ */
15
19
  const READ_ONLY_TOOLS = new Set([
16
20
  // Issues
17
21
  "list_issues",
@@ -73,7 +77,13 @@ const READ_ONLY_TOOLS = new Set([
73
77
  // than argues.
74
78
  "extract_images",
75
79
  ]);
76
- /** Reviewed writes; `save_*` upsert rationale lives in `documentation/linear.md`. */
80
+ /**
81
+ * Reviewed writes with their destructive verdict. Every `save_*` is destructive
82
+ * because Linear's `save_*` tools are upserts: omitting a record id creates,
83
+ * supplying one overwrites in place, and the schema cannot tell them apart. The
84
+ * genuine creates — the `create_*_label` and attachment tools — stay additive,
85
+ * since `readOnlyHint: false` already routes them through the approval path.
86
+ */
77
87
  const WRITE_TOOLS = new Map([
78
88
  // Issues
79
89
  ["save_issue", "destructive"],
@@ -107,7 +117,7 @@ const WRITE_TOOLS = new Map([
107
117
  ["create_attachment_from_upload", "additive"],
108
118
  ["create_attachment", "additive"],
109
119
  ["delete_attachment", "destructive"],
110
- // Explicit issue access
120
+ // Explicit issue access. Both halves change an existing issue's audience.
111
121
  ["share_issue", "destructive"],
112
122
  ["unshare_issue", "destructive"],
113
123
  // Customer requests (plan-gated)
@@ -116,7 +126,12 @@ const WRITE_TOOLS = new Map([
116
126
  ["save_customer_need", "destructive"],
117
127
  ["delete_customer_need", "destructive"],
118
128
  ]);
119
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
129
+ /**
130
+ * One release-reviewed manifest, used both to classify a live tool and as the
131
+ * baseline the drift check compares against, so the annotation a caller gets
132
+ * and the verdict a check reads can never disagree. Drift must surface as an
133
+ * unclassified tool on the approval path (P5), never a quiet re-guess.
134
+ */
120
135
  export const LINEAR_VETTED_CATALOG = vettedCatalog({
121
136
  reads: READ_ONLY_TOOLS,
122
137
  writes: WRITE_TOOLS,
@@ -13,7 +13,10 @@ export interface MixpanelOptions {
13
13
  authScope?: "shared" | "personal";
14
14
  /** Who should use this account and for what decisions. */
15
15
  purpose: string;
16
- /** Data residency region; see `documentation/mixpanel.md`. */
16
+ /**
17
+ * Data residency region, selecting the matching official endpoint. A wrong
18
+ * region answers empty rather than wrong, which reads as an empty project.
19
+ */
17
20
  region?: MixpanelRegion;
18
21
  /**
19
22
  * OAuth by default; static headers support Mixpanel service accounts, and
@@ -25,10 +28,20 @@ export interface MixpanelOptions {
25
28
  instructions?: string;
26
29
  /** Connector-specific inline result limit; omit to inherit the deployment. */
27
30
  maxResultBytes?: number;
28
- /** Optional per-runtime policy; see `documentation/mixpanel.md#rate-limits`. */
31
+ /**
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.
36
+ */
29
37
  callAdmission?: ConnectorCallAdmissionPolicy;
30
38
  }
31
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
39
+ /**
40
+ * One release-reviewed manifest, used both to classify a live tool and as the
41
+ * baseline the drift check compares against, so the annotation a caller gets
42
+ * and the verdict a check reads can never disagree. Drift must surface as an
43
+ * unclassified tool on the approval path (P5), never a quiet re-guess.
44
+ */
32
45
  export declare const MIXPANEL_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
33
46
  /** A maintained Mixpanel hosted-MCP connection. */
34
47
  export declare function mixpanel(id: string, options: MixpanelOptions): Connector;
@@ -44,7 +44,12 @@ const READ_ONLY_TOOLS = new Set([
44
44
  "Get-Feature-Flag-Setup-Guidance",
45
45
  "Get-Feature-Flag-Lifecycle-Guidance",
46
46
  ]);
47
- /** Reviewed writes; see provider convention P5. */
47
+ /**
48
+ * Reviewed writes, each with its destructive verdict (P5). Additive writes stay
49
+ * `additive` because `readOnlyHint: false` already routes them through the
50
+ * approval path, and claiming destruction only inflates the copy a host shows a
51
+ * human. Anything unlisted is not read-only, so a new name fails closed.
52
+ */
48
53
  const WRITE_TOOLS = new Map([
49
54
  ["Create-Dashboard", "additive"],
50
55
  ["Update-Dashboard", "destructive"],
@@ -74,6 +79,7 @@ const WRITE_TOOLS = new Map([
74
79
  ["Update-Experiment", "destructive"],
75
80
  ["Create-Feature-Flag", "additive"],
76
81
  ["Update-Feature-Flag", "destructive"],
82
+ // Applies generated names and descriptions to existing Lexicon events.
77
83
  ["Fill-Event-Metadata", "destructive"],
78
84
  ]);
79
85
  /** Live US hosted-MCP schemas reviewed read-only on 2026-08-30 (#395, #512). */
@@ -143,7 +149,12 @@ const MIXPANEL_SCHEMA_DIGESTS = {
143
149
  "Update-Lookup-Table": "sha256:ef28f1ec9c9484a7a53b5e2b6659e70f79a8e71e55bda17025aa4ab3c23b6a63",
144
150
  "Update-Metric": "sha256:739bb6abdab19282afd4a6644a96183daabe9098a5322c44a77b840608a5ee9d",
145
151
  };
146
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
152
+ /**
153
+ * One release-reviewed manifest, used both to classify a live tool and as the
154
+ * baseline the drift check compares against, so the annotation a caller gets
155
+ * and the verdict a check reads can never disagree. Drift must surface as an
156
+ * unclassified tool on the approval path (P5), never a quiet re-guess.
157
+ */
147
158
  export const MIXPANEL_VETTED_CATALOG = vettedCatalog({
148
159
  reads: READ_ONLY_TOOLS,
149
160
  writes: WRITE_TOOLS,
@@ -3,7 +3,14 @@ import type { Connector, ConnectorCallAdmissionPolicy } from "../types.js";
3
3
  export declare const NOTION_API_BASE_URL = "https://api.notion.com";
4
4
  /** Notion's official hosted MCP endpoint. */
5
5
  export declare const NOTION_MCP_ENDPOINT = "https://mcp.notion.com/mcp";
6
- /** See documentation/notion.md#the-pinned-api-version. */
6
+ /**
7
+ * Pinned with no override. Notion's date-named versions keep working
8
+ * indefinitely, which makes an override look harmless; it is not. `2026-03-11`
9
+ * is where databases split into data sources, `archived` became `in_trash`, and
10
+ * block append took a `position` object instead of an `after` string. Every
11
+ * projection and write body here assumes those shapes, so an older version would
12
+ * return quietly wrong results rather than fail loudly.
13
+ */
7
14
  export declare const NOTION_API_VERSION = "2026-03-11";
8
15
  interface NotionCommonOptions {
9
16
  /** Human-readable display name; defaults identify the selected interface. */
@@ -7,7 +7,14 @@ import { ConnectorCallError } from "../errors.js";
7
7
  export const NOTION_API_BASE_URL = "https://api.notion.com";
8
8
  /** Notion's official hosted MCP endpoint. */
9
9
  export const NOTION_MCP_ENDPOINT = "https://mcp.notion.com/mcp";
10
- /** See documentation/notion.md#the-pinned-api-version. */
10
+ /**
11
+ * Pinned with no override. Notion's date-named versions keep working
12
+ * indefinitely, which makes an override look harmless; it is not. `2026-03-11`
13
+ * is where databases split into data sources, `archived` became `in_trash`, and
14
+ * block append took a `position` object instead of an `after` string. Every
15
+ * projection and write body here assumes those shapes, so an older version would
16
+ * return quietly wrong results rather than fail loudly.
17
+ */
11
18
  export const NOTION_API_VERSION = "2026-03-11";
12
19
  /** Notion's hard cap on `page_size` for every paginated endpoint. */
13
20
  const MAX_PAGE_SIZE = 100;
@@ -15,7 +22,12 @@ const MAX_PAGE_SIZE = 100;
15
22
  const DEFAULT_PAGE_SIZE = 25;
16
23
  /** Notion's cap on `children` per append, and on blocks per page create. */
17
24
  const MAX_CHILDREN_PER_REQUEST = 100;
18
- /** See documentation/notion.md#rate-limiting. */
25
+ /**
26
+ * Ceiling on the fetches one `get_page_content` walk may spend. Call admission
27
+ * meters tool calls, not the requests inside them, so a deep `depth` would
28
+ * otherwise drain the budget invisibly; the walk stops here and reports
29
+ * `truncated: true` instead.
30
+ */
19
31
  const MAX_CONTENT_REQUESTS = 20;
20
32
  /**
21
33
  * The largest response this connection will read.
@@ -26,7 +38,15 @@ const MAX_CONTENT_REQUESTS = 20;
26
38
  * a budget any real read has to think about.
27
39
  */
28
40
  const NOTION_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
29
- /** See documentation/notion.md#rate-limiting. */
41
+ /**
42
+ * Notion documents "an average of three requests per second, with some bursts
43
+ * beyond the average allowed" per connection. 180 calls per minute is that
44
+ * average over a window short bursts pass and a sustained loop does not, and
45
+ * `maxConcurrency: 3` is the load-bearing half — an averaged budget cannot stop
46
+ * forty calls in one tick, and declaring a queue is also what makes the queue
47
+ * settings legal at construction. Per runtime, and a floor on the real request
48
+ * rate rather than a ceiling, since one admitted call can spend many fetches.
49
+ */
30
50
  const NOTION_ADMISSION = {
31
51
  rules: [
32
52
  {
@@ -38,7 +58,18 @@ const NOTION_ADMISSION = {
38
58
  },
39
59
  ],
40
60
  };
41
- /** See documentation/notion.md#typed-failures. */
61
+ /**
62
+ * Map to what the caller should do next, not to what Notion's `code` says
63
+ * happened. The two that are easy to mistranslate:
64
+ *
65
+ * - **403 is not an authentication failure.** The token is fine; the integration
66
+ * lacks a capability or was never shared the object, and neither
67
+ * `authorize_connector` nor a new token can fix it. Non-retryable call failure.
68
+ * - **404 does not prove absence.** Notion returns `object_not_found` both for an
69
+ * object that does not exist and for one never shared with the integration, and
70
+ * will not say which — so not `not_found`, which exists to assert absence
71
+ * (H11). The message names both possibilities.
72
+ */
42
73
  function notionFailure(status, body, headers) {
43
74
  const code = typeof body?.["code"] === "string" ? body["code"] : undefined;
44
75
  const detail = typeof body?.["message"] === "string" && body["message"].trim()
@@ -82,7 +113,18 @@ function notionFailure(status, body, headers) {
82
113
  retryable: false,
83
114
  });
84
115
  }
85
- /** See documentation/connectors.md#the-guarded-fetch-transport. */
116
+ /**
117
+ * The shared transport owns the mechanical, provider-independent guards: path
118
+ * confinement re-checked after URL normalization, encoded query construction,
119
+ * credential headers applied last and unshadowable, a refused 3xx (a redirect is
120
+ * an instruction to re-send the credential to another origin), and a body
121
+ * abandoned at `maxResponseBytes`. It deliberately owns no meaning — it never
122
+ * reads a status code, because Notion's 403 is an ungranted capability while
123
+ * another provider's is a token scope, and those want opposite next moves. The
124
+ * mapper below supplies that. Note it must re-throw `ConnectorCallError`: a bare
125
+ * catch around a body read swallows the transport's own refusal and turns a
126
+ * response nobody was allowed to read into an empty success.
127
+ */
86
128
  const send = guardedFetch({
87
129
  provider: "Notion",
88
130
  baseUrl: NOTION_API_BASE_URL,
@@ -108,7 +150,16 @@ async function notionRequest(ctx, request) {
108
150
  return payload ?? {};
109
151
  });
110
152
  }
111
- // Projections: documentation/notion.md#lean-projections-and-the-raw-escape-hatch.
153
+ // Projections. Notion wraps every property in a discriminated object, every
154
+ // string in an array of annotated rich-text runs, and every user in a nested
155
+ // object, so a small query is tens of kilobytes of structure around a few
156
+ // hundred bytes of meaning. Every read collapses that to ids, plain text, and
157
+ // flattened values. Nothing is lost for good: the reads that can drop something
158
+ // take `raw: true` and return Notion's untouched response instead.
159
+ //
160
+ // Unknown types unwrap rather than switch exhaustively, because Notion ships
161
+ // additive changes to every pinned version at once — a property type newer than
162
+ // this release degrades to its raw value instead of vanishing.
112
163
  /** Concatenate a rich-text array to its plain text. Safe for every variant. */
113
164
  function plainText(value) {
114
165
  if (!Array.isArray(value))
@@ -292,7 +343,11 @@ function projectPage(page, select) {
292
343
  ...(truncated.length ? { truncated_properties: truncated } : {}),
293
344
  };
294
345
  }
295
- /** See documentation/notion.md#lean-projections-and-the-raw-escape-hatch. */
346
+ /**
347
+ * Identity fields only, no properties. A 25-result search across a populated
348
+ * database would otherwise drag back hundreds of flattened values for results
349
+ * the agent is about to discard; `get_page` fetches them for the one that hit.
350
+ */
296
351
  function projectSearchHit(hit) {
297
352
  if (hit?.object === "data_source") {
298
353
  return {
@@ -359,7 +414,9 @@ function projectBlock(block, depth) {
359
414
  projected["icon"] = iconRef(payload?.icon);
360
415
  break;
361
416
  default:
362
- // Rationale: documentation/notion.md#lean-projections-and-the-raw-escape-hatch.
417
+ // A block type this projection does not model keeps its payload verbatim
418
+ // rather than collapsing to an empty string, so a block type newer than
419
+ // this release still carries its content.
363
420
  if (carriesUnprojectedContent(payload))
364
421
  projected["raw"] = payload;
365
422
  break;
@@ -705,6 +762,9 @@ function buildTools(defaultPageSize) {
705
762
  },
706
763
  {
707
764
  name: "get_page_content",
765
+ // `raw: true` returns the requested level exactly as Notion sent it and
766
+ // does not walk nested children, so `depth` is ignored alongside it: a
767
+ // raw read of a deep page yields one level.
708
768
  description: "Read a page's body as a flat list of blocks reduced to plain text. Each block keeps its id, type, and depth so it can be quoted, appended after, or drilled into. Nested content requires depth > 0.",
709
769
  annotations: { readOnlyHint: true },
710
770
  inputSchema: {
@@ -1107,7 +1167,11 @@ function buildTools(defaultPageSize) {
1107
1167
  description: "Create a page, either as a child of another page or as a row in a data source. Notion has no idempotency key: a retried create makes a second page, so confirm with search before repeating one.",
1108
1168
  annotations: { readOnlyHint: false },
1109
1169
  inputSchema: {
1110
- // See documentation/notion.md#what-this-connection-does-not-do.
1170
+ // A deliberate subset of the 2026-03-11 create contract, reviewed after
1171
+ // the 0.17.0 drift check (#408). Workspace-private pages, templates,
1172
+ // page placement, and the expanded icon/cover forms stay out: they
1173
+ // change ownership, start asynchronous content work, control ordering,
1174
+ // or depend on file surfaces, none of which extend page/row authoring.
1111
1175
  required: [],
1112
1176
  type: "object",
1113
1177
  properties: {
@@ -1263,6 +1327,10 @@ function buildTools(defaultPageSize) {
1263
1327
  },
1264
1328
  {
1265
1329
  name: "update_page_properties",
1330
+ // Deliberately no locking, templates, or `erase_content` (#409): locking
1331
+ // is coordination state, templates finish asynchronously, and
1332
+ // `erase_content` permanently deletes every child block. None belongs
1333
+ // under an approval named for property replacement.
1266
1334
  description: "Overwrite property values on an existing page. Every named property is replaced, not merged, so send a multi_select or relation's complete intended value. Cannot move a page and cannot trash one.",
1267
1335
  // Replaces values that already exist: the host should say so out loud.
1268
1336
  annotations: { readOnlyHint: false, destructiveHint: true },
@@ -1396,7 +1464,12 @@ function buildTools(defaultPageSize) {
1396
1464
  // ---------------------------------------------------------------------------
1397
1465
  // Guide and constructor
1398
1466
  // ---------------------------------------------------------------------------
1399
- /** See documentation/notion.md#databases-contain-data-sources. */
1467
+ /**
1468
+ * Marked `required` for one reason: a Notion database is a container, and the
1469
+ * rows and schema live in a data source inside it. The id in a database URL is a
1470
+ * database id, and passing it to `query_data_source` or `create_page` fails —
1471
+ * a trap no input schema can teach, so the guide has to.
1472
+ */
1400
1473
  function apiUsageGuide(purpose, instructions) {
1401
1474
  const accountInstructions = instructions?.trim();
1402
1475
  return `# Notion usage
@@ -3,7 +3,12 @@ import type { Connector, ConnectorCallAdmissionPolicy } from "../types.js";
3
3
  /** RevenueCat publishes one hosted MCP endpoint, streamable HTTP. */
4
4
  export declare const REVENUECAT_MCP_ENDPOINT = "https://mcp.revenuecat.ai/mcp";
5
5
  export interface RevenueCatOptions {
6
- /** Display name; scope defaults are in `documentation/revenuecat.md`. */
6
+ /**
7
+ * Display name. It defaults to the credential's scope, because that is what
8
+ * decides how an agent must address the connector: `RevenueCat (single
9
+ * project)` for a project-wide `sk_` key, plain `RevenueCat` for an
10
+ * account-scoped OAuth session that must resolve a `project_id` first.
11
+ */
7
12
  title?: string;
8
13
  /** Downstream auth ownership. Defaults to one shared deployment grant. */
9
14
  authScope?: "shared" | "personal";
@@ -13,16 +18,37 @@ export interface RevenueCatOptions {
13
18
  * so it goes in the guide's first line and its summary.
14
19
  */
15
20
  purpose: string;
16
- /** OAuth or a single-project API v2 key; see `documentation/revenuecat.md`. */
21
+ /**
22
+ * OAuth (account-scoped, reaches every project the account can see) or an
23
+ * API v2 `sk_` secret key, which RevenueCat scopes to exactly one project —
24
+ * hence one key, one connector, and no `project` option: checking a declared
25
+ * project against `list-projects` at construction would be the unasked-for
26
+ * credential test P10 forbids. Nor can Connecta tell a read-only key from a
27
+ * write-enabled one without spending a call, so every write is offered and a
28
+ * read-only key's refusal arrives from RevenueCat.
29
+ */
17
30
  auth?: RemoteMcpAuth;
18
31
  /** Project-specific conventions appended to the maintained provider guide. */
19
32
  instructions?: string;
20
33
  /** Connector-specific inline result limit; omit to inherit the deployment. */
21
34
  maxResultBytes?: number;
22
- /** Optional per-runtime policy; see `documentation/revenuecat.md#rate-limits`. */
35
+ /**
36
+ * Optional per-runtime policy. There is no default even though RevenueCat
37
+ * publishes numbers (API v2, read 2026-08-18): the limit is per minute per
38
+ * domain — 480 for customer information, virtual currencies, and refunds; 60
39
+ * for audiences and project configuration; 25 for charts and metrics — and a
40
+ * policy carries one rule, so any single number is wrong for most tools. The
41
+ * metering scope is per developer for developer-level keys, which a
42
+ * per-runtime counter cannot approximate either. The operator picks (P12).
43
+ */
23
44
  callAdmission?: ConnectorCallAdmissionPolicy;
24
45
  }
25
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
46
+ /**
47
+ * One release-reviewed manifest, used both to classify a live tool and as the
48
+ * baseline the drift check compares against, so the annotation a caller gets
49
+ * and the verdict a check reads can never disagree. Names and verdicts only —
50
+ * no schemas are vendored; the live `tools/list` response stays authoritative.
51
+ */
26
52
  export declare const REVENUECAT_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
27
53
  /** A maintained RevenueCat hosted-MCP connection. */
28
54
  export declare function revenuecat(id: string, options: RevenueCatOptions): Connector;