@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,160 +0,0 @@
1
- # Mixpanel prebuilt connection
2
-
3
- Import `mixpanel()` independently from
4
- `@zackbart/connecta/providers/mixpanel`. It wraps Mixpanel's hosted MCP server
5
- with regional endpoint selection, OAuth by default, a task-oriented usage
6
- guide, and a vetted safety classification. It adds no provider dependency and
7
- is not reachable from Connecta's root entry.
8
-
9
- ```ts
10
- import { mixpanel } from "@zackbart/connecta/providers/mixpanel";
11
-
12
- const analytics = mixpanel("product_analytics", {
13
- title: "Production product analytics",
14
- purpose: "Product and growth decisions for the production app",
15
- region: "us",
16
- instructions: "Use the Core Product project unless the request says otherwise.",
17
- });
18
- ```
19
-
20
- The `id` owns the ordinary connector namespaces; use a different id for every
21
- Mixpanel account. `purpose` is required because an agent choosing between two
22
- instances needs to know which account answers the question. Account
23
- `instructions` are appended to the maintained guide and cannot change the
24
- connector's safety classification.
25
-
26
- `region` accepts `"us"` (the default), `"eu"`, or `"in"` and selects the
27
- corresponding [official hosted endpoint](https://docs.mixpanel.com/docs/mcp#mcp-server-urls).
28
- A project lives in exactly one residency, so the region also decides what this
29
- connection can see at all: a question pointed at the wrong one comes back empty
30
- rather than wrong, which reads as the project having no data. That makes it a
31
- routing fact, so it rides the default `title` (`Mixpanel (us)`, `Mixpanel
32
- (eu)`, `Mixpanel (in)`) and opens the usage guide — `search_tools` renders a
33
- connector's title and guide summary and never its description.
34
- OAuth is the recommended default and keeps each connector instance's flow and
35
- tokens in its connector-scoped storage. Mixpanel service accounts are also
36
- supported with an explicit header override:
37
-
38
- ```ts
39
- mixpanel("automation_analytics", {
40
- purpose: "Headless release-health reporting",
41
- auth: {
42
- type: "headers",
43
- headers: { Authorization: `Bearer Basic ${env.MIXPANEL_SA_TOKEN}` },
44
- },
45
- });
46
- ```
47
-
48
- Keep that encoded service-account value in the runtime's secret store; it is a
49
- password, not ordinary configuration. Mixpanel currently labels service-account
50
- MCP authentication beta. Prefer OAuth unless the deployment is intentionally
51
- headless.
52
-
53
- The same service account can arrive from the connection UI at `/` instead, and there the
54
- operator pastes the readable pair rather than an encoded blob:
55
-
56
- ```ts
57
- mixpanel("automation_analytics", {
58
- purpose: "Headless release-health reporting",
59
- auth: { type: "credential" },
60
- });
61
- ```
62
-
63
- The slot renders as "Service account" and takes `username:secret`. Connecta
64
- base64-encodes it and sends Mixpanel's documented `Bearer Basic` framing, so the
65
- operator never has to encode anything by hand. **The two paths take different
66
- strings:** the `headers` example above wants the already-encoded blob
67
- (`echo -n "username:secret" | base64`), and this one wants the plaintext pair.
68
- Migrating from one to the other means decoding, not copying. Until a value is
69
- saved the connector is present and reports `auth_required`. See
70
- [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
71
-
72
- ## Conditional input contracts
73
-
74
- Mixpanel's hosted descriptions enforce three cross-field conditions that its
75
- input schemas do not encode. Connecta preserves those schemas unchanged under
76
- [P1](./provider-conventions.md#p1--normalize-by-adding-never-by-rewriting), so
77
- the maintained guide carries the missing call guidance:
78
-
79
- - `Get-Business-Context` requires `project_id` or `organization_id`.
80
- - `Get-Property-Values` requires `properties` or the deprecated `property`
81
- alias. Event property values also require `event`.
82
- - `List-Properties` accepts `names` or `query`, never both.
83
-
84
- A read-only live audit on 2026-08-13 confirmed all three refusals against the
85
- US hosted endpoint. They are reported upstream as
86
- [`mixpanel/mixpanel-headless#202`](https://github.com/mixpanel/mixpanel-headless/issues/202).
87
- The vetted catalog records current schema digests for all 64 tools,
88
- so a later schema correction or regression increments runtime drift when an
89
- ordinary catalog refresh observes it. The live definition is still served
90
- unchanged. The credential-free provider check does not depend on those digests.
91
- The guide can shrink when the downstream schema becomes complete; Connecta does
92
- not absorb the defect permanently.
93
-
94
- The wrapper classifies the documented observational tools as reads and the
95
- documented create, update, edit, merge, dismiss, duplicate, and delete tools as
96
- writes. An unfamiliar tool the downstream leaves unannotated fails closed onto
97
- `call_destructive_tool` until a Connecta release reviews it.
98
-
99
- That classification fills in downstream silence and otherwise preserves
100
- explicit annotations. A tool on the read allowlist arriving with
101
- `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
102
- downstream said and stays behind `call_destructive_tool`. A tool on neither
103
- maintained list arriving with `readOnlyHint: true` keeps that too, and stays
104
- callable from `execute_code`. Both are the downstream telling you this
105
- release's allowlist is stale, and on a name no release has reviewed its word is
106
- the only evidence there is. One narrow fail-closed exception applies to a name
107
- this release reviewed and filed destructive: a `Delete-Dashboard` claiming
108
- `readOnlyHint: true` is a downstream bug rather than news, and stays on the
109
- approval path. Maintained writes that only create
110
- something new (`Create-Dashboard`, `Create-Cohort`, `Create-Metric`, and the
111
- rest) leave `destructiveHint` unset; `readOnlyHint: false` already routes them
112
- through the destructive path, and asserting destruction only inflates the
113
- approval copy the host shows a human.
114
-
115
- Experiments and Feature Flags are Mixpanel beta surfaces. Their three changed
116
- schemas were reviewed again on 2026-08-30. The same review added
117
- `Fill-Event-Metadata` as a destructive write because it applies generated names
118
- and descriptions to existing Lexicon events.
119
-
120
- ## Rate limits
121
-
122
- Mixpanel meters its MCP server **per user per hour**, shared with everything
123
- else that credential does. Connecta's counter is per runtime, not per user, and
124
- the two cannot be reconciled in either direction: one runtime serving several
125
- users under-counts, and several Worker isolates or Node processes sharing one
126
- credential each admit a full budget. A hardcoded ceiling would therefore either
127
- throttle a healthy deployment or fail to protect a busy one, so this connection
128
- declares **no call-admission budget by default**. An operator who knows the
129
- account can supply one explicitly:
130
-
131
- ```ts
132
- mixpanel("product_analytics", {
133
- purpose: "Product and growth decisions for the production app",
134
- callAdmission: {
135
- rules: [
136
- { budget: { kind: "rolling-window", maxCalls: 300, windowMs: 3_600_000 } },
137
- ],
138
- },
139
- });
140
- ```
141
-
142
- A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
143
- for a queue, and the admission controller then requires the rest of the queue
144
- settings at construction. Discovery traffic is outside connector call admission
145
- either way and still needs restrained use.
146
-
147
- ## Public contract check
148
-
149
- `npm run drift:check -- --docs --provider mixpanel` compares Mixpanel's
150
- official Available Tools table with the vetted manifest and checks all three
151
- regional endpoints plus OAuth support. The current table lists 63 tools. It
152
- omits `Fill-Event-Metadata`, which remains classified from the last
153
- authenticated review and is reported as `not documented`, not silently removed.
154
-
155
- ## Conventions
156
-
157
- This connection is audited against
158
- [the provider conventions](./provider-conventions.md). Its verdict per
159
- convention, including every recorded exception, is the Mixpanel section of
160
- [the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
@@ -1,308 +0,0 @@
1
- # Notion prebuilt connection
2
-
3
- Import `notion()` independently from `@zackbart/connecta/providers/notion`. It
4
- offers two deployment-time interfaces. The default is a hand-written `api()`
5
- surface over Notion's public REST API: fifteen deliberate tools, lean
6
- projections of Notion's famously bloated payloads, typed failures, a rate
7
- budget matched to the documented limit, and a required usage guide. The other
8
- choice is Notion's official hosted MCP with live provider-owned schemas and a
9
- broader workspace, files, views, agents, and sessions catalog. Neither is
10
- reachable from Connecta's root entry.
11
-
12
- ```ts
13
- import { notion } from "@zackbart/connecta/providers/notion";
14
-
15
- const wiki = notion("engineering_wiki", {
16
- surface: "api", // optional; this is the backward-compatible default
17
- title: "Engineering wiki",
18
- purpose: "Runbooks, specs, and on-call notes for the platform team",
19
- instructions: "Prefer the Runbooks database; specs live under Projects.",
20
- });
21
- ```
22
-
23
- Use Notion's hosted MCP instead:
24
-
25
- ```ts
26
- const workspace = notion("notion_mcp", {
27
- surface: "mcp",
28
- purpose: "Workspace search, files, views, and asynchronous agent sessions",
29
- });
30
- ```
31
-
32
- The `id` owns the ordinary connector namespaces; use a different id for every
33
- Notion workspace. `purpose` is required because an agent choosing between two
34
- instances needs to know which workspace answers the question. Workspace
35
- `instructions` are appended to the maintained guide and cannot change the
36
- connector's safety classification.
37
-
38
- ## Choosing an interface
39
-
40
- Use the API interface for its compact, stable projections. A single Notion
41
- page returns every property as a
42
- discriminated wrapper object, every string as an array of rich-text runs each
43
- carrying its own annotations block, and every user reference as a nested
44
- object. A twenty-five row database query is tens of kilobytes of structure
45
- around a few hundred bytes of meaning. Hand-writing the surface is what makes
46
- the projections possible, and the projections are the point.
47
-
48
- Use `surface: "mcp"` for Notion's wider official capabilities, including
49
- connected-source search, attachments, saved views, Notion Skills, agents, and
50
- asynchronous sessions. Tool names and schemas come from the live server.
51
- Connecta preserves them and only fills in release-reviewed safety annotations
52
- when Notion is silent. OAuth is the hosted server's authentication contract.
53
- Do not apply the hand-written REST schemas to similarly named MCP tools. The
54
- MCP interface accepts `callAdmission` for an operator-supplied runtime policy;
55
- it does not assume the REST interface's endpoint budget describes MCP traffic.
56
-
57
- ## API authentication
58
-
59
- One operator-managed credential: an internal integration token from
60
- [notion.so/profile/integrations](https://www.notion.so/profile/integrations).
61
- The deployment needs `vault: encryptedCredentialVault(storage, encryptionKey)` configured, or the token
62
- cannot be stored and every call fails `auth_required` at use.
63
-
64
- Two Notion-specific facts decide whether a working token is enough:
65
-
66
- - **Sharing is per object.** A token reaches only what has been explicitly
67
- shared with its integration. An unshared page returns 404, not 403.
68
- - **Capabilities are per integration**, and the comment capabilities are off by
69
- default. `list_comments` and `add_comment` fail with 403 until an operator
70
- turns them on in Notion.
71
-
72
- The connection UI at `/` offers a Test action, which calls `GET /v1/users/me` — the
73
- cheapest call that proves a token is live — and reports the workspace it
74
- authenticated into.
75
-
76
- The MCP interface uses Notion OAuth instead of the integration-token form. An
77
- `auth_required` failure means the grant is absent or expired and must be
78
- completed again through `authorize_connector`.
79
-
80
- ## The pinned API version
81
-
82
- The connection pins `Notion-Version: 2026-03-11` and offers no override. That
83
- is deliberate rather than lazy. Notion's versions are date-named and old ones
84
- keep working indefinitely, so an override would look harmless; it is not.
85
- `2026-03-11` is the version in which databases split into data sources,
86
- `archived` became `in_trash`, and block append took a `position` object instead
87
- of an `after` string. Every projection and write body here assumes those
88
- shapes, so a deployment that pinned an older version would get quietly wrong
89
- results instead of a loud failure.
90
-
91
- Notion also ships *additive* changes to every version simultaneously, which is
92
- why the property and block projections fall back to unwrapping an unknown
93
- type's payload rather than switching exhaustively. A property type that ships
94
- after this release degrades to its raw value, and a block type that does keeps
95
- its payload under `raw`; neither vanishes.
96
-
97
- The remaining sections document the hand-written API interface. MCP tool
98
- arguments and results are intentionally read from the live server instead.
99
-
100
- ## API tools
101
-
102
- Ten reads, all annotated `readOnlyHint: true`:
103
-
104
- | Tool | What it is for |
105
- | --- | --- |
106
- | `search` | Find pages and data sources by **title**. Never searches content. |
107
- | `get_page` | One page's metadata and flattened property values. |
108
- | `get_page_content` | A page's body as flat blocks reduced to plain text. |
109
- | `get_page_property` | One property in full, past the 25-entry truncation. |
110
- | `get_database` | A database container and the data sources inside it. |
111
- | `get_data_source_schema` | Property ids, types, and select/status options. |
112
- | `query_data_source` | Filtered, sorted rows with properties already flattened. |
113
- | `list_users` | Workspace users and bots with their ids. |
114
- | `get_self` | Which integration and workspace this connector authenticates as. |
115
- | `list_comments` | Unresolved comments as plain text with discussion ids. |
116
-
117
- Five writes, none read-only, so all of them route through
118
- `call_destructive_tool`:
119
-
120
- | Tool | Classification |
121
- | --- | --- |
122
- | `create_page` | additive |
123
- | `append_blocks` | additive |
124
- | `add_comment` | additive |
125
- | `update_page_properties` | `destructiveHint: true` — replaces existing values |
126
- | `trash_page` | `destructiveHint: true` — removes a page from reads |
127
-
128
- The additive three leave `destructiveHint` unset: `readOnlyHint: false` already
129
- routes them through the approval path, and claiming a create destroys something
130
- only inflates the copy a host shows a human. `update_page_properties`
131
- deliberately has no `in_trash` argument, so an update can never trash a page by
132
- accident; trashing is its own named, reversible tool.
133
-
134
- ## Lean projections, and the raw escape hatch
135
-
136
- Every read projects. A page becomes ids, plain text, and flattened values:
137
- `title` and `rich_text` collapse to strings, `select` and `status` to their
138
- option name, `multi_select` to an array of names, `relation` to an array of
139
- page ids, `people` to `{ id, name }`, `unique_id` to `"RL-12"`, and
140
- `formula`/`rollup` to their computed value. No `plain_text` runs, no
141
- `annotations` blocks, no property wrappers survive.
142
-
143
- Two projections are opinionated enough to call out:
144
-
145
- - **`search` returns identity fields only** — no properties at all. A
146
- twenty-five result search across a populated database would otherwise drag
147
- back several hundred flattened values for results the agent is about to
148
- discard. `get_page` fetches properties for the one that matched.
149
- - **`get_page` reports what Notion hid.** Notion paginates four property types
150
- — `title`, `rich_text`, `relation`, and `people` — cutting each off at 25
151
- entries and signalling it only with a `has_more` flag on the property itself.
152
- The projection surfaces those in `truncated_properties` as `{ name, id }`,
153
- which is what stops an agent from confidently reasoning about 25 of 300
154
- relations. The `id` is there because the handoff needs it: `get_page_property`
155
- addresses a property by id, not by name.
156
-
157
- Where the dropped detail can matter — `search`, `get_page`, `get_page_content`,
158
- `get_page_property`, `get_data_source_schema`, `query_data_source`,
159
- `list_comments` — `raw: true` returns Notion's untouched response instead. It is
160
- much larger; it exists so a missing field is never a dead end. When the goal is
161
- *fewer* fields rather than more, `get_page` and `query_data_source` also take a
162
- `properties` array to project only the named ones.
163
-
164
- One caveat with `get_page_content`: `raw: true` returns the requested level
165
- exactly as Notion sent it and does not walk nested children, so `depth` is
166
- ignored alongside it. A raw read of a deep page yields one level, not three.
167
- Unmodelled *block* types are covered without it — a block whose type this
168
- projection does not know, and whose payload is not plain rich text, keeps that
169
- payload verbatim under `raw` on the block, so nothing collapses to an empty
170
- string.
171
-
172
- ## Databases contain data sources
173
-
174
- This is the trap the guide is marked `required` for. A Notion database is a
175
- container; the rows and the schema live in a *data source* inside it, and the
176
- two ids are not interchangeable. The id in a database's URL is a **database
177
- id**, and passing it to `query_data_source` fails. The sequence is
178
- `get_database` → `get_data_source_schema` → `query_data_source`; `search`
179
- returns data sources directly and skips the first step.
180
-
181
- `create_page` needs the same distinction: a row is created under
182
- `parent_data_source_id`, never a database id. Its title also needs
183
- `title_property` from the schema, because a database's title column is rarely
184
- called "title".
185
-
186
- ## Typed failures
187
-
188
- The mapping is deliberately not one-to-one with Notion's error codes. Notion's
189
- `code` says what its API thinks happened; Connecta's says what the caller
190
- should do next, and two of Notion's are easy to mistranslate.
191
-
192
- | Notion | Connecta | Why |
193
- | --- | --- | --- |
194
- | 400 (`validation_error`, `invalid_json`, `invalid_request`, `missing_version`, …) | `invalid_args` | every documented 400 is a malformed request |
195
- | 401 `unauthorized` | `auth_required` | the token is missing or invalid |
196
- | 403 `restricted_resource` | `connector_call_failed`, non-retryable | **not** `auth_required` |
197
- | 404 `object_not_found` | `connector_call_failed`, non-retryable | overloaded — deliberately **not** `not_found`; see below |
198
- | 409 `conflict_error` | `unavailable`, retryable | Notion says to retry |
199
- | 429 `rate_limited` | `rate_limited` + `retryAfterMs` | `Retry-After` seconds → ms |
200
- | 529 `service_overload` | `unavailable` + `retryAfterMs` | back off like a 429 |
201
- | 5xx | `unavailable`, retryable | upstream failure |
202
-
203
- The two that matter:
204
-
205
- **403 is not an authentication failure.** The token is fine; the integration
206
- lacks a capability or was never shared the object. Routing it to
207
- `auth_required` would send an agent to `authorize_connector`, which cannot
208
- grant a Notion capability or share a page. It is a non-retryable call failure
209
- whose message says an operator must change it in Notion.
210
-
211
- **404 does not prove absence.** Notion returns `object_not_found` both for an
212
- object that does not exist and for one that exists but has not been shared with
213
- the integration, and it will not say which. The message says both, because
214
- treating it as deletion is exactly how an agent concludes a page is gone when
215
- it was simply never shared. This is why the row above does not use `not_found`,
216
- which exists precisely to say "it is not there": the qualifier on that code
217
- ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next))
218
- is that the provider must tell absence apart from a permission gap, and Notion
219
- does not. A program that skipped this id as missing would be right about half
220
- the time, which is the half that matters.
221
-
222
- ## Rate limiting
223
-
224
- Notion documents "an average of three requests per second, with some bursts
225
- beyond the average allowed" per connection, plus a separate per-workspace limit
226
- scaled to the plan. The connection declares a rolling budget of 180 calls per
227
- minute — the same average expressed over a window short bursts pass and a
228
- sustained loop does not — **paired with `maxConcurrency: 3`**.
229
-
230
- The concurrency cap is the load-bearing half. A budget alone is an average, and
231
- an averaged budget cannot stop a program from firing forty calls in the same
232
- tick; the cap keeps a burst shaped roughly like the one Notion documents.
233
- Neither half is a guarantee, because admission meters *tool calls* rather than
234
- requests — a single admitted `get_page_content` can spend twenty fetches, so
235
- 180 calls per minute is a floor on the real request rate, not a ceiling.
236
- Declaring the cap is also what makes the queue settings legal — the admission
237
- controller refuses queue settings without a queue at construction.
238
-
239
- Like every connector budget this is per-runtime: N Worker isolates or Node
240
- processes serving one deployment each keep their own counter. It approximates
241
- the provider's limit; it does not enforce it.
242
-
243
- One tool can turn a single agent call into several downstream requests:
244
- `get_page_content` with `depth > 0` walks nested blocks, and call admission
245
- meters tool calls, not the fetches inside them. That walk stops at an internal
246
- ceiling of twenty requests and reports `truncated: true` rather than spending
247
- the whole budget invisibly.
248
-
249
- ## Pagination
250
-
251
- List-shaped tools take `page_size` (1–100) and `start_cursor`, and return
252
- `has_more` with `next_cursor`. The default page size is 25 rather than Notion's
253
- 100, because a first read should be cheap; `defaultPageSize` raises it for a
254
- deployment that pages a lot.
255
-
256
- Cursors are opaque. Notion's own versioning page is explicit that they may
257
- change in length, format, and structure at any time and must be passed back
258
- verbatim — never parsed, validated, or constructed.
259
-
260
- ## What the API interface does not do
261
-
262
- No file uploads, no database or data-source creation, no schema editing, no
263
- block updates or deletes, no page moves. Those are all real Notion endpoints
264
- and all deliberately absent: this is a deliberate tool surface, not a mirror of
265
- the API. Some are present on Notion's hosted MCP interface. Anything still
266
- missing is reachable through a custom `api()` connector beside this one, which
267
- remains a first-class path.
268
-
269
- The 2026-03-11 contract also offers more fields on create and update. They were
270
- reviewed after the 0.17.0 drift check and remain deliberately absent:
271
-
272
- - `create_page` does not create workspace-private pages, apply templates,
273
- choose page placement, or accept expanded icon and cover forms. Those change
274
- ownership, start asynchronous content work, control ordering, or depend on
275
- file surfaces. They are not extensions of the maintained page/row authoring
276
- contract (#408).
277
- - `update_page_properties` does not lock pages, apply templates, or erase page
278
- content. Locking is coordination state, templates finish asynchronously, and
279
- `erase_content` permanently deletes every child block through the API. None
280
- belongs under an approval named for property replacement (#409).
281
-
282
- `trash_page` stays separate and reversible. The current `create_page`,
283
- `update_page_properties`, and `trash_page` request subsets remain valid against
284
- the expanded published contract.
285
-
286
- There is also **no guarded raw-REST escape hatch** — no `notion_api_get`, no
287
- `notion_api_mutate`. The convention that permits one
288
- ([H14](./provider-conventions.md#h14--a-named-tool-must-beat-the-escape-hatch-and-the-escape-hatch-splits-by-safety))
289
- also permits a small provider to have none, provided it says so, and Notion's
290
- public API is finite and slow-moving enough that a named surface can cover it.
291
- The usage guide says it too, because an agent that assumes a hatch exists
292
- spends a search proving it does not: absent from the tool list means absent
293
- from this connection, not hidden behind a generic call.
294
-
295
- ## Contract checks
296
-
297
- `npm run providers:check` compares the 14 fixed REST endpoints with Notion's
298
- published OpenAPI document and the 34 MCP names, endpoint, and OAuth support
299
- with Notion's official pages. It needs no Notion credential. The MCP schemas
300
- are not vendored or reconstructed: the live `tools/list` response remains the
301
- contract agents receive.
302
-
303
- ## Conventions
304
-
305
- This connection is audited against
306
- [the provider conventions](./provider-conventions.md). Its verdict per
307
- convention, including every recorded exception, is the Notion section of
308
- [the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).