@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.
- package/CHANGELOG.md +324 -0
- package/README.md +4 -1
- package/dist/catalog-service.d.ts +41 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +127 -11
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts +5 -4
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +5 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +5 -4
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +12 -10
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +5 -4
- package/dist/meta-tools.js.map +1 -1
- package/dist/providers/cloudflare.d.ts +53 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +1385 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/linear.d.ts +44 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +243 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/mixpanel.d.ts +21 -0
- package/dist/providers/mixpanel.d.ts.map +1 -0
- package/dist/providers/mixpanel.js +191 -0
- package/dist/providers/mixpanel.js.map +1 -0
- package/dist/providers/notion.d.ts +39 -0
- package/dist/providers/notion.d.ts.map +1 -0
- package/dist/providers/notion.js +1625 -0
- package/dist/providers/notion.js.map +1 -0
- package/dist/providers/stripe.d.ts +37 -0
- package/dist/providers/stripe.d.ts.map +1 -0
- package/dist/providers/stripe.js +232 -0
- package/dist/providers/stripe.js.map +1 -0
- package/dist/skills.d.ts +7 -9
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +58 -24
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +26 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/cloudflare.md +268 -0
- package/documentation/code-mode.md +6 -6
- package/documentation/connectors.md +122 -4
- package/documentation/linear.md +144 -0
- package/documentation/meta-tools.md +91 -8
- package/documentation/mixpanel.md +77 -0
- package/documentation/notion.md +233 -0
- package/documentation/stripe.md +202 -0
- package/ethos.md +8 -3
- package/package.json +21 -1
- package/src/catalog-service.ts +174 -10
- package/src/connectors/api.ts +10 -4
- package/src/connectors/remote-mcp.ts +5 -3
- package/src/execute.ts +18 -10
- package/src/index.ts +1 -0
- package/src/meta-tools.ts +10 -4
- package/src/providers/cloudflare.ts +1696 -0
- package/src/providers/linear.ts +301 -0
- package/src/providers/mixpanel.ts +228 -0
- package/src/providers/notion.ts +1879 -0
- package/src/providers/stripe.ts +306 -0
- package/src/skills.ts +64 -23
- package/src/types.ts +27 -6
- package/src/version.ts +1 -1
- 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.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
48
|
-
|
|
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.
|
|
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.
|