@zackbart/connecta 0.18.3 → 0.19.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 +73 -4
- package/dist/catalog-service.d.ts +16 -13
- package/dist/catalog-service.js +106 -115
- package/dist/catalog.js +29 -46
- package/dist/connector-scope.js +2 -7
- package/dist/connectors/api.d.ts +4 -16
- package/dist/connectors/api.js +19 -46
- package/dist/connectors/guarded-fetch.d.ts +9 -23
- package/dist/connectors/guarded-fetch.js +38 -76
- package/dist/connectors/remote-mcp.js +36 -79
- package/dist/errors.d.ts +6 -27
- package/dist/errors.js +8 -5
- package/dist/execute.d.ts +24 -22
- package/dist/execute.js +98 -145
- package/dist/executor-result.d.ts +1 -0
- package/dist/executor-result.js +4 -11
- package/dist/executors/quickjs-child.js +1 -3
- package/dist/executors/quickjs-runtime.js +1 -3
- package/dist/executors/quickjs.js +1 -3
- package/dist/index.js +27 -57
- package/dist/invocation.js +113 -183
- package/dist/meta-tools.d.ts +15 -28
- package/dist/meta-tools.js +33 -89
- package/dist/providers/cloudflare.d.ts +2 -18
- package/dist/providers/cloudflare.js +1460 -2451
- package/dist/providers/linear.d.ts +4 -41
- package/dist/providers/linear.js +8 -39
- package/dist/providers/mixpanel.d.ts +3 -25
- package/dist/providers/mixpanel.js +7 -22
- package/dist/providers/notion.d.ts +1 -15
- package/dist/providers/notion.js +44 -173
- package/dist/providers/revenuecat.d.ts +4 -57
- package/dist/providers/revenuecat.js +10 -93
- package/dist/providers/stripe.d.ts +1 -12
- package/dist/providers/stripe.js +7 -45
- package/dist/registry.d.ts +9 -34
- package/dist/registry.js +9 -103
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -3
- package/dist/routes/shared.d.ts +15 -15
- package/dist/routes/shared.js +1 -3
- package/dist/timeout.d.ts +8 -7
- package/dist/timeout.js +47 -38
- package/dist/types.d.ts +3 -3
- package/dist/ui.d.ts +1 -25
- package/dist/ui.js +18 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/call-admission.md +1 -1
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +7 -7
- package/documentation/connectors.md +24 -1
- package/documentation/linear.md +1 -1
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +1 -1
- package/documentation/operations.md +18 -14
- package/documentation/provider-conventions.md +1 -1
- package/documentation/revenuecat.md +1 -1
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +13 -4
- package/ethos.md +75 -121
- package/package.json +3 -4
- package/templates/node/package.json +1 -1
- package/documentation/code-first-exploration.md +0 -292
- package/documentation/mcp-2026-07-28.md +0 -46
- package/documentation/mcp-ui-design.md +0 -382
- package/documentation/program-ui-read-calls.md +0 -213
- package/documentation/provider-audit.md +0 -198
- package/documentation/rich-output-design.md +0 -211
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
# Provider audit
|
|
2
|
-
|
|
3
|
-
[`provider-conventions.md`](./provider-conventions.md) wrote the bar down. This
|
|
4
|
-
document runs it against the six maintained prebuilt connections and returns a
|
|
5
|
-
verdict for every applicable convention: **meets**, **misses** (with the fix),
|
|
6
|
-
or **n/a** (with the reason). A convention is never quietly skipped, and an
|
|
7
|
-
accepted miss is recorded as a provider-specific exception with its argument
|
|
8
|
-
rather than left blank.
|
|
9
|
-
|
|
10
|
-
Hand-written HTTP providers are audited against H1–H14; hosted-MCP proxies
|
|
11
|
-
against P1–P13. Applying a hand-written convention to a proxy is a category
|
|
12
|
-
error, not a finding, so the proxy reports have no H rows.
|
|
13
|
-
|
|
14
|
-
Every miss below is fixed in the same change that recorded it
|
|
15
|
-
([#342](https://github.com/zackbart/connecta/issues/342)), except where the row
|
|
16
|
-
says otherwise. The mechanically checkable half of the hand-written bar is now
|
|
17
|
-
a test — [`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts)
|
|
18
|
-
walks the shipped surface of both `api()` providers on every run, so these
|
|
19
|
-
verdicts cannot rot quietly back into prose. The proxies' mechanical rows live
|
|
20
|
-
in their own suites, because what they assert is the wrapper's identity,
|
|
21
|
-
classification, and budget rather than tool shapes the wrapper does not own.
|
|
22
|
-
|
|
23
|
-
Two things this audit deliberately does not decide:
|
|
24
|
-
|
|
25
|
-
- **Whether each Cloudflare named tool earns its place.** That is
|
|
26
|
-
[#350](https://github.com/zackbart/connecta/issues/350), which measures the
|
|
27
|
-
named surface against the escape hatch with usage evidence. H14's keep/prune
|
|
28
|
-
judgment is reported here as open, not answered.
|
|
29
|
-
- **Whether a downstream catalog has drifted since a release reviewed it.**
|
|
30
|
-
Detection at refresh shipped with
|
|
31
|
-
[#343](https://github.com/zackbart/connecta/issues/343) and the maintainer-run
|
|
32
|
-
check is [#351](https://github.com/zackbart/connecta/issues/351). P13 is
|
|
33
|
-
audited as "is the list addressable by such a check", not as "is the list
|
|
34
|
-
current" — the answer to the second question arrives from a running
|
|
35
|
-
deployment, not from a reading.
|
|
36
|
-
|
|
37
|
-
## Cloudflare — hand-written HTTP
|
|
38
|
-
|
|
39
|
-
Fifty-five tools over the v4 REST API: fifty-two named, three guarded escape
|
|
40
|
-
hatches. The largest surface here and the one with the most to get wrong.
|
|
41
|
-
|
|
42
|
-
| Convention | Verdict | Notes |
|
|
43
|
-
| --- | --- | --- |
|
|
44
|
-
| H1 identity | meets | `id`, required `purpose` (blank throws), optional `title`, `instructions` appended under `## Account instructions` |
|
|
45
|
-
| H2 names | meets | every name is `snake_case` and opens with a verb from the connector's own set; the hatches sort together as `cloudflare_api_*` |
|
|
46
|
-
| H3 budgets | **missed → fixed** | `cloudflare_api_get` (289), `cloudflare_api_mutate` (266), and `create_dns_record` (305) exceeded the 240-character describe budget. All three trimmed; the record-type list `create_dns_record` was spending 60 characters on is already the `type` enum |
|
|
47
|
-
| H4 disqualifiers | meets (a reading) | the hatch descriptions say what they will not do — mutate, upload — and `create_dns_record` now names the types it reads but cannot create |
|
|
48
|
-
| H5 schemas | **missed → fixed** | the top level was already exemplary — every tool a closed plain object with a `required` list, a description on every property, and an `api()` construction contract that refuses an unenforceable schema (#340) — but H5 says *every* property, and the nested ones had been read as furniture. `bulk_write_kv_values` shipped six undescribed fields inside `entries[]`, including the expiry pair whose units and floor are the whole question. All six now say what they are, and the convention test walks every depth rather than the first one |
|
|
49
|
-
| H5 exception | recorded | the three escape hatches' request parts — `query[]`, `headers[]`, `fields[]`, `files[]` — keep undescribed `name`/`value` members, because H5 collides with H7 there. `query` and `headers` are one shared constant the renderer inlines into all three hatches, and `cloudflare_api_upload` sits at 1,007 of the 1,024-byte budget this same audit brought it back under; describing name/value pairs the parent property has already named as name/value pairs would truncate the whole tool in discovery. The 21 properties are listed by path in `test/provider-conventions.test.ts` and asserted exactly, so a new one fails and so does a stale entry |
|
|
50
|
-
| H6 whose bound | meets | exemplary. `pagingInputProperties` carries a three-way `bounds` vocabulary — `cloudflare`, `clamped`, `undocumented` — and the description says which one applies |
|
|
51
|
-
| H7 compact fit | **missed → fixed** | `cloudflare_api_upload` rendered to 1,297 bytes. The refused-header list, inlined once per hatch, moved to the usage guide; the remaining upload descriptions were cut to the fact each adds. Now 1,007 |
|
|
52
|
-
| H8 output schemas | meets | 52 of 52 declare one |
|
|
53
|
-
| H9 projection | meets | reads project and rename; `raw: true` wherever the projection drops something recoverable; `cloudflare_api_get` is the universal unprojected read |
|
|
54
|
-
| H10 pagination | **missed → fixed** | two conventions live here, which H10 allows, but only the guide said so. Three of the four cursor tools had a bare `nextCursor: { type: "string" }` with no description. Both ends now state it: `cursor` says the endpoint pages by cursor, `nextCursor` says it is the only signal and no `page` object is coming |
|
|
55
|
-
| H11 errors | meets | mapped by the caller's next move, including the 400-with-credential-code case that is `auth_required` rather than `invalid_args`; every mapped status has a test asserting code and retryability |
|
|
56
|
-
| H12 credential | meets | labeled fields per authentication mode, `testCredential` on the token path (`/user/tokens/verify`) and `testCredentials` on the Global API Key pair (`/user`), each reporting who it authenticated as |
|
|
57
|
-
| H13 guide | **missed → fixed** | the guide was a bare string, so its summary was derived from the first content line — the zone-scoping rule, which varies per deployment and reads as an instruction. Now structured with a declared summary. `required` stays unset, deliberately: every named schema is complete enough to call on its own and the scoping convention is repeated on each `zoneId` and `accountId` property, so forcing the guide into context before every operation would buy nothing |
|
|
58
|
-
| H14 hatch shape | meets | split GET / JSON-mutate / upload, the split is Connecta's, the GET tool is annotated read-only, paths are provider-relative and confined |
|
|
59
|
-
| H14 keep/prune | **open — [#350](https://github.com/zackbart/connecta/issues/350)** | whether each of the named tools beats the hatch on schema, projection, or safety routing is a reading that needs usage evidence. Out of scope here by the issue's own terms |
|
|
60
|
-
|
|
61
|
-
## Notion — hand-written HTTP
|
|
62
|
-
|
|
63
|
-
Fifteen tools over the public REST API, pinned to `2026-03-11`. A small,
|
|
64
|
-
deliberate surface.
|
|
65
|
-
|
|
66
|
-
| Convention | Verdict | Notes |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| H1 identity | meets | `id`, required `purpose` (blank throws), optional `title`, `instructions` appended under `## Workspace instructions` |
|
|
69
|
-
| H2 names | meets | `snake_case` throughout, and Notion's own vocabulary (`query_`, `append_`, `trash_`) where the shared verbs would lie — `trash_page` is not `delete_page`, because Notion does not delete |
|
|
70
|
-
| H3 budgets | **missed → fixed** | `search` was 252 characters. Trimmed to 205; the clause it lost restated what `get_page` is for |
|
|
71
|
-
| H4 disqualifiers | meets (a reading) | `search` "Never searches page content" is the convention document's own worked example, and it came from here |
|
|
72
|
-
| H5 schemas | **missed → fixed** | four gaps. A schema the validator could not evaluate would have forwarded arguments unchecked rather than refusing — in a surface we wrote ourselves that is our bug being papered over; `api()` now refuses such a schema at construction for every connector (#340). `search`, `list_users`, and `get_self` carried no `required` list at all, and `create_page` carried none because its constraint is exclusive rather than positional. All four now declare one, and the shipped schemas are asserted evaluable so fail-closed handling cannot become a blanket refusal. The fourth gap was nested: `query_data_source`'s `sorts[].direction` carried an enum and no description, which the top-level-only reading of H5 had missed |
|
|
73
|
-
| H5 exception | recorded | `create_page` declares `required: []`, not the truth. A page needs exactly one parent, but *which* parent is an exclusive choice a plain-object `required` list cannot express, and the top-level `anyOf` that could would cost the tool its `inputKeys` in discovery — the caller would learn nothing about the arguments without expanding the schema. The rule is stated in both parent descriptions and enforced locally as `invalid_args` before any round trip, so the cost H5 exists to avoid is still avoided |
|
|
74
|
-
| H6 whose bound | meets | `page_size` names Notion's 1–100 and says the default is the connector's configured one |
|
|
75
|
-
| H7 compact fit | **missed → fixed** | `query_data_source` rendered to 1,087 bytes. Three shared property descriptions (`raw`, `start_cursor`, `properties`) were carrying guide-length prose that the renderer inlines once per tool that uses them; cut to the fact each one adds. `query_data_source` is now 909 and every other tool got smaller for free |
|
|
76
|
-
| H8 output schemas | meets | 15 of 15 |
|
|
77
|
-
| H9 projection | meets | every read projects; `raw: true` on the seven reads where the dropped detail can matter; Notion's own 25-entry property truncation is surfaced as `truncated_properties` with the `property_id` needed to fetch the rest, rather than handed back as a confident partial |
|
|
78
|
-
| H10 pagination | meets | one convention, one signal: `page_size` and `start_cursor` in, `has_more` beside `next_cursor` out, default 25 against Notion's 100 |
|
|
79
|
-
| H11 errors | meets | and unusually careful about the ambiguity: a 404 is `connector_call_failed`, not `auth_required`, because `authorize_connector` cannot fix "never shared with this integration", and the message states the ambiguity instead of picking the convenient reading |
|
|
80
|
-
| H12 credential | meets | one labeled integration-token field; `testCredential` calls `/v1/users/me` and reports the workspace it authenticated as |
|
|
81
|
-
| H13 guide | meets | structured, declared summary, `required: true` with a stated reason — the database→data-source lookup is a sequence no complete schema can express |
|
|
82
|
-
| H14 hatch | **missed → fixed** | Notion has no guarded raw-REST tool, which H14 explicitly permits for a finite surface — provided it says so. It did not. The guide now names the absence, so an agent does not spend a search proving there is no `notion_api_get` |
|
|
83
|
-
|
|
84
|
-
The 0.17.0 drift review also considered Notion's expanded create and update
|
|
85
|
-
contracts. Workspace-private creation, templates, placement, richer media,
|
|
86
|
-
locking, and irreversible content erasure stay outside the maintained surface.
|
|
87
|
-
They are separate ownership, ordering, asynchronous, coordination, file, or
|
|
88
|
-
deletion workflows rather than missing fields on the five existing writes
|
|
89
|
-
([#408](https://github.com/zackbart/connecta/issues/408),
|
|
90
|
-
[#409](https://github.com/zackbart/connecta/issues/409)).
|
|
91
|
-
|
|
92
|
-
## Linear — hosted-MCP proxy
|
|
93
|
-
|
|
94
|
-
| Convention | Verdict | Notes |
|
|
95
|
-
| --- | --- | --- |
|
|
96
|
-
| P1 add, never rewrite | meets | `listTools` maps annotations and returns every other field untouched |
|
|
97
|
-
| P2 identity | meets | required `purpose` (blank throws), `instructions` appended, and appended text cannot reach the classification |
|
|
98
|
-
| P3 routing fact | meets | read-only rides the default title *and* opens the guide, because search renders neither description |
|
|
99
|
-
| P4 endpoint default | **missed → fixed, departing from the letter** | `access` defaulted to `"read-write"`, which is not the safe endpoint. It is now required with no default, and construction throws naming both options. The convention says "default to the safe one"; the honest reading of its heading — *the safest **honest** default* — is that Linear has none. Defaulting to `"read-only"` would turn a deployment that does write into one whose every write fails at Linear, at runtime, where no agent can repair it; defaulting to `"read-write"` hands out writes nobody asked for. Requiring the declaration fails at construction, where an operator can act. This is the one place in the audit where the fix departs from a convention's literal text, and it is recorded here rather than smuggled |
|
|
100
|
-
| P5 classification | meets | reads and writes named, unlisted resolves to not-read-only, reviewed destructive beats a contradictory `readOnlyHint: true`, additive writes leave `destructiveHint` unset |
|
|
101
|
-
| P6 catalog varies | meets | the guide names customer requests, releases, and code review as the plan- and feature-gated areas where absence is expected |
|
|
102
|
-
| P7 reduction advice | **missed → fixed** | the guide was a bare string. Its derived summary was the access note cut mid-sentence at 120 characters — the one fact an agent must not get wrong, delivered as a fragment. Now structured with a declared summary per access mode. `required` stays unset: Linear's own schemas describe each call, and the guide's value is cross-tool sequence advice worth reading before a write, not before every read |
|
|
103
|
-
| P8 identity resolution | meets | exemplary. The guide names the read tools that produce each id, and separates Linear's human identifier (`ENG-123`) from its UUID |
|
|
104
|
-
| P9 authentication | meets | OAuth default, `requireHttps`, personal API key documented as a secret and paired with the narrowest access, `auth_required` → `authorize_connector` route named in the guide |
|
|
105
|
-
| P10 no credential test | meets | no `credential`, `testCredential`, or `testCredentials` on the wrapper |
|
|
106
|
-
| P11 transport vs tool error | meets | inherited whole from `remoteMcp()`; the wrapper adds no error handling and reads no downstream prose |
|
|
107
|
-
| P12 admission budget | meets | exemplary, and the reason P12 exists. Linear documents no MCP-specific limit and meters the underlying API per user per hour, so the connection declares no budget and documents how an operator supplies one |
|
|
108
|
-
| P13 drift visible | meets | both lists are module-level constants in one file per provider, and now *are* the manifest the wrapper classifies from, compared against the live catalog on every refresh ([#343](https://github.com/zackbart/connecta/issues/343)); the maintainer-run check is [#351](https://github.com/zackbart/connecta/issues/351) |
|
|
109
|
-
|
|
110
|
-
## Stripe — hosted-MCP proxy
|
|
111
|
-
|
|
112
|
-
| Convention | Verdict | Notes |
|
|
113
|
-
| --- | --- | --- |
|
|
114
|
-
| P1 add, never rewrite | meets | annotations only |
|
|
115
|
-
| P2 identity | meets | required `purpose`, `instructions` appended, classification untouchable from there; purpose states deployment routing intent and the guide says it is not proof of authenticated account identity |
|
|
116
|
-
| P3 routing fact | meets | OAuth metadata states mixed account scope and the guide resolves mode from `livemode`; fixed header credentials retain their mode in every routing surface |
|
|
117
|
-
| P4 endpoint default | meets | OAuth has no connector-wide mode to default; static headers require one, and construction throws when a recognizable key prefix contradicts it |
|
|
118
|
-
| P5 classification | meets | including the two verdicts that needed an argument — `stripe_api_read` is a read because the tool is the boundary, `create_refund` is destructive despite its name |
|
|
119
|
-
| P6 catalog varies | **missed → fixed** | the doc already knew this (`get_balance_summary` is Treasury and gated; a `create_customer` example survives in Stripe's prose but not its tool table), but the *guide* did not say it, and the guide is what reaches the agent. Added |
|
|
120
|
-
| P7 reduction advice | **missed → fixed** | OAuth has a mixed-scope summary; fixed credentials keep mode-shaped summaries. `required` stays unset because the four generic tools remain the routing decision |
|
|
121
|
-
| P8 identity resolution | **missed → fixed** | The guide names typed object ids and their read sources. For OAuth it requires `list_available_accounts_or_orgs`, then carries the returned `stripe_context` and `livemode` unchanged; ambiguity stops ([#404](https://github.com/zackbart/connecta/issues/404), [#414](https://github.com/zackbart/connecta/issues/414)) |
|
|
122
|
-
| P9 authentication | meets | OAuth default, `requireHttps`, restricted key documented as a secret and paired with the narrowest scope. The guide distinguishes organization accounts within an OAuth session from Connect connected accounts, whose calls reject OAuth and use a deployment-configured restricted key plus `Stripe-Account`. The `auth_required` → `authorize_connector` route was added alongside P8, since a proxy's only recovery instruction lives there |
|
|
123
|
-
| P10 no credential test | meets | no credential slot; the mode/key contradiction throws at construction instead, which is where P10 says the H12 guarantee gets paid |
|
|
124
|
-
| P11 transport vs tool error | meets | inherited from `remoteMcp()`; the guide now also says that a rejected argument or plan restriction arrives in Stripe's own words and is not an authorization problem |
|
|
125
|
-
| P12 admission budget | meets | fixed credentials use their documented mode rate; mixed OAuth uses the stricter 25/s sandbox rate and concurrency bound |
|
|
126
|
-
| P13 drift visible | meets | both lists are module-level constants in one file, and are the manifest the refresh-time drift check compares against ([#343](https://github.com/zackbart/connecta/issues/343)) |
|
|
127
|
-
|
|
128
|
-
## Mixpanel — hosted-MCP proxy
|
|
129
|
-
|
|
130
|
-
The proxy with the most misses, and none of them subtle: it was written before
|
|
131
|
-
the conventions existed and inherited its shape from Linear without inheriting
|
|
132
|
-
Linear's reasoning.
|
|
133
|
-
|
|
134
|
-
| Convention | Verdict | Notes |
|
|
135
|
-
| --- | --- | --- |
|
|
136
|
-
| P1 add, never rewrite | meets | annotations only |
|
|
137
|
-
| P2 identity | meets | required `purpose`, `instructions` appended |
|
|
138
|
-
| P3 routing fact | **missed → fixed** | region is exactly the fact P3 names, and it appeared in neither the default title (`"Mixpanel"`) nor the guide's first line. A project lives in one residency, so a question pointed at the wrong connector comes back empty rather than wrong — which reads as the project having no data. The title now carries it (`Mixpanel (us)`, `(eu)`, `(in)`) and the guide opens with it |
|
|
139
|
-
| P4 endpoint default | meets | three published endpoints, an option that selects between them, and `"us"` as the default because that is where a project lives unless it was explicitly created elsewhere. Unlike Linear's, this default is honest: a wrong region cannot cause an irrecoverable write, only an empty read. Construction now also rejects a region there is no endpoint for |
|
|
140
|
-
| P5 classification | meets | reads and writes named, unlisted fails closed, reviewed destructive beats a contradictory `readOnlyHint: true` |
|
|
141
|
-
| P6 catalog varies | **missed → fixed** | the provider doc knew that 15 of the 63 classified tools are beta surfaces; the guide did not say so. Added, naming experiments, feature flags, session replay, and issue triage as the usual absentees |
|
|
142
|
-
| P7 reduction advice | **missed → fixed** | bare string, derived summary. Now structured with a declared, region-shaped summary. `required` unset: the project-then-context sequence is worth reading before an analysis, not before every call |
|
|
143
|
-
| P8 identity resolution | **missed → fixed** | the guide told an agent not to guess event and property *spelling* but said nothing about ids, and Mixpanel's `Get-`, `Update-`, and `Delete-` tools all take them. Added, naming the `List-` tools that produce each one |
|
|
144
|
-
| P9 authentication | meets | OAuth default, `requireHttps`, service account documented as a secret |
|
|
145
|
-
| P10 no credential test | partial — n/a for half | no `credential`, `testCredential`, or `testCredentials`, as required. The construction-time contradiction check P10 points at has nothing to check here: a Mixpanel service-account token does not encode its region, so there is no recognizable credential for a declared region to contradict. Recorded rather than invented — guessing a region from a token shape this release does not understand is precisely what P4 tells Stripe not to do |
|
|
146
|
-
| P11 transport vs tool error | meets | inherited from `remoteMcp()`; the guide now names the `auth_required` → `authorize_connector` route and says a plan restriction arrives in Mixpanel's own words |
|
|
147
|
-
| P12 admission budget | **missed → fixed** | the connection hardcoded a 600-call hourly budget transcribed from a limit Mixpanel meters **per user**. P12 names this case exactly: a per-runtime counter cannot approximate a per-user quota in either direction — one runtime serving several users under-counts, several isolates sharing one credential each admit a full budget. The default is removed; `callAdmission` is now an operator option with a documented example, matching Linear |
|
|
148
|
-
| P13 drift visible | meets | both lists are module-level constants in one file, and are the manifest the refresh-time drift check compares against ([#343](https://github.com/zackbart/connecta/issues/343)) |
|
|
149
|
-
|
|
150
|
-
## RevenueCat — hosted-MCP proxy
|
|
151
|
-
|
|
152
|
-
Written after the conventions existed, so it has no misses to record — only two
|
|
153
|
-
places where the honest answer departs from the obvious one, both argued below.
|
|
154
|
-
Ninety-five documented tools, ninety-four classified, one deliberately not.
|
|
155
|
-
|
|
156
|
-
| Convention | Verdict | Notes |
|
|
157
|
-
| --- | --- | --- |
|
|
158
|
-
| P1 add, never rewrite | meets | `listTools` maps annotations and returns every other field untouched |
|
|
159
|
-
| P2 identity | meets | required `purpose` (blank throws), `instructions` appended under `## Project instructions`, and appended text cannot reach the classification |
|
|
160
|
-
| P3 routing fact | meets, with the fact split in two | the routing fact is scope, and it has two halves. The *shape* — one project versus every project the account can reach — is knowable at construction and rides the default title (`RevenueCat (single project)` versus `RevenueCat`). *Which* project a key opens is not knowable without calling something, which P10 forbids, so it rides the guide's first line and the declared summary, built from the operator's `purpose`. That makes this the one maintained proxy with a purpose-bearing summary rather than a static one, and the reason is P3's own cost: two `sk_` connectors share a title, an endpoint, and a catalog, so a static summary would leave them indistinguishable in the only field search returns |
|
|
161
|
-
| P4 endpoint default | n/a — one endpoint, and the scope rides the credential | RevenueCat publishes a single MCP endpoint, so there is nothing to select between. The scope difference comes from the credential shape itself, which the constructor reads rather than asks for: `auth.type === "headers"` *is* the single-project declaration. There is no mode to default and no mode to contradict, so the P4 machinery Stripe needs has nothing to do here |
|
|
162
|
-
| P5 classification | meets | 50 reads, 15 additive writes, 29 destructive writes named; `render-paywall-screenshot` is on neither list because RevenueCat's reference gives it no access column, and it fails closed. Nine borderline verdicts are argued beside the rows they decide, and asserted in the suite so a silent flip fails |
|
|
163
|
-
| P6 catalog varies | meets | the guide names paywall AI editing, benchmarks, experiments, virtual currencies, and account billing as the plan-, platform-, and beta-gated areas where absence is expected, and separately names the unclassified tool so its approval prompt does not read as a bug |
|
|
164
|
-
| P7 reduction advice | meets | structured guide, declared summary, cursor-then-reduce advice aimed at the two objects that are actually large here (customers and their event history). `required` stays unset: the project-resolution sequence is worth reading before a run, not before every call |
|
|
165
|
-
| P8 identity resolution | meets | the guide names the whole chain — `list-projects` for the `project_id` every project-scoped call takes, then `list-apps`, `list-products`, `list-entitlements`, `list-offerings`, `list-paywalls`, `list-audiences`, and `list-customers` for the ids their `get-`, `update-`, `archive-`, and `delete-` counterparts expect — and says a plausible-looking id belongs to another project or to nobody. For OAuth it also says to stop and ask when more than one project fits |
|
|
166
|
-
| P9 authentication | meets | OAuth default, `requireHttps`, the API v2 secret key documented as a secret and paired with the narrowest scope RevenueCat offers (one project). The guide names the `auth_required` → `authorize_connector` route, and separately says that a read-only key's refusal is RevenueCat's own words rather than an authorization gap connecta can repair |
|
|
167
|
-
| P10 no credential test | meets | no `credential`, `testCredential`, or `testCredentials`. This is also where the constructor's most tempting option was refused: a `project?: string` checked against `list-projects` at construction is a credential test wearing a configuration hat, so the operator's stated purpose carries the claim and the agent confirms it on first use. There is no recognizable-credential contradiction to throw on either — an `sk_` key encodes no project — so the construction-time half of P10 has nothing to check here, exactly as it has nothing to check for Mixpanel's region |
|
|
168
|
-
| P11 transport vs tool error | meets | inherited whole from `remoteMcp()`; the wrapper adds no error handling and reads no downstream prose. The guide says a rejected argument, a permission gap, and a plan restriction all arrive in RevenueCat's own words |
|
|
169
|
-
| P12 admission budget | meets, by declining a number that exists | RevenueCat does publish limits, which is why this row needed an argument rather than a shrug. It meters per domain — 480/min for customer information, virtual currencies, and refunds; 60 for project configuration and audiences; 25 for charts and metrics — and a `ConnectorCallAdmissionPolicy` carries exactly one rule. Picking 25 throttles a customer read loop to a nineteenth of its allowance; picking 480 leaves a chart sweep unprotected; neither is the provider's limit. The metering scope repeats the point: developer-level keys are metered per developer, which a per-runtime counter cannot approximate. So the guide states RevenueCat's own numbers and the `429` / `Retry-After` / `backoff_ms` signals, and `callAdmission` stays an operator option with a documented example |
|
|
170
|
-
| P13 drift visible | meets | both lists are module-level constants in one file and *are* the manifest the wrapper classifies from, compared against the live catalog on every refresh ([#343](https://github.com/zackbart/connecta/issues/343)). The maintainer-run check accepts `revenuecat` with `CONNECTA_DRIFT_REVENUECAT_KEY`. No schema digests are recorded, and the manifest says so rather than shipping invented ones |
|
|
171
|
-
|
|
172
|
-
## Scoreboard
|
|
173
|
-
|
|
174
|
-
| Provider | Meets | Missed and fixed | Recorded exception | Open |
|
|
175
|
-
| --- | --- | --- | --- | --- |
|
|
176
|
-
| Cloudflare | 9 | 5 | H5 hatch request parts | H14 keep/prune ([#350](https://github.com/zackbart/connecta/issues/350)) |
|
|
177
|
-
| Notion | 10 | 4 | H5 exclusive parent | — |
|
|
178
|
-
| Linear | 11 | 2 | P4 departs from the letter | — |
|
|
179
|
-
| Stripe | 10 | 3 | — | — |
|
|
180
|
-
| Mixpanel | 7 | 5 | P10 half n/a | — |
|
|
181
|
-
| RevenueCat | 12 | 0 | P4 n/a (one endpoint); P3 met with a purpose-bearing summary | — |
|
|
182
|
-
|
|
183
|
-
Nineteen misses, nineteen fixes, six recorded exceptions, one judgment left to
|
|
184
|
-
the issue that owns it. The pattern in the misses is worth naming: sixteen of
|
|
185
|
-
the nineteen are a guide, a title, or a schema description failing to *say*
|
|
186
|
-
something the implementation already did correctly. Only three changed what a
|
|
187
|
-
provider does — Notion refusing an unevaluable schema, Linear requiring an
|
|
188
|
-
access declaration, Mixpanel dropping a budget it could not honestly compute.
|
|
189
|
-
The conventions are mostly not asking for different behavior. They are asking
|
|
190
|
-
for the behavior to reach the agent, which is a different problem and, on this
|
|
191
|
-
evidence, the one the providers were losing.
|
|
192
|
-
|
|
193
|
-
RevenueCat is the first connection written *after* the conventions and adds no
|
|
194
|
-
misses to those nineteen, which is the least interesting thing about its row.
|
|
195
|
-
The interesting part is that two conventions came out somewhere other than
|
|
196
|
-
their obvious reading — P4 has no endpoint to select and P12 declines a number
|
|
197
|
-
the provider actually publishes — and both had to be argued rather than
|
|
198
|
-
skipped. A convention that only ever returns "meets" is not being applied.
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
# Rich MCP output from programs — design record
|
|
2
|
-
|
|
3
|
-
The decision record for [#267](https://github.com/zackbart/connecta/issues/267):
|
|
4
|
-
whether and how an `execute_code` program may deliver MCP-native output — text,
|
|
5
|
-
images, audio — instead of only a single JSON return value. The verdicts live in
|
|
6
|
-
[`ethos.md`](../ethos.md); this document carries the argument, the contract
|
|
7
|
-
precisely enough to implement, and the shapes that were considered and refused.
|
|
8
|
-
The implementation landed via
|
|
9
|
-
[#270](https://github.com/zackbart/connecta/issues/270): the contract clauses
|
|
10
|
-
below (`M1`–`M10`) are now normative in [`code-mode.md`](./code-mode.md)'s
|
|
11
|
-
"Emitted output" section, which wins where the two disagree. This document
|
|
12
|
-
remains the argument and the record of rejected shapes.
|
|
13
|
-
|
|
14
|
-
## The problem
|
|
15
|
-
|
|
16
|
-
A program returns one JSON value, bounded at 24,000 serialized characters
|
|
17
|
-
(`R2`). That is the right contract for data: compose, project, return only what
|
|
18
|
-
you need. It is no contract at all for an image. Base64 is not projectable — a
|
|
19
|
-
program cannot "return less" of a screenshot — so any rich block a downstream
|
|
20
|
-
tool produces dies at the exit guard, converted to a truncation envelope whose
|
|
21
|
-
preview is the head of a base64 string, which is of use to no one.
|
|
22
|
-
|
|
23
|
-
The asymmetry is already visible in the direct-call surface. `call_tool` forwards
|
|
24
|
-
non-text downstream blocks to the client untouched when they fit the result cap
|
|
25
|
-
(the fix for issue #43 made the guard measure every block, then pass them
|
|
26
|
-
through in original order). And on the intake side, a host call **inside** a
|
|
27
|
-
program is not size-capped at all: `unwrapMcpResult` hands mixed content
|
|
28
|
-
through as a raw object, base64 and all. So today a downstream image survives
|
|
29
|
-
the trip *into* the sandbox and survives the explicit tool boundary — the one
|
|
30
|
-
place it cannot survive is the exit of the surface connecta calls primary.
|
|
31
|
-
|
|
32
|
-
## The shape: `connecta.emit(block)`
|
|
33
|
-
|
|
34
|
-
One new provider function. A program emits zero or more validated MCP content
|
|
35
|
-
blocks during its run; the host collects them request-locally and appends them
|
|
36
|
-
to the final `execute_code` result, after the JSON envelope, in emission order.
|
|
37
|
-
|
|
38
|
-
The single most load-bearing fact about this shape: **it does not touch the
|
|
39
|
-
executor contract.** `Executor.execute()` still returns
|
|
40
|
-
`{ result, error?, logs? }`, which is what keeps the interface structurally
|
|
41
|
-
compatible with `DynamicWorkerExecutor` from `@cloudflare/codemode` — a class
|
|
42
|
-
connecta does not control and must not fork. `emit` is a provider function like
|
|
43
|
-
`connecta.call`: both executors already bridge provider calls (QuickJS over
|
|
44
|
-
child-process IPC, the Dynamic Worker over isolate RPC), so emitted bytes cross
|
|
45
|
-
the guest boundary exactly once, as an argument, and are never seen again by
|
|
46
|
-
the sandbox, the `ExecuteResult`, or the `R2` guard. Executor parity is not a
|
|
47
|
-
test obligation bolted on afterward; it is structural. A third-party executor
|
|
48
|
-
that correctly bridges provider functions gets emission without writing a line.
|
|
49
|
-
|
|
50
|
-
### Contract (drafts for code-mode.md)
|
|
51
|
-
|
|
52
|
-
**M1.** `connecta.emit(block)` accepts exactly one MCP content block of type
|
|
53
|
-
`text`, `image`, or `audio`:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
{ type: "text", text: string }
|
|
57
|
-
{ type: "image", data: string /* base64 */, mimeType: string }
|
|
58
|
-
{ type: "audio", data: string /* base64 */, mimeType: string }
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The shape is validated strictly at the call — required fields present, no
|
|
62
|
-
extra fields, no `annotations`, no `_meta`. An invalid block throws a
|
|
63
|
-
catchable error and nothing is accepted. Rejected, not stripped: silently
|
|
64
|
-
deleting fields would deliver something the program did not ask to emit.
|
|
65
|
-
There is no sugar form (`emit("text")` is invalid); sugar is how a
|
|
66
|
-
one-shape contract grows hair.
|
|
67
|
-
|
|
68
|
-
**M2.** Emitted blocks are collected on the host in emission order and
|
|
69
|
-
delivered only with a successful result: the response's `content` array is the
|
|
70
|
-
JSON envelope text block first, then the emitted blocks. When at least one
|
|
71
|
-
block was emitted the envelope gains `emitted: N`; when none were, the
|
|
72
|
-
response is byte-for-byte the ordinary path (the `R6` discipline).
|
|
73
|
-
`structuredContent` remains the JSON envelope alone — emission is a
|
|
74
|
-
presentation channel, not a second data channel.
|
|
75
|
-
|
|
76
|
-
**M3.** The return value and emission are independent. `R2` applies to the
|
|
77
|
-
return value exactly as before and never measures emitted bytes; a truncated
|
|
78
|
-
return value does not suppress emitted blocks, and emitted blocks do not
|
|
79
|
-
shrink the return budget.
|
|
80
|
-
|
|
81
|
-
**M4.** A program that ends in an error delivers no emitted blocks. The error
|
|
82
|
-
envelope reports `emittedDiscarded: N` when N > 0, so the discard is visible
|
|
83
|
-
rather than silent. Partial rich output from a failed program is ambiguity,
|
|
84
|
-
not a deliverable.
|
|
85
|
-
|
|
86
|
-
**M5.** Two budgets, both deployment-configurable, both failing loudly at the
|
|
87
|
-
`emit` call: an aggregate serialized-byte budget (default 4,000,000 bytes) and
|
|
88
|
-
a block count (default 32). An emit that would exceed either throws a
|
|
89
|
-
catchable error naming the budget and the room remaining; the block is not
|
|
90
|
-
partially accepted, and prior accepted blocks are unaffected. There is no
|
|
91
|
-
`get_result` stash and no paging: the program learns it is over budget while
|
|
92
|
-
it can still choose differently, which is `R4`'s spirit applied to bytes that
|
|
93
|
-
genuinely cannot be shrunk — refuse at the door instead of paging after the
|
|
94
|
-
fact.
|
|
95
|
-
|
|
96
|
-
**M6.** Connecta claims no provenance. Every emitted block is program output,
|
|
97
|
-
trusted exactly as much as the program's return value — no more because its
|
|
98
|
-
bytes happen to have come from a downstream call, no less because they were
|
|
99
|
-
assembled in the sandbox. Preserving a downstream image means re-emitting it:
|
|
100
|
-
the program selects the block from the raw downstream result (which reaches it
|
|
101
|
-
uncapped) and emits it. The host attaches no attribution.
|
|
102
|
-
|
|
103
|
-
**M7.** `emit` does not spend the host-call budget (`L4`). Its bounds are
|
|
104
|
-
`M5`'s and only `M5`'s.
|
|
105
|
-
|
|
106
|
-
**M8.** `ExecuteResult` is unchanged. Neither QuickJS, Dynamic Worker, nor a
|
|
107
|
-
third-party executor needs modification, and the parity suite runs the same
|
|
108
|
-
emitting program through both vitest projects and asserts identical delivered
|
|
109
|
-
content.
|
|
110
|
-
|
|
111
|
-
**M9.** Emission is request-local and unstreamed. Blocks exist only in the
|
|
112
|
-
finished response; nothing is pushed early, nothing survives the request, and
|
|
113
|
-
`emit` resolving means "accepted into the collection," never "delivered."
|
|
114
|
-
|
|
115
|
-
**M10.** Activity remains payload-free by construction. With
|
|
116
|
-
`diagnostics: true` the diagnostics block gains one aggregate — emitted block
|
|
117
|
-
count and serialized bytes — numbers only, per `R8`.
|
|
118
|
-
|
|
119
|
-
### Intake, specified
|
|
120
|
-
|
|
121
|
-
`unwrapMcpResult`'s current behavior becomes contract rather than accident: an
|
|
122
|
-
all-text downstream result is JSON-parsed when possible (as today), and a
|
|
123
|
-
result carrying non-text blocks passes through as the raw object, `content`
|
|
124
|
-
array intact, uncapped. That raw fallthrough is the preservation path `M6`
|
|
125
|
-
depends on. Bounding it would kill re-emission at intake; projecting it would
|
|
126
|
-
repeat the mistake `R1` refuses.
|
|
127
|
-
|
|
128
|
-
## Sizing rationale
|
|
129
|
-
|
|
130
|
-
The 24,000-character return boundary is a *context* budget — the return value
|
|
131
|
-
lands in the model's window as text. Emitted image and audio blocks do not:
|
|
132
|
-
MCP-aware hosts deliver them as media, which models ingest at media prices,
|
|
133
|
-
not base64-text prices. So the emission budget is a *transport* bound, not a
|
|
134
|
-
context bound, and 4,000,000 serialized bytes (roughly a 3 MB binary after
|
|
135
|
-
base64's 4/3 inflation — two or three real screenshots) is deliberately far
|
|
136
|
-
above `R2` without being a file-hosting ambition. Deployments that know their
|
|
137
|
-
client's limits tune it; the default just has to make `emit` useful for the
|
|
138
|
-
motivating case without inviting anyone to ship video through an MCP response.
|
|
139
|
-
|
|
140
|
-
## Security posture
|
|
141
|
-
|
|
142
|
-
- **Nothing is minted.** `emit` grants no authority: it cannot cause a fetch,
|
|
143
|
-
reference a credential, or make the host serve anything. It moves bytes the
|
|
144
|
-
program already had into the response.
|
|
145
|
-
- **Strict typing is the lure defense.** The block validator accepting only
|
|
146
|
-
`text`, `image`, and `audio` is what keeps a program from emitting a
|
|
147
|
-
`resource_link` whose URI a helpful client might dereference. The refused
|
|
148
|
-
types are refused precisely because they are pointers, and pointers get
|
|
149
|
-
followed.
|
|
150
|
-
- **Image-borne injection is the existing class.** A downstream screenshot
|
|
151
|
-
containing hostile text is the same hazard through `emit` as through
|
|
152
|
-
`call_tool`'s block passthrough today; connecta's posture is unchanged — all
|
|
153
|
-
tool output is untrusted input to the client, and connecta adds no claim
|
|
154
|
-
otherwise.
|
|
155
|
-
|
|
156
|
-
## Considered and refused
|
|
157
|
-
|
|
158
|
-
**A sentinel return shape** (`return { $mcpContent: [...] }`). Overloads the
|
|
159
|
-
one data channel with a magic key that collides with honest data, subjects
|
|
160
|
-
rich blocks to the `R2` guard they cannot survive, and turns "what did this
|
|
161
|
-
program return" into a parse question. The return value stays a value.
|
|
162
|
-
|
|
163
|
-
**Widening `ExecuteResult` or the `Executor` interface.** The obvious place
|
|
164
|
-
and the wrong one: the interface's value is that `@cloudflare/codemode`'s
|
|
165
|
-
executor already satisfies it. A `content` field connecta added would either
|
|
166
|
-
fork the Workers executor or wait on a vendor; and every third-party executor
|
|
167
|
-
would need matching surgery. Refused as a class, not just deferred.
|
|
168
|
-
|
|
169
|
-
**Provenance-preserving handles** (downstream blocks stay host-side; the guest
|
|
170
|
-
gets `{ $ref }` tokens to pass to `emit`). Honest attribution, but it creates
|
|
171
|
-
host-side object identity that generated code holds references to —
|
|
172
|
-
capability-shaped machinery in the one place the ethos says generated code
|
|
173
|
-
mints nothing. And the label buys nothing: no MCP client trusts a tool result
|
|
174
|
-
more because an aggregator vouches for its lineage. Re-emission delivers the
|
|
175
|
-
same bytes with a simpler story: everything a program emits is program output.
|
|
176
|
-
|
|
177
|
-
**`resource` and `resource_link` emission.** Connecta already refuses to
|
|
178
|
-
aggregate resources; letting programs emit them would introduce through the
|
|
179
|
-
back door a surface the front door refused, and a guest-minted URI is a lure
|
|
180
|
-
(see the posture above). A future argument would have to be a new one.
|
|
181
|
-
|
|
182
|
-
**A `get_result` stash for over-budget emissions.** Paging exists for
|
|
183
|
-
downstream payloads a model could not shrink; an over-budget emission is a
|
|
184
|
-
program decision that has not happened yet. Failing the `emit` call while the
|
|
185
|
-
program can still adapt beats stashing megabytes nobody may page.
|
|
186
|
-
|
|
187
|
-
**Streaming or partial delivery.** Standing invariant — no server push,
|
|
188
|
-
nothing request-bound survives the request. Emission is collect-then-deliver
|
|
189
|
-
by construction (`M9`).
|
|
190
|
-
|
|
191
|
-
**Delivering emissions from failed programs.** The blocks may describe a world
|
|
192
|
-
the error contradicts. Failure delivers the error, the logs, and an honest
|
|
193
|
-
count of what was discarded (`M4`).
|
|
194
|
-
|
|
195
|
-
## Verification sketch
|
|
196
|
-
|
|
197
|
-
- Parity: one emitting program, both vitest projects (`WORKERS_SUITES`),
|
|
198
|
-
identical delivered content arrays.
|
|
199
|
-
- Validation: each rejected shape (missing field, extra field, `annotations`,
|
|
200
|
-
unknown type, bare string) throws catchably and accepts nothing.
|
|
201
|
-
- Budgets: byte and count budgets fail at the crossing call, prior blocks
|
|
202
|
-
intact; error names the budget.
|
|
203
|
-
- Independence: over-`R2` return value + emitted blocks → truncation envelope
|
|
204
|
-
*and* delivered blocks in one response.
|
|
205
|
-
- Discard: throwing program with prior emits → error envelope with
|
|
206
|
-
`emittedDiscarded`, no blocks in `content`.
|
|
207
|
-
- Byte-for-byte: a program that never emits produces today's exact response.
|
|
208
|
-
|
|
209
|
-
The implementation issue carries these as acceptance criteria; the clauses
|
|
210
|
-
fold into `code-mode.md` under a new "Emitted output" section when it closes,
|
|
211
|
-
and the suite takes its row in the [test map](./operations.md#the-test-map).
|