@zackbart/connecta 0.12.2 → 0.14.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 (75) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/README.md +4 -1
  3. package/dist/catalog-service.d.ts +41 -0
  4. package/dist/catalog-service.d.ts.map +1 -1
  5. package/dist/catalog-service.js +127 -11
  6. package/dist/catalog-service.js.map +1 -1
  7. package/dist/connectors/api.d.ts +5 -4
  8. package/dist/connectors/api.d.ts.map +1 -1
  9. package/dist/connectors/api.js +5 -1
  10. package/dist/connectors/api.js.map +1 -1
  11. package/dist/connectors/remote-mcp.d.ts +5 -4
  12. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  13. package/dist/connectors/remote-mcp.js.map +1 -1
  14. package/dist/execute.d.ts.map +1 -1
  15. package/dist/execute.js +12 -10
  16. package/dist/execute.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/meta-tools.d.ts.map +1 -1
  21. package/dist/meta-tools.js +5 -4
  22. package/dist/meta-tools.js.map +1 -1
  23. package/dist/providers/cloudflare.d.ts +53 -0
  24. package/dist/providers/cloudflare.d.ts.map +1 -0
  25. package/dist/providers/cloudflare.js +1385 -0
  26. package/dist/providers/cloudflare.js.map +1 -0
  27. package/dist/providers/linear.d.ts +44 -0
  28. package/dist/providers/linear.d.ts.map +1 -0
  29. package/dist/providers/linear.js +243 -0
  30. package/dist/providers/linear.js.map +1 -0
  31. package/dist/providers/mixpanel.d.ts +21 -0
  32. package/dist/providers/mixpanel.d.ts.map +1 -0
  33. package/dist/providers/mixpanel.js +191 -0
  34. package/dist/providers/mixpanel.js.map +1 -0
  35. package/dist/providers/notion.d.ts +39 -0
  36. package/dist/providers/notion.d.ts.map +1 -0
  37. package/dist/providers/notion.js +1625 -0
  38. package/dist/providers/notion.js.map +1 -0
  39. package/dist/providers/stripe.d.ts +37 -0
  40. package/dist/providers/stripe.d.ts.map +1 -0
  41. package/dist/providers/stripe.js +232 -0
  42. package/dist/providers/stripe.js.map +1 -0
  43. package/dist/skills.d.ts +7 -9
  44. package/dist/skills.d.ts.map +1 -1
  45. package/dist/skills.js +58 -24
  46. package/dist/skills.js.map +1 -1
  47. package/dist/types.d.ts +26 -6
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/documentation/cloudflare.md +268 -0
  52. package/documentation/code-mode.md +6 -6
  53. package/documentation/connectors.md +122 -4
  54. package/documentation/linear.md +144 -0
  55. package/documentation/meta-tools.md +91 -8
  56. package/documentation/mixpanel.md +77 -0
  57. package/documentation/notion.md +233 -0
  58. package/documentation/stripe.md +202 -0
  59. package/ethos.md +8 -3
  60. package/package.json +21 -1
  61. package/src/catalog-service.ts +174 -10
  62. package/src/connectors/api.ts +10 -4
  63. package/src/connectors/remote-mcp.ts +5 -3
  64. package/src/execute.ts +18 -10
  65. package/src/index.ts +1 -0
  66. package/src/meta-tools.ts +10 -4
  67. package/src/providers/cloudflare.ts +1696 -0
  68. package/src/providers/linear.ts +301 -0
  69. package/src/providers/mixpanel.ts +228 -0
  70. package/src/providers/notion.ts +1879 -0
  71. package/src/providers/stripe.ts +306 -0
  72. package/src/skills.ts +64 -23
  73. package/src/types.ts +27 -6
  74. package/src/version.ts +1 -1
  75. package/templates/node/package.json +1 -1
@@ -0,0 +1,144 @@
1
+ # Linear prebuilt connection
2
+
3
+ Import `linear()` independently from `@zackbart/connecta/providers/linear`. It
4
+ wraps Linear's hosted MCP server with endpoint selection, OAuth by default, a
5
+ task-oriented usage guide, and a vetted safety classification. It adds no
6
+ provider dependency and is not reachable from Connecta's root entry.
7
+
8
+ ```ts
9
+ import { linear } from "@zackbart/connecta/providers/linear";
10
+
11
+ const tracker = linear("product_tracker", {
12
+ title: "Product issue tracking",
13
+ purpose: "Issue and project planning for the platform team",
14
+ instructions: "File bugs into the Platform team unless the request names another.",
15
+ });
16
+ ```
17
+
18
+ The `id` owns the ordinary connector namespaces; use a different id for every
19
+ Linear workspace or access mode. `purpose` is required because an agent
20
+ choosing between two instances needs to know which workspace answers the
21
+ question. Workspace `instructions` are appended to the maintained guide and
22
+ cannot change the connector's safety classification.
23
+
24
+ ## Access modes
25
+
26
+ Linear publishes two hosted endpoints, and `access` selects between them:
27
+
28
+ | `access` | Endpoint | OAuth scopes |
29
+ | --- | --- | --- |
30
+ | `"read-write"` (default) | `https://mcp.linear.app/mcp` | `read`, `write` |
31
+ | `"read-only"` | `https://mcp.linear.app/mcp/readonly` | `read` |
32
+
33
+ Read-only is not a client-side filter. The endpoint advertises the `read` scope
34
+ alone, so the token minted for it cannot reach Linear's write APIs — a stronger
35
+ guarantee than any annotation Connecta applies. A deployment that only reports
36
+ on delivery should use it, and can run it beside a read-write instance under a
37
+ different id:
38
+
39
+ ```ts
40
+ linear("delivery_reporting", {
41
+ purpose: "Executive delivery reporting",
42
+ access: "read-only",
43
+ });
44
+ ```
45
+
46
+ The mode is legible at browse time, not only after the guide is fetched. A
47
+ read-only connection titles itself `Linear (read-only)` unless the operator
48
+ gives a `title`, and its guide opens with the access note rather than the
49
+ workspace purpose — `search_tools` renders a connector's title and guide
50
+ summary but never its description, and the summary is the guide's first content
51
+ line.
52
+
53
+ Linear's deprecated `/sse` transport is deliberately unreachable from this
54
+ connection; it now answers 404.
55
+
56
+ ## Authentication
57
+
58
+ OAuth 2.1 with dynamic client registration is the default and keeps each
59
+ connector instance's flow and tokens in its connector-scoped storage. Linear
60
+ also accepts a bearer token or a personal API key passed directly in the
61
+ `Authorization` header, which suits a headless deployment:
62
+
63
+ ```ts
64
+ linear("automation_tracker", {
65
+ purpose: "Headless release reporting",
66
+ auth: {
67
+ type: "headers",
68
+ headers: { Authorization: env.LINEAR_API_KEY },
69
+ },
70
+ });
71
+ ```
72
+
73
+ Keep that key in the runtime's secret store; it is a password, not ordinary
74
+ configuration. A personal API key carries the acting user's full workspace
75
+ permissions, so pair it with `access: "read-only"` unless the deployment
76
+ genuinely writes.
77
+
78
+ ## Safety classification
79
+
80
+ The wrapper classifies Linear's documented `list_*`, `get_*`, and
81
+ `search_documentation` tools as reads, and its `save_*`, `create_*`, `delete_*`,
82
+ `resolve_*`, `submit_*`, and `merge_*` tools as writes. An unfamiliar tool the
83
+ downstream leaves unannotated fails closed onto `call_destructive_tool` until a
84
+ Connecta release reviews it.
85
+
86
+ That classification is **fill-in only**, and unconditionally so: it supplies
87
+ the annotations Linear leaves unset and contradicts an explicit downstream
88
+ annotation in neither direction. A tool on the read allowlist arriving with
89
+ `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
90
+ downstream said and stays behind `call_destructive_tool`. A tool on neither
91
+ maintained list arriving with `readOnlyHint: true` keeps that too, and stays
92
+ callable from `execute_code`. Both are the downstream telling you this
93
+ release's allowlist is stale, and on a name no release has reviewed its word is
94
+ the only evidence there is. The one classification that still outranks the
95
+ downstream is a name this release reviewed and filed destructive: a `save_*`
96
+ tool claiming `readOnlyHint: true` is a downstream bug rather than news, and
97
+ stays on the approval path.
98
+
99
+ One detail of Linear's own design shapes the classification: **`save_*` tools
100
+ are upserts.** Omitting a record id creates; supplying one updates in place.
101
+ Because an upsert can overwrite, every `save_*` is classified destructive even
102
+ though some calls only create. The genuine creates are `create_issue_label` and
103
+ `create_initiative_label`, plus the attachment upload tools, which assert
104
+ `readOnlyHint: false` without claiming a destruction they do not perform.
105
+
106
+ ## The catalog is not a fixed set
107
+
108
+ Linear's hosted `tools/list` varies by workspace plan and enabled features:
109
+ customer requests, releases, and code review do not appear in every workspace.
110
+ The maintained allowlists are therefore a superset — a classified name a
111
+ workspace never returns costs nothing, and a genuinely new tool fails closed.
112
+ Agents should search this connector's catalog for what the workspace actually
113
+ exposes rather than assuming a tool exists; the usage guide says so explicitly.
114
+
115
+ ## Rate limits
116
+
117
+ Linear documents no MCP-specific rate limit. The MCP server rides the
118
+ [GraphQL API limits](https://linear.app/developers/rate-limiting), which are
119
+ metered **per user per hour** and shared with everything else that credential
120
+ does. Linear's own page is internally inconsistent on the API-key request
121
+ figure — the prose says 5,000 requests per hour while the table below it says
122
+ 2,500 for an API key and 5,000 for an OAuth app, against 600 unauthenticated —
123
+ and limits are raised dynamically for workspace-level OAuth apps using Actor
124
+ Authorization.
125
+
126
+ For that reason this connection declares **no call-admission budget by
127
+ default**. Connecta's counter is per runtime, not per user, so a hardcoded
128
+ ceiling would either throttle a healthy deployment or fail to protect a busy
129
+ one. An operator who knows their workspace can supply one explicitly:
130
+
131
+ ```ts
132
+ linear("product_tracker", {
133
+ purpose: "Issue and project planning for the platform team",
134
+ callAdmission: {
135
+ rules: [
136
+ { budget: { kind: "rolling-window", maxCalls: 1_000, 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.
@@ -19,6 +19,21 @@ The consolidation removed overlapping routing choices while preserving the
19
19
  cheaper direct path for one cold call. The [guest API contract](./code-mode.md)
20
20
  is what a program is promised.
21
21
 
22
+ The route is chosen before discovery. A result that will be reduced, a call
23
+ whose arguments depend on an earlier result, or work with multiple operations
24
+ starts with one `execute_code` call and keeps discovery, calls, and reduction
25
+ inside it. Distinct operations get distinct short `connecta.search` queries in
26
+ that program. Only one unknown-address read takes the cheaper top-level
27
+ `search_tools` → `call_tool` path; a known address needs only `call_tool`.
28
+
29
+ That routing is about read-only work, because that is the only work a program
30
+ can do. Anything unannotated, write-capable, or destructive is inadmissible
31
+ inside the sandbox, so multi-step destructive work discovers at the top level
32
+ and runs each step through `call_destructive_tool` — where the host can put the
33
+ question to a human. Telling an agent never to search at the top level for
34
+ multiple calls would close the only route that work has
35
+ ([#295](https://github.com/zackbart/connecta/issues/295)).
36
+
22
37
  `execute_code` accepts optional `diagnostics: true` when a caller is measuring
23
38
  a workflow. It adds only compact request-local timing and serialized-size
24
39
  aggregates; normal calls carry no diagnostics block or response-context cost.
@@ -35,17 +50,23 @@ probing is an operator concern: the operator pages and `/health` own it.
35
50
 
36
51
  Start an unknown-address lookup with two to four distinctive action/object
37
52
  terms, not the full request, and omit `limit` so the default eight-result page
38
- stays small. Set `safety: "readOnly"` when the result is headed to `call_tool`
39
- or generated code; `safety: "approvalRequired"` finds the complementary set
40
- that must cross `call_destructive_tool`. Omitting `safety`, or setting it to
41
- `"all"`, preserves the complete configured catalog. This is only a discovery
42
- filter: it neither grants authority nor changes invocation admission.
53
+ stays small. When the integration is obvious, set `connector` to its id: a
54
+ scoped search loads that catalog alone, while an unscoped search must fan out
55
+ across every configured connector. Leave the search unscoped when the right
56
+ integration is genuinely ambiguous. Set `safety: "readOnly"` when the result is
57
+ headed to `call_tool` or generated code; `safety: "approvalRequired"` finds the
58
+ complementary set that must cross `call_destructive_tool`. Omitting `safety`,
59
+ or setting it to `"all"`, preserves the complete configured catalog. This is
60
+ only a discovery filter: it neither grants authority nor changes invocation admission.
43
61
  `includeSchemas: "compact"` adds each match's input and any declared output
44
62
  shape. Bounded plain-object schemas also expose `inputKeys`,
45
63
  `requiredInputKeys`, and `outputKeys`; a truncated shape omits its corresponding
46
64
  list rather than repeating a large partial inventory. Matches carry declared
47
- behavior annotations. When
48
- that shape is sufficient, call the returned address directly. Reserve schema
65
+ behavior annotations. Lexical rank is only one signal: select a candidate whose
66
+ required inputs are available, whose schema is complete enough for the call,
67
+ and whose safety and declared outputs fit the work. A reducer uses `outputKeys`
68
+ before inspecting the value; it does not assume a collection is named `items`
69
+ or `results`. When that shape is sufficient, call the returned address directly. Reserve schema
49
70
  expansion through `connecta.describe` for a search without schemas, an
50
71
  ambiguous compact shape, or exact
51
72
  constraints that require `format: "json"`.
@@ -60,6 +81,51 @@ types; other shapes become `unknown /* truncated */`. The match also carries
60
81
  `includeSchemas: "json"` or use the existing describe path when exact
61
82
  constraints matter.
62
83
 
84
+ ## Connector guide selection
85
+
86
+ A connector may attach a deployment-owned guide as markdown, preserving the
87
+ original `usageGuide: string` configuration, or as
88
+ `{ content, summary?, required? }`. The structured form does not register a
89
+ connector or create a shared runtime template. `content` remains the markdown
90
+ returned verbatim by `skills`; `summary` is normalized and capped at 120
91
+ characters for discovery. When it is absent, Connecta derives the same bounded
92
+ fallback used by the skills listing: the first meaningful body line, with a
93
+ heading used only when the guide has no body. `required: true` is reserved for generic
94
+ API wrappers and cross-operation conventions a complete downstream schema
95
+ cannot express.
96
+
97
+ Search and describe results keep the existing `guide: "connector:<id>"`
98
+ pointer and add `guideSummary`. A matching tool also carries
99
+ `guideRequired: true` and `guideRequiredReasons` when Connecta can prove review
100
+ is necessary:
101
+ `connector_required` for the explicit configuration above,
102
+ `approval_required` for an unannotated or write-capable tool, and
103
+ `schema_truncated` when a requested compact input or output shape was capped.
104
+ The boolean is an instruction, not a server-side gate — nothing refuses the
105
+ call, so the agent is told to fetch the guide before making it, for any reason
106
+ listed. `connector_required` and `approval_required` survive exact schema
107
+ expansion; `schema_truncated` is cleared by the describe that returns the exact
108
+ shape, and describe reports whatever reasons remain in the same two fields.
109
+ Otherwise it reads the
110
+ bounded summary: connector-specific sequencing, units, pagination, aliases,
111
+ and generic API conventions still require the guide when they affect the task,
112
+ while a complete and unambiguous one-read schema proceeds directly.
113
+ Guide lookup always uses an exact name returned by `skills({})`, search, or
114
+ describe; callers do not manufacture `connector:<id>` from an unmarked
115
+ connector.
116
+
117
+ A connector-scoped lexical miss retains that connector's guide metadata under
118
+ `queryAnalysis`. This matters for generic wrappers whose broad tool name does
119
+ not contain endpoint vocabulary: a required guide remains discoverable before
120
+ the caller falls back to an empty-query browse, rather than disappearing with
121
+ the zero-tool page.
122
+
123
+ The built-in `usage` skill is byte-identical across deployments and says to
124
+ read it at most once per task. Connector guides remain scoped to the deployment
125
+ that listed them, even when two deployments happen to use identical content.
126
+ Deployments without connector guides receive none of the conditional guide
127
+ sentences in their always-loaded tool descriptions.
128
+
63
129
  ## Result representation
64
130
 
65
131
  For object results, `structuredContent` is the canonical full-fidelity value.
@@ -122,7 +188,24 @@ results explain that no single tool covered every term and recommend splitting
122
188
  distinct intents. A true negative says that no matching capability is
123
189
  configured and recommends refining, connector-scoping, or browsing; when a
124
190
  connector catalog was unavailable, the response includes
125
- `unavailableConnectorCount` instead of making that stronger claim. Analysis
191
+ `unavailableConnectorCount` instead of making that stronger claim. A search
192
+ explicitly scoped to that unavailable connector also receives `catalogError` —
193
+ the bounded classified failure (`code`, `message`, `retryable`, and any
194
+ `retryAfterMs`) so the caller can tell a transient outage from one a deployment
195
+ operator must clear. It carries nothing else the call-path classifier knows: a
196
+ discovery read is not a call. Unscoped searches keep the count only — one
197
+ connector's failure is not another search's context. An empty query browses
198
+ rather than searches, so it reports no term analysis — except when the scope
199
+ itself failed, where the same fields apply. A browse scoped to an unavailable
200
+ connector carries `unavailableConnectorCount`, `catalogError`, and guidance,
201
+ and an unscoped browse again carries the count alone. A browse scoped to an ID
202
+ that is not configured at all carries `connectorScope`, `unknownConnector`, and
203
+ the same omit-the-connector guidance the term-bearing path gives — nothing was
204
+ attempted, so there is no count and no `catalogError` — and it names no
205
+ connector but the one the caller supplied. A connector that correctly exposes
206
+ no tools still reports no analysis, so the two do not serialize alike. The
207
+ advice to browse a connector with an empty query must not land in silence that
208
+ reads like a connector with no tools. Analysis
126
209
  from a connector-filtered search includes `connectorScope` and speaks only
127
210
  about that connector; `unknownConnector` distinguishes an unconfigured ID from
128
211
  a known connector with no match. Analysis covers at most eight distinct terms
@@ -0,0 +1,77 @@
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 provider-rate admission
6
+ budget, a task-oriented usage guide, and a vetted safety classification. It
7
+ adds no provider dependency and 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
+ OAuth is the recommended default and keeps each connector instance's flow and
29
+ tokens in its connector-scoped storage. Mixpanel service accounts are also
30
+ supported with an explicit header override:
31
+
32
+ ```ts
33
+ mixpanel("automation_analytics", {
34
+ purpose: "Headless release-health reporting",
35
+ auth: {
36
+ type: "headers",
37
+ headers: { Authorization: `Bearer Basic ${env.MIXPANEL_SA_TOKEN}` },
38
+ },
39
+ });
40
+ ```
41
+
42
+ Keep that encoded service-account value in the runtime's secret store; it is a
43
+ password, not ordinary configuration. Mixpanel currently labels service-account
44
+ MCP authentication beta. Prefer OAuth unless the deployment is intentionally
45
+ headless.
46
+
47
+ The wrapper classifies the documented observational tools as reads and the
48
+ documented create, update, edit, merge, dismiss, duplicate, and delete tools as
49
+ writes. An unfamiliar tool the downstream leaves unannotated fails closed onto
50
+ `call_destructive_tool` until a Connecta release reviews it.
51
+
52
+ That classification is **fill-in only**, and unconditionally so: it supplies
53
+ the annotations Mixpanel leaves unset and contradicts an explicit downstream
54
+ annotation in neither direction. A tool on the read allowlist arriving with
55
+ `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
56
+ downstream said and stays behind `call_destructive_tool`. A tool on neither
57
+ maintained list arriving with `readOnlyHint: true` keeps that too, and stays
58
+ callable from `execute_code`. Both are the downstream telling you this
59
+ release's allowlist is stale, and on a name no release has reviewed its word is
60
+ the only evidence there is. The one classification that still outranks the
61
+ downstream is a name this release reviewed and filed destructive: a
62
+ `Delete-Dashboard` claiming `readOnlyHint: true` is a downstream bug rather
63
+ than news, and stays on the approval path. Maintained writes that only create
64
+ something new (`Create-Dashboard`, `Create-Cohort`, `Create-Metric`, and the
65
+ rest) leave `destructiveHint` unset; `readOnlyHint: false` already routes them
66
+ through the destructive path, and asserting destruction only inflates the
67
+ approval copy the host shows a human.
68
+
69
+ Experiments and Feature Flags — 15 of the 63 classified tools — are Mixpanel
70
+ beta surfaces. Expect their names and schemas to move faster than the rest.
71
+
72
+ The connection also declares a per-runtime call-admission budget matching
73
+ Mixpanel's documented 600 requests per hour — a best-effort approximation of
74
+ the per-user limit, not an enforcement of it. Each runtime keeps its own
75
+ counter, so N Worker isolates or Node processes serving one deployment can each
76
+ admit up to 600. Discovery traffic is outside connector call admission and
77
+ still needs restrained use.
@@ -0,0 +1,233 @@
1
+ # Notion prebuilt connection
2
+
3
+ Import `notion()` independently from `@zackbart/connecta/providers/notion`. It
4
+ is a hand-written `api()` surface over Notion's public REST API — fifteen
5
+ deliberate tools, lean projections of Notion's famously bloated payloads, typed
6
+ failures, a rate budget matched to the documented limit, and a required usage
7
+ guide. It adds no provider dependency, imports no `node:` builtin, and is not
8
+ reachable from Connecta's root entry.
9
+
10
+ ```ts
11
+ import { notion } from "@zackbart/connecta/providers/notion";
12
+
13
+ const wiki = notion("engineering_wiki", {
14
+ title: "Engineering wiki",
15
+ purpose: "Runbooks, specs, and on-call notes for the platform team",
16
+ instructions: "Prefer the Runbooks database; specs live under Projects.",
17
+ });
18
+ ```
19
+
20
+ The `id` owns the ordinary connector namespaces; use a different id for every
21
+ Notion workspace. `purpose` is required because an agent choosing between two
22
+ instances needs to know which workspace answers the question. Workspace
23
+ `instructions` are appended to the maintained guide and cannot change the
24
+ connector's safety classification.
25
+
26
+ ## Why this one is `api()` and not `remoteMcp()`
27
+
28
+ Notion publishes an MCP server, but the interesting problem here is not
29
+ transport — it is shape. A single Notion page returns every property as a
30
+ discriminated wrapper object, every string as an array of rich-text runs each
31
+ carrying its own annotations block, and every user reference as a nested
32
+ object. A twenty-five row database query is tens of kilobytes of structure
33
+ around a few hundred bytes of meaning. Hand-writing the surface is what makes
34
+ the projections possible, and the projections are the point.
35
+
36
+ ## Authentication
37
+
38
+ One operator-managed credential: an internal integration token from
39
+ [notion.so/profile/integrations](https://www.notion.so/profile/integrations).
40
+ The deployment needs `credentials.encryptionKey` configured, or the token
41
+ cannot be stored and every call fails `auth_required` at use.
42
+
43
+ Two Notion-specific facts decide whether a working token is enough:
44
+
45
+ - **Sharing is per object.** A token reaches only what has been explicitly
46
+ shared with its integration. An unshared page returns 404, not 403.
47
+ - **Capabilities are per integration**, and the comment capabilities are off by
48
+ default. `list_comments` and `add_comment` fail with 403 until an operator
49
+ turns them on in Notion.
50
+
51
+ `/credentials` offers a Test action, which calls `GET /v1/users/me` — the
52
+ cheapest call that proves a token is live — and reports the workspace it
53
+ authenticated into.
54
+
55
+ ## The pinned API version
56
+
57
+ The connection pins `Notion-Version: 2026-03-11` and offers no override. That
58
+ is deliberate rather than lazy. Notion's versions are date-named and old ones
59
+ keep working indefinitely, so an override would look harmless; it is not.
60
+ `2026-03-11` is the version in which databases split into data sources,
61
+ `archived` became `in_trash`, and block append took a `position` object instead
62
+ of an `after` string. Every projection and write body here assumes those
63
+ shapes, so a deployment that pinned an older version would get quietly wrong
64
+ results instead of a loud failure.
65
+
66
+ Notion also ships *additive* changes to every version simultaneously, which is
67
+ why the property and block projections fall back to unwrapping an unknown
68
+ type's payload rather than switching exhaustively. A property type that ships
69
+ after this release degrades to its raw value, and a block type that does keeps
70
+ its payload under `raw`; neither vanishes.
71
+
72
+ ## Tools
73
+
74
+ Ten reads, all annotated `readOnlyHint: true`:
75
+
76
+ | Tool | What it is for |
77
+ | --- | --- |
78
+ | `search` | Find pages and data sources by **title**. Never searches content. |
79
+ | `get_page` | One page's metadata and flattened property values. |
80
+ | `get_page_content` | A page's body as flat blocks reduced to plain text. |
81
+ | `get_page_property` | One property in full, past the 25-entry truncation. |
82
+ | `get_database` | A database container and the data sources inside it. |
83
+ | `get_data_source_schema` | Property ids, types, and select/status options. |
84
+ | `query_data_source` | Filtered, sorted rows with properties already flattened. |
85
+ | `list_users` | Workspace users and bots with their ids. |
86
+ | `get_self` | Which integration and workspace this connector authenticates as. |
87
+ | `list_comments` | Unresolved comments as plain text with discussion ids. |
88
+
89
+ Five writes, none read-only, so all of them route through
90
+ `call_destructive_tool`:
91
+
92
+ | Tool | Classification |
93
+ | --- | --- |
94
+ | `create_page` | additive |
95
+ | `append_blocks` | additive |
96
+ | `add_comment` | additive |
97
+ | `update_page_properties` | `destructiveHint: true` — replaces existing values |
98
+ | `trash_page` | `destructiveHint: true` — removes a page from reads |
99
+
100
+ The additive three leave `destructiveHint` unset: `readOnlyHint: false` already
101
+ routes them through the approval path, and claiming a create destroys something
102
+ only inflates the copy a host shows a human. `update_page_properties`
103
+ deliberately has no `in_trash` argument, so an update can never trash a page by
104
+ accident; trashing is its own named, reversible tool.
105
+
106
+ ## Lean projections, and the raw escape hatch
107
+
108
+ Every read projects. A page becomes ids, plain text, and flattened values:
109
+ `title` and `rich_text` collapse to strings, `select` and `status` to their
110
+ option name, `multi_select` to an array of names, `relation` to an array of
111
+ page ids, `people` to `{ id, name }`, `unique_id` to `"RL-12"`, and
112
+ `formula`/`rollup` to their computed value. No `plain_text` runs, no
113
+ `annotations` blocks, no property wrappers survive.
114
+
115
+ Two projections are opinionated enough to call out:
116
+
117
+ - **`search` returns identity fields only** — no properties at all. A
118
+ twenty-five result search across a populated database would otherwise drag
119
+ back several hundred flattened values for results the agent is about to
120
+ discard. `get_page` fetches properties for the one that matched.
121
+ - **`get_page` reports what Notion hid.** Notion paginates four property types
122
+ — `title`, `rich_text`, `relation`, and `people` — cutting each off at 25
123
+ entries and signalling it only with a `has_more` flag on the property itself.
124
+ The projection surfaces those in `truncated_properties` as `{ name, id }`,
125
+ which is what stops an agent from confidently reasoning about 25 of 300
126
+ relations. The `id` is there because the handoff needs it: `get_page_property`
127
+ addresses a property by id, not by name.
128
+
129
+ Where the dropped detail can matter — `search`, `get_page`, `get_page_content`,
130
+ `get_page_property`, `get_data_source_schema`, `query_data_source`,
131
+ `list_comments` — `raw: true` returns Notion's untouched response instead. It is
132
+ much larger; it exists so a missing field is never a dead end. When the goal is
133
+ *fewer* fields rather than more, `get_page` and `query_data_source` also take a
134
+ `properties` array to project only the named ones.
135
+
136
+ One caveat with `get_page_content`: `raw: true` returns the requested level
137
+ exactly as Notion sent it and does not walk nested children, so `depth` is
138
+ ignored alongside it. A raw read of a deep page yields one level, not three.
139
+ Unmodelled *block* types are covered without it — a block whose type this
140
+ projection does not know, and whose payload is not plain rich text, keeps that
141
+ payload verbatim under `raw` on the block, so nothing collapses to an empty
142
+ string.
143
+
144
+ ## Databases contain data sources
145
+
146
+ This is the trap the guide is marked `required` for. A Notion database is a
147
+ container; the rows and the schema live in a *data source* inside it, and the
148
+ two ids are not interchangeable. The id in a database's URL is a **database
149
+ id**, and passing it to `query_data_source` fails. The sequence is
150
+ `get_database` → `get_data_source_schema` → `query_data_source`; `search`
151
+ returns data sources directly and skips the first step.
152
+
153
+ `create_page` needs the same distinction: a row is created under
154
+ `parent_data_source_id`, never a database id. Its title also needs
155
+ `title_property` from the schema, because a database's title column is rarely
156
+ called "title".
157
+
158
+ ## Typed failures
159
+
160
+ The mapping is deliberately not one-to-one with Notion's error codes. Notion's
161
+ `code` says what its API thinks happened; Connecta's says what the caller
162
+ should do next, and two of Notion's are easy to mistranslate.
163
+
164
+ | Notion | Connecta | Why |
165
+ | --- | --- | --- |
166
+ | 400 (`validation_error`, `invalid_json`, `invalid_request`, `missing_version`, …) | `invalid_args` | every documented 400 is a malformed request |
167
+ | 401 `unauthorized` | `auth_required` | the token is missing or invalid |
168
+ | 403 `restricted_resource` | `connector_call_failed`, non-retryable | **not** `auth_required` |
169
+ | 404 `object_not_found` | `connector_call_failed`, non-retryable | overloaded; see below |
170
+ | 409 `conflict_error` | `unavailable`, retryable | Notion says to retry |
171
+ | 429 `rate_limited` | `rate_limited` + `retryAfterMs` | `Retry-After` seconds → ms |
172
+ | 529 `service_overload` | `unavailable` + `retryAfterMs` | back off like a 429 |
173
+ | 5xx | `unavailable`, retryable | upstream failure |
174
+
175
+ The two that matter:
176
+
177
+ **403 is not an authentication failure.** The token is fine; the integration
178
+ lacks a capability or was never shared the object. Routing it to
179
+ `auth_required` would send an agent to `authorize_connector`, which cannot
180
+ grant a Notion capability or share a page. It is a non-retryable call failure
181
+ whose message says an operator must change it in Notion.
182
+
183
+ **404 does not prove absence.** Notion returns `object_not_found` both for an
184
+ object that does not exist and for one that exists but has not been shared with
185
+ the integration, and it will not say which. The message says both, because
186
+ treating it as deletion is exactly how an agent concludes a page is gone when
187
+ it was simply never shared.
188
+
189
+ ## Rate limiting
190
+
191
+ Notion documents "an average of three requests per second, with some bursts
192
+ beyond the average allowed" per connection, plus a separate per-workspace limit
193
+ scaled to the plan. The connection declares a rolling budget of 180 calls per
194
+ minute — the same average expressed over a window short bursts pass and a
195
+ sustained loop does not — **paired with `maxConcurrency: 3`**.
196
+
197
+ The concurrency cap is the load-bearing half. A budget alone is an average, and
198
+ an averaged budget cannot stop a program from firing forty calls in the same
199
+ tick; the cap keeps a burst shaped roughly like the one Notion documents.
200
+ Neither half is a guarantee, because admission meters *tool calls* rather than
201
+ requests — a single admitted `get_page_content` can spend twenty fetches, so
202
+ 180 calls per minute is a floor on the real request rate, not a ceiling.
203
+ Declaring the cap is also what makes the queue settings legal — the admission
204
+ controller refuses queue settings without a queue at construction.
205
+
206
+ Like every connector budget this is per-runtime: N Worker isolates or Node
207
+ processes serving one deployment each keep their own counter. It approximates
208
+ the provider's limit; it does not enforce it.
209
+
210
+ One tool can turn a single agent call into several downstream requests:
211
+ `get_page_content` with `depth > 0` walks nested blocks, and call admission
212
+ meters tool calls, not the fetches inside them. That walk stops at an internal
213
+ ceiling of twenty requests and reports `truncated: true` rather than spending
214
+ the whole budget invisibly.
215
+
216
+ ## Pagination
217
+
218
+ List-shaped tools take `page_size` (1–100) and `start_cursor`, and return
219
+ `has_more` with `next_cursor`. The default page size is 25 rather than Notion's
220
+ 100, because a first read should be cheap; `defaultPageSize` raises it for a
221
+ deployment that pages a lot.
222
+
223
+ Cursors are opaque. Notion's own versioning page is explicit that they may
224
+ change in length, format, and structure at any time and must be passed back
225
+ verbatim — never parsed, validated, or constructed.
226
+
227
+ ## What this connection does not do
228
+
229
+ No file uploads, no database or data-source creation, no schema editing, no
230
+ block updates or deletes, no page moves. Those are all real Notion endpoints
231
+ and all deliberately absent: this is a deliberate tool surface, not a mirror of
232
+ the API. Anything missing is reachable through a custom `api()` connector
233
+ beside this one, which remains a first-class path.