@zackbart/connecta 0.13.0 → 0.14.1
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 +225 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +33 -6
- package/dist/catalog-service.js.map +1 -1
- 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/execute.js +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/providers/cloudflare.d.ts +54 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +3210 -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.map +1 -1
- package/dist/providers/mixpanel.js +15 -7
- package/dist/providers/mixpanel.js.map +1 -1
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/cloudflare.md +313 -0
- package/documentation/connectors.md +13 -7
- package/documentation/linear.md +144 -0
- package/documentation/meta-tools.md +12 -1
- package/documentation/mixpanel.md +12 -7
- package/documentation/notion.md +233 -0
- package/documentation/stripe.md +202 -0
- package/ethos.md +1 -0
- package/package.json +17 -1
- package/src/catalog-service.ts +35 -6
- package/src/connectors/api.ts +5 -1
- package/src/execute.ts +1 -1
- package/src/providers/cloudflare.ts +3803 -0
- package/src/providers/linear.ts +301 -0
- package/src/providers/mixpanel.ts +15 -7
- package/src/providers/notion.ts +1879 -0
- package/src/providers/stripe.ts +306 -0
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
|
@@ -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.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Stripe prebuilt connection
|
|
2
|
+
|
|
3
|
+
Import `stripe()` independently from `@zackbart/connecta/providers/stripe`. It
|
|
4
|
+
wraps [Stripe's hosted MCP server](https://docs.stripe.com/mcp) with a required
|
|
5
|
+
production/sandbox mode, OAuth by default, a mode-scaled admission policy, a
|
|
6
|
+
task-oriented usage guide, and a vetted safety classification. It adds no
|
|
7
|
+
provider dependency and is not reachable from Connecta's root entry.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { stripe } from "@zackbart/connecta/providers/stripe";
|
|
11
|
+
|
|
12
|
+
const billing = stripe("stripe_live", {
|
|
13
|
+
mode: "production",
|
|
14
|
+
title: "Stripe (production)",
|
|
15
|
+
purpose: "Revenue, disputes, and refunds for the real business",
|
|
16
|
+
instructions: "Never refund above $500 without a human in the loop.",
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `id` owns the ordinary connector namespaces; use a different id for every
|
|
21
|
+
Stripe 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
|
+
## Mode is required, and it is the whole point
|
|
27
|
+
|
|
28
|
+
`mode` accepts `"production"` or `"sandbox"` and has **no default**. There is no
|
|
29
|
+
safe guess between an account that moves real money and one that does not, so
|
|
30
|
+
the deployment has to say which it configured.
|
|
31
|
+
|
|
32
|
+
Stripe publishes exactly one endpoint — `https://mcp.stripe.com/` — and the
|
|
33
|
+
environment is selected by the credential, not the URL. Connecta therefore
|
|
34
|
+
cannot *route* by mode; what it can do is make the mode impossible for an agent
|
|
35
|
+
to miss, and refuse a deployment whose declaration and credential disagree.
|
|
36
|
+
|
|
37
|
+
`mode` shows up in four places an agent actually reads:
|
|
38
|
+
|
|
39
|
+
- the default `title` (`Stripe (production)` / `Stripe (sandbox)`);
|
|
40
|
+
- the `description`, which is what `search_tools` ranks and returns — production
|
|
41
|
+
reads `Stripe payments (production — live money and real customers) — …`,
|
|
42
|
+
sandbox reads `Stripe payments (sandbox — test data, no real money) — …`;
|
|
43
|
+
- the first two lines of the usage guide, which state the mode and then say
|
|
44
|
+
either "every write moves real money … a refund cannot be undone" or "never
|
|
45
|
+
answer a question about live revenue, payouts, or a named customer from this
|
|
46
|
+
connector";
|
|
47
|
+
- the admission policy, below.
|
|
48
|
+
|
|
49
|
+
And one place a deployment author reads: if `auth` is a `headers` credential
|
|
50
|
+
carrying a recognizable Stripe key prefix (`sk_`, `rk_`, or `pk_` with `_live_`
|
|
51
|
+
or `_test_`), construction throws when the key's mode contradicts the declared
|
|
52
|
+
one. That check reads nothing it cannot classify — an OAuth connector, or a
|
|
53
|
+
credential shape this release does not recognize, is left alone rather than
|
|
54
|
+
guessed at — and the error names only the two modes, never the key.
|
|
55
|
+
|
|
56
|
+
Deploy both side by side. Two instances are isolated exactly like two
|
|
57
|
+
hand-written connectors with different ids: separate addresses, catalogs,
|
|
58
|
+
credentials, storage, admission counters, and health.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
connectors: [
|
|
62
|
+
stripe("stripe_live", {
|
|
63
|
+
mode: "production",
|
|
64
|
+
purpose: "Revenue, disputes, and refunds for the real business",
|
|
65
|
+
}),
|
|
66
|
+
stripe("stripe_sandbox", {
|
|
67
|
+
mode: "sandbox",
|
|
68
|
+
purpose: "Rehearsing billing changes before they touch production",
|
|
69
|
+
}),
|
|
70
|
+
]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
OAuth is the default and the option Stripe recommends: it supports dynamic
|
|
76
|
+
client registration and PKCE, and each connector instance keeps its own flow
|
|
77
|
+
and tokens in connector-scoped storage. Stripe also accepts a
|
|
78
|
+
[restricted API key](https://docs.stripe.com/keys#create-restricted-api-key) as
|
|
79
|
+
a bearer token for headless agents:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
stripe("stripe_sandbox", {
|
|
83
|
+
mode: "sandbox",
|
|
84
|
+
purpose: "Automated billing rehearsal",
|
|
85
|
+
auth: {
|
|
86
|
+
type: "headers",
|
|
87
|
+
headers: { Authorization: `Bearer ${env.STRIPE_RESTRICTED_KEY}` },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use a restricted key, not a secret key, and scope it to the operations the
|
|
93
|
+
agent actually needs; Stripe's own guidance is to "limit your agent's access to
|
|
94
|
+
exactly the functionality it requires". Keep it in the runtime's secret store.
|
|
95
|
+
|
|
96
|
+
Connect platforms can act as a connected account with `connectedAccount`, which
|
|
97
|
+
adds Stripe's `Stripe-Account` header. Stripe does not support OAuth for
|
|
98
|
+
connected-account calls, so this requires `headers` auth and throws otherwise:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
stripe("merchant_42", {
|
|
102
|
+
mode: "production",
|
|
103
|
+
purpose: "Billing questions for the merchant on account 42",
|
|
104
|
+
connectedAccount: "acct_1234567890",
|
|
105
|
+
auth: {
|
|
106
|
+
type: "headers",
|
|
107
|
+
headers: { Authorization: `Bearer ${env.STRIPE_PLATFORM_KEY}` },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Administrators must enable MCP access in the Stripe Dashboard, and Stripe
|
|
113
|
+
manages that setting **separately for sandbox and live mode**. A connector that
|
|
114
|
+
boots but cannot list tools is usually a dashboard toggle, not a bad key.
|
|
115
|
+
|
|
116
|
+
## The eleven tools, and what they are classified as
|
|
117
|
+
|
|
118
|
+
Stripe documents eleven tools on the hosted server. Seven are reads:
|
|
119
|
+
|
|
120
|
+
`stripe_api_search`, `stripe_api_details`, `stripe_api_read`,
|
|
121
|
+
`get_stripe_account_info`, `get_balance_summary`,
|
|
122
|
+
`search_stripe_documentation`, `stripe_implementation_planner`.
|
|
123
|
+
|
|
124
|
+
Four are writes:
|
|
125
|
+
|
|
126
|
+
`stripe_api_write` and `create_refund` are classified destructive;
|
|
127
|
+
`stripe_report` and `send_stripe_mcp_feedback` are additive.
|
|
128
|
+
|
|
129
|
+
Two of those deserve a sentence. `stripe_api_read` is a read because Stripe
|
|
130
|
+
documents it as the `GET` half of a generic pair — the tool is the read
|
|
131
|
+
boundary, not whichever endpoint an agent names inside it, and its sibling
|
|
132
|
+
`stripe_api_write` carries every `POST`, `PATCH`, `PUT`, and `DELETE`.
|
|
133
|
+
`create_refund` is filed destructive despite its name: it reverses a settled
|
|
134
|
+
charge and moves money back out, which is a mutation of something that already
|
|
135
|
+
exists rather than a fresh object appearing beside it. Additive writes
|
|
136
|
+
(`stripe_report`, `send_stripe_mcp_feedback`) leave `destructiveHint` unset;
|
|
137
|
+
`readOnlyHint: false` already routes them through `call_destructive_tool`, and
|
|
138
|
+
asserting destruction only inflates the approval copy the host shows a human.
|
|
139
|
+
|
|
140
|
+
That classification is **fill-in only**, and unconditionally so: it supplies
|
|
141
|
+
the annotations Stripe leaves unset — Stripe documents no MCP annotations at
|
|
142
|
+
all — and contradicts an explicit downstream annotation in neither direction. A
|
|
143
|
+
tool on the read allowlist arriving with `destructiveHint: true` or
|
|
144
|
+
`readOnlyHint: false` keeps exactly what the downstream said and stays behind
|
|
145
|
+
`call_destructive_tool`. A tool on neither maintained list arriving with
|
|
146
|
+
`readOnlyHint: true` keeps that too, and stays callable from `execute_code`.
|
|
147
|
+
Both are the downstream telling you this release's allowlist is stale, and on a
|
|
148
|
+
name no release has reviewed its word is the only evidence there is. The one
|
|
149
|
+
classification that still outranks the downstream is a name this release
|
|
150
|
+
reviewed and filed destructive: a `create_refund` claiming `readOnlyHint: true`
|
|
151
|
+
is a downstream bug rather than news, and stays on the approval path.
|
|
152
|
+
|
|
153
|
+
An unfamiliar tool that annotates nothing fails closed onto
|
|
154
|
+
`call_destructive_tool` until a Connecta release reviews it. That is not
|
|
155
|
+
hypothetical here: Stripe's own MCP page still carries a `create_customer`
|
|
156
|
+
example that its tool table no longer lists. Whatever the server actually
|
|
157
|
+
serves, an unclassified and unannotated `create_customer` lands on the approval
|
|
158
|
+
path. Expect the undocumented Treasury tools Stripe alludes to to arrive
|
|
159
|
+
unclassified as well — annotated ones will be taken at their word.
|
|
160
|
+
|
|
161
|
+
Stripe publishes no stability or deprecation policy for this tool set and
|
|
162
|
+
invites tool requests by email, so treat the list as unversioned. `get_balance_summary`
|
|
163
|
+
is Treasury, which Stripe labels public preview and gates behind an access
|
|
164
|
+
request — expect it to be absent unless the account is allowlisted, and expect
|
|
165
|
+
the other Treasury tools Stripe alludes to but does not document to arrive
|
|
166
|
+
unclassified.
|
|
167
|
+
|
|
168
|
+
## Rate limits
|
|
169
|
+
|
|
170
|
+
Stripe documents no rate limit specific to the MCP server. The connection
|
|
171
|
+
therefore transcribes the account limit that MCP traffic spends
|
|
172
|
+
([rate limits](https://docs.stripe.com/rate-limits)): **100 requests per second
|
|
173
|
+
in live mode, 25 in a sandbox**, and any single endpoint is capped at 25 per
|
|
174
|
+
second regardless of mode, so paging one list is the real constraint. The
|
|
175
|
+
`maxConcurrency` beside it — 8 for
|
|
176
|
+
production, 4 for sandbox — is Connecta's own conservative choice: Stripe
|
|
177
|
+
documents that per-account and per-endpoint concurrency limits exist, and
|
|
178
|
+
surface as `429` with a `Stripe-Rate-Limited-Reason` of `global-concurrency` or
|
|
179
|
+
`endpoint-concurrency`, but publishes no number.
|
|
180
|
+
|
|
181
|
+
As with every connector policy this is a **best-effort approximation** of the
|
|
182
|
+
provider's limit, not an enforcement of it. Each runtime keeps its own counter,
|
|
183
|
+
so N Worker isolates or Node processes serving one deployment can each admit up
|
|
184
|
+
to the stated rate, and the same Stripe account may be spending its budget on
|
|
185
|
+
traffic Connecta never sees. Discovery traffic is outside connector call
|
|
186
|
+
admission and still needs restrained use.
|
|
187
|
+
|
|
188
|
+
## What is not verified
|
|
189
|
+
|
|
190
|
+
Stripe's MCP documentation is silent on two things this connection had to reason
|
|
191
|
+
about rather than read:
|
|
192
|
+
|
|
193
|
+
- **How an OAuth session resolves to live versus sandbox at call time.** Stripe
|
|
194
|
+
says sessions are "scoped to … the current environment (live mode or a
|
|
195
|
+
sandbox)" and that dashboard access is managed separately per environment, but
|
|
196
|
+
never states the mechanism. The key-prefix check covers `headers` auth only;
|
|
197
|
+
for OAuth, `mode` is a declaration Connecta surfaces and cannot verify.
|
|
198
|
+
- **Whether pagination cursors and `Idempotency-Key` are passable through
|
|
199
|
+
`stripe_api_read` / `stripe_api_write`.** The conventions in the usage guide
|
|
200
|
+
are Stripe's documented API conventions; how they thread through the generic
|
|
201
|
+
tools' arguments is not documented. The guide states them because an agent
|
|
202
|
+
that ignores them is wrong either way.
|
package/ethos.md
CHANGED
|
@@ -70,6 +70,7 @@ proposing one without a new argument is not.
|
|
|
70
70
|
| Policy engine, approvals, pauses | refused | the host asks the human; connecta only annotates |
|
|
71
71
|
| Runtime connector registration | refused | config-as-code is the security model |
|
|
72
72
|
| Prebuilt connections as the preferred authoring path | accepted | an a-la-carte provider constructor, imported and constructed in the deployment file, encodes maintained defaults for providers connecta actually uses — preferred *when maintained*, with no promise of one per provider; it returns exactly one ordinary `Connector` with no extra privileges — never a bundle, a group, a preset, or a registry — its tools are hand-written or proxied from a downstream MCP catalog, never generated from a schema document; its vetted annotations classify what the downstream leaves unannotated and never overrule an explicit one; `remoteMcp()` and `api()` stay first-class ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
73
|
+
| Guarded raw REST escape hatches in a prebuilt connection | accepted | a large, fast-moving provider cannot be honestly represented by a small frozen list: a GET-only tool may expose provider-relative reads, while JSON mutations and explicit-content uploads stay separate and always cross the destructive boundary; the connector owns authentication, rate limits, error mapping, URL confinement, and safe method classification, while the provider token remains the capability boundary — this is not schema ingestion, runtime connector registration, or permission widening |
|
|
73
74
|
| Provider registry / integration marketplace | refused | prebuilt connections are imports, not listings; discovery happens in documentation, never at runtime ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
74
75
|
| Protocol sessions & server push | refused | stateless per request |
|
|
75
76
|
| Resources & prompts aggregation | refused | tools only; connecta's own Apps shell is the one `resources/read` carve-out ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
|
|
@@ -65,9 +65,25 @@
|
|
|
65
65
|
"types": "./dist/auth/clerk.d.ts",
|
|
66
66
|
"import": "./dist/auth/clerk.js"
|
|
67
67
|
},
|
|
68
|
+
"./providers/cloudflare": {
|
|
69
|
+
"types": "./dist/providers/cloudflare.d.ts",
|
|
70
|
+
"import": "./dist/providers/cloudflare.js"
|
|
71
|
+
},
|
|
72
|
+
"./providers/linear": {
|
|
73
|
+
"types": "./dist/providers/linear.d.ts",
|
|
74
|
+
"import": "./dist/providers/linear.js"
|
|
75
|
+
},
|
|
68
76
|
"./providers/mixpanel": {
|
|
69
77
|
"types": "./dist/providers/mixpanel.d.ts",
|
|
70
78
|
"import": "./dist/providers/mixpanel.js"
|
|
79
|
+
},
|
|
80
|
+
"./providers/notion": {
|
|
81
|
+
"types": "./dist/providers/notion.d.ts",
|
|
82
|
+
"import": "./dist/providers/notion.js"
|
|
83
|
+
},
|
|
84
|
+
"./providers/stripe": {
|
|
85
|
+
"types": "./dist/providers/stripe.d.ts",
|
|
86
|
+
"import": "./dist/providers/stripe.js"
|
|
71
87
|
}
|
|
72
88
|
},
|
|
73
89
|
"scripts": {
|
package/src/catalog-service.ts
CHANGED
|
@@ -834,13 +834,42 @@ export class CatalogService {
|
|
|
834
834
|
required: connectorGuideRequired(scopedConnector),
|
|
835
835
|
}
|
|
836
836
|
: undefined;
|
|
837
|
+
// A scope that resolved to nothing is the same silence one step earlier in
|
|
838
|
+
// the lookup: no connector resolved, so no catalog was even attempted, so
|
|
839
|
+
// no catalog failed and the unavailable path below never fires. Echo only
|
|
840
|
+
// the ID the caller already supplied — naming what else is configured
|
|
841
|
+
// would answer a question they did not ask, past a filter they may not
|
|
842
|
+
// pass.
|
|
843
|
+
const unknownConnectorGuidance =
|
|
844
|
+
args.connector && !scopedConnector
|
|
845
|
+
? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
|
|
846
|
+
: undefined;
|
|
847
|
+
// Term-bearing searches report analysis only when the scorer had to
|
|
848
|
+
// degrade; a browse has no terms to analyse and normally reports none at
|
|
849
|
+
// all. But neither "this catalog is unavailable" nor "there is no such
|
|
850
|
+
// connector" is a statement about terms, and answering either browse with
|
|
851
|
+
// an empty entry list alone is indistinguishable from a connector that
|
|
852
|
+
// simply exposes no tools. The term partitions stay empty on those paths
|
|
853
|
+
// because there were no terms — the scope fields carry the whole message.
|
|
854
|
+
const reportsQueryAnalysis =
|
|
855
|
+
queryTerms.length > 0
|
|
856
|
+
? matchMode === "partial"
|
|
857
|
+
: unknownConnectorGuidance !== undefined || unavailableCatalogs > 0;
|
|
837
858
|
const guidance =
|
|
838
859
|
queryTerms.length === 0
|
|
839
|
-
?
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
860
|
+
? // A browse has no terms to advise about, so it stays silent unless
|
|
861
|
+
// the scope itself failed: the guidance on a scoped miss recommends
|
|
862
|
+
// browsing with an empty query, and that advice must not lead into a
|
|
863
|
+
// dead end that looks like a connector with no tools.
|
|
864
|
+
(unknownConnectorGuidance ??
|
|
865
|
+
(unavailableCatalogs === 0
|
|
866
|
+
? undefined
|
|
843
867
|
: scopedConnector
|
|
868
|
+
? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
|
|
869
|
+
: `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`))
|
|
870
|
+
: matches.length === 0
|
|
871
|
+
? (unknownConnectorGuidance ??
|
|
872
|
+
(scopedConnector
|
|
844
873
|
? unavailableCatalogs > 0
|
|
845
874
|
? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
|
|
846
875
|
: scopedGuide?.required
|
|
@@ -848,7 +877,7 @@ export class CatalogService {
|
|
|
848
877
|
: `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
|
|
849
878
|
: unavailableCatalogs === 0
|
|
850
879
|
? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
851
|
-
: `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
880
|
+
: `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))
|
|
852
881
|
: matchMode === "partial"
|
|
853
882
|
? scopedConnector
|
|
854
883
|
? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
|
|
@@ -866,7 +895,7 @@ export class CatalogService {
|
|
|
866
895
|
...(matchMode === "partial" && matches.length > 0
|
|
867
896
|
? { matchMode }
|
|
868
897
|
: {}),
|
|
869
|
-
...(
|
|
898
|
+
...(reportsQueryAnalysis
|
|
870
899
|
? {
|
|
871
900
|
queryAnalysis: {
|
|
872
901
|
representedTerms,
|
package/src/connectors/api.ts
CHANGED
|
@@ -149,7 +149,11 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
149
149
|
});
|
|
150
150
|
if (invalid) throw invalid;
|
|
151
151
|
}
|
|
152
|
-
return
|
|
152
|
+
// `await` (not a bare promise return) so a handler that throws before
|
|
153
|
+
// its first await never sits handler-less for the thenable-adoption
|
|
154
|
+
// microtask — workerd and vitest both report that gap as an unhandled
|
|
155
|
+
// rejection even though the caller catches the failure.
|
|
156
|
+
return await tool.handler(input, ctx);
|
|
153
157
|
},
|
|
154
158
|
};
|
|
155
159
|
}
|
package/src/execute.ts
CHANGED
|
@@ -1351,7 +1351,7 @@ export function registerExecuteTool(
|
|
|
1351
1351
|
code: z
|
|
1352
1352
|
.string()
|
|
1353
1353
|
.describe(
|
|
1354
|
-
"One complete JavaScript async arrow function. Consume search/describe results and finish the task inside it; returning catalog data for a later call spends a round trip and buys nothing.",
|
|
1354
|
+
"One complete JavaScript async arrow function. Consume search/describe results and finish the task inside it; returning catalog data for a later call spends a round trip and buys nothing. So does aborting on a missing tool match or result key — re-search, describe, or read the result's actual keys here instead.",
|
|
1355
1355
|
),
|
|
1356
1356
|
diagnostics: z
|
|
1357
1357
|
.boolean()
|