@zackbart/connecta 0.10.5 → 0.11.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/AGENTS.md +8 -6
- package/CHANGELOG.md +150 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +0 -7
- package/dist/activity.d.ts +11 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +44 -3
- package/dist/activity.js.map +1 -1
- package/dist/catalog-service.d.ts +24 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +68 -9
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/errors.d.ts +49 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +68 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +73 -3
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +161 -29
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +28 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -37
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +9 -2
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +61 -31
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +24 -59
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +107 -359
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/registry.d.ts +12 -10
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -17
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +19 -21
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +9 -11
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +8 -18
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +13 -60
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +6 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-first-exploration.md +16 -16
- package/documentation/code-mode.md +137 -63
- package/documentation/connectors.md +1 -1
- package/documentation/meta-tools.md +96 -33
- package/documentation/rich-output-design.md +212 -0
- package/ethos.md +17 -19
- package/examples/node/README.md +1 -2
- package/examples/node/src/index.ts +1 -3
- package/examples/worker/README.md +19 -16
- package/examples/worker/src/d1-activity-row.ts +40 -0
- package/examples/worker/src/d1-activity.ts +3 -2
- package/examples/worker/src/index.ts +6 -14
- package/examples/worker/wrangler.jsonc +3 -6
- package/package.json +1 -1
- package/src/activity.ts +69 -3
- package/src/catalog-service.ts +113 -20
- package/src/connectors/api.ts +2 -2
- package/src/connectors/remote-mcp.ts +1 -1
- package/src/errors.ts +104 -3
- package/src/execute.ts +237 -37
- package/src/index.ts +60 -67
- package/src/invocation.ts +61 -19
- package/src/meta-tools.ts +136 -482
- package/src/operator-ui/browser.ts +10 -2
- package/src/operator-ui/generated.ts +1 -1
- package/src/registry.ts +7 -35
- package/src/routes/mcp.ts +19 -21
- package/src/routes/shared.ts +8 -11
- package/src/server.ts +7 -7
- package/src/skills.ts +11 -74
- package/src/types.ts +6 -21
- package/src/version.ts +1 -1
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
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 test-map row in [`operations.md`](./operations.md) lands with the
|
|
212
|
+
suite.
|
package/ethos.md
CHANGED
|
@@ -18,13 +18,15 @@ order, and amending it is a design decision, not a drive-by edit.
|
|
|
18
18
|
- **Two equal ways in.** `remoteMcp()` proxies a downstream MCP server;
|
|
19
19
|
`api()` hand-writes a deliberate tool surface over a plain HTTP API. Both
|
|
20
20
|
come out identical: same addresses, same catalog, same safety rules.
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
- **Seven tools, an executor required.** The primary surface is a program, so
|
|
22
|
+
every deployment runs an executor — a Dynamic Worker on Cloudflare, QuickJS
|
|
23
|
+
behind its optional-peer subpath on Node — and one without refuses to boot
|
|
24
|
+
([#273](https://github.com/zackbart/connecta/issues/273)). Every meta-tool
|
|
25
|
+
earns its keep: the default answer to "agents need X" is the program
|
|
26
|
+
surface, and a capability has to be shown inexpressible through it before
|
|
27
|
+
it earns a top-level tool
|
|
28
|
+
([#224](https://github.com/zackbart/connecta/issues/224)). Packaging is
|
|
29
|
+
unchanged: required describes the deployment, never the dependencies.
|
|
28
30
|
- **Safe by default.** Only tools explicitly annotated read-only are callable
|
|
29
31
|
without crossing the destructive boundary — directly or from generated code;
|
|
30
32
|
everything else goes through `call_destructive_tool`, where the MCP host can
|
|
@@ -33,12 +35,6 @@ order, and amending it is a design decision, not a drive-by edit.
|
|
|
33
35
|
- **One fetch-native core, two runtimes.** The same code runs unchanged on
|
|
34
36
|
Cloudflare Workers and in Node — a Worker or a Docker stack, your pick. Web
|
|
35
37
|
APIs only in the core; Node touches live behind explicit subpaths.
|
|
36
|
-
- **An executor is the assumed posture.** The primary surface is a program, so
|
|
37
|
-
the deployment connecta is written toward has one — a Dynamic Worker on
|
|
38
|
-
Cloudflare, QuickJS behind its optional-peer subpath on Node. Configuring one
|
|
39
|
-
is what selects the code-first surface; executor-free stays supported as
|
|
40
|
-
compatibility, not as an equal citizen. Packaging is unchanged: assumed is
|
|
41
|
-
about defaults, never about dependencies.
|
|
42
38
|
- **Observable, never administrable.** Operator pages show connector status,
|
|
43
39
|
masked credentials, and payload-free activity. They can rotate a secret;
|
|
44
40
|
they cannot add a connector, change policy, or alter what an agent can call.
|
|
@@ -79,12 +75,10 @@ proposing one without a new argument is not.
|
|
|
79
75
|
| Agent credential recovery | accepted | one `auth_required` route through `authorize_connector`; only an operator handles secrets ([#192](https://github.com/zackbart/connecta/issues/192)) |
|
|
80
76
|
| Operator-issued MCP access tokens | accepted | named, revocable authentication gives header-capable clients a small alternative to OAuth; tokens identify callers but never scope tools or become operator credentials |
|
|
81
77
|
| Structured result surface | accepted | canonical `structuredContent` plus complete compact `content`; summary-only text is gated on host-forwarding evidence ([#191](https://github.com/zackbart/connecta/issues/191)) |
|
|
82
|
-
| Code mode (`execute_code`) | accepted | the primary read, discovery, and composition surface: smaller serialized definitions
|
|
83
|
-
| Code-first as the
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
| Executor-assumed posture | accepted | the primary surface is a program, so a deployment without an executor can only ever be the compatibility shape; packaging invariants are untouched ([#224](https://github.com/zackbart/connecta/issues/224)) |
|
|
87
|
-
| Classic surface retention | accepted | what an executor-free deployment necessarily serves, the rollback path, and the eval's control arm; `surface: "classic"` alongside an executor is the only knob, and whether classic is ever removed is a separate future decision ([#224](https://github.com/zackbart/connecta/issues/224)) |
|
|
78
|
+
| Code mode (`execute_code`) | accepted | the primary read, discovery, and composition surface: smaller serialized definitions, far smaller results once composition and projection happen before the model sees them, and a cold-start model that read the interface without help ([exploration](./documentation/code-first-exploration.md), [#224](https://github.com/zackbart/connecta/issues/224)) |
|
|
79
|
+
| Code-first as the default; the eval gate retired | accepted | owner decision, 2026-07-30: one operator, no deploy-time flip; [`eval/code-first-gate`](./eval/code-first-gate/README.md) survives as measurement, but nothing waits on its verdict ([#222](https://github.com/zackbart/connecta/issues/222), [#224](https://github.com/zackbart/connecta/issues/224)) |
|
|
80
|
+
| Surface consolidation to seven tools | accepted | `list_connectors`, `describe_tools`, and `batch_call` fold into the program surface, deleting the routing choice between direct calls, batches, discovery, and execution; `call_tool` stays because a simple call is not cheaper through code ([#224](https://github.com/zackbart/connecta/issues/224)) |
|
|
81
|
+
| Classic (executor-free) surface | removed | supersedes its provisional retention under [#224](https://github.com/zackbart/connecta/issues/224) — an executor is mandatory, and a deployment without one refuses to boot rather than serving a fallback shape ([#273](https://github.com/zackbart/connecta/issues/273)) |
|
|
88
82
|
| Connector shortcut namespaces in programs | accepted | sugar over canonical addressing, kept but frozen — every expansion invents a collision class `<connectorId>.<toolName>` already solved ([#223](https://github.com/zackbart/connecta/issues/223)) |
|
|
89
83
|
| Automatic host-side projection of program results | refused | the measured win was program-authored projection; a host heuristic drops fields a program chose to return and is invisible in the transcript ([#223](https://github.com/zackbart/connecta/issues/223)) |
|
|
90
84
|
| Caller-visible execution diagnostics | accepted | optional request-local timing and size aggregates make catalog, connector, and executor costs distinguishable without persisting payloads, adding a tool, or charging normal responses context ([#247](https://github.com/zackbart/connecta/issues/247)) |
|
|
@@ -94,6 +88,10 @@ proposing one without a new argument is not.
|
|
|
94
88
|
| MRTR / `input_required` passthrough | gated | statelessly relayable via `requestState`, but no host or downstream emits it yet; fails loudly until adoption evidence ([#176](https://github.com/zackbart/connecta/issues/176)) |
|
|
95
89
|
| Native Tasks for oversized results | refused | tasks solve duration, `get_result` solves size; paging on a polling extension adds round trips for nothing ([#176](https://github.com/zackbart/connecta/issues/176)) |
|
|
96
90
|
| Downstream `ttlMs` cache hints | gated | fixed TTL + fingerprint is battle-tested and catalog reads are ~3 ms; earns its way in with refresh-churn evidence ([#176](https://github.com/zackbart/connecta/issues/176)) |
|
|
91
|
+
| Rich program output (`connecta.emit`) | accepted | one host-collected emission channel: programs emit strictly validated text/image/audio blocks, delivered after the result envelope on success only; budgets fail loudly at the emit call ([design record](./documentation/rich-output-design.md), [#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
92
|
+
| Result-channel widening of the `Executor` contract | refused | `ExecuteResult` stays `{ result, error?, logs? }` — structural compatibility with `@cloudflare/codemode` is the parity guarantee; emission rides the provider bridge instead ([#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
93
|
+
| Guest-emitted `resource` / `resource_link` blocks | gated | a program can never mint a URI a client may dereference; MCP UI ([#266](https://github.com/zackbart/connecta/issues/266)) may design the one carve-out — programs supply only content, connecta mints the `ui://` address outside the sandbox ([#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
94
|
+
| Provenance tracking for emitted content | refused | everything a program emits is program output; handles or attribution labels are capability-shaped machinery that changes no client's trust posture ([#267](https://github.com/zackbart/connecta/issues/267)) |
|
|
97
95
|
|
|
98
96
|
## Invariants
|
|
99
97
|
|
package/examples/node/README.md
CHANGED
|
@@ -32,8 +32,7 @@ value outside this local example.
|
|
|
32
32
|
Keep this deployment small:
|
|
33
33
|
|
|
34
34
|
- Edit `src/index.ts` to change connectors, auth, storage, and the public URL.
|
|
35
|
-
- Keep `executor: quickJsExecutor()`
|
|
36
|
-
compatibility surface.
|
|
35
|
+
- Keep the required `executor: quickJsExecutor()` configuration.
|
|
37
36
|
- Keep secrets in environment variables or an external secret store. Never put
|
|
38
37
|
tokens in `src/index.ts`, connector guides, or committed JSON.
|
|
39
38
|
- Add application code only when implementing a deliberate `api()` connector.
|
|
@@ -30,9 +30,7 @@ const connecta = createConnecta({
|
|
|
30
30
|
auth: bearerToken(token, { subjectId: "operator" }),
|
|
31
31
|
// Downstream OAuth callbacks use this deployment origin.
|
|
32
32
|
publicUrl: `http://localhost:${port}`,
|
|
33
|
-
//
|
|
34
|
-
// This line is also what selects the seven-tool code-first surface; remove it
|
|
35
|
-
// to serve the nine classic meta-tools instead.
|
|
33
|
+
// Required: QuickJS runs model-written JS in a bounded disposable child.
|
|
36
34
|
executor: quickJsExecutor(),
|
|
37
35
|
connectors: [
|
|
38
36
|
api("time", {
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A deployable Worker that aggregates a downstream remote MCP and an in-code HTTP
|
|
4
4
|
API connector, guarded by Clerk OAuth *and* a static bearer token, with state in
|
|
5
|
-
a KV namespace.
|
|
6
|
-
|
|
7
|
-
without it, serving the nine classic meta-tools on the Workers Free plan.
|
|
5
|
+
a KV namespace. Its required Worker Loader binding backs the seven-tool surface
|
|
6
|
+
and requires the Workers Paid plan.
|
|
8
7
|
|
|
9
8
|
This is also the **starting template for a deployment**: a real deployment
|
|
10
9
|
should be its own repository that pins an exact `@zackbart/connecta` version and
|
|
@@ -56,21 +55,17 @@ Connections. Credentials is at `/credentials`, named MCP access tokens are at
|
|
|
56
55
|
|
|
57
56
|
## Code mode
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
The Worker Loader binding is
|
|
62
|
-
environment variable is needed. Add this block to `wrangler.jsonc` (and a comma
|
|
63
|
-
after the preceding property):
|
|
58
|
+
The Dynamic Worker sandbox requires the
|
|
59
|
+
[Workers Paid plan](https://developers.cloudflare.com/dynamic-workers/pricing/).
|
|
60
|
+
The required Worker Loader binding is checked into `wrangler.jsonc`:
|
|
64
61
|
|
|
65
62
|
```jsonc
|
|
66
63
|
"worker_loaders": [{ "binding": "LOADER" }]
|
|
67
64
|
```
|
|
68
65
|
|
|
69
|
-
`src/index.ts`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
the nine classic meta-tools. A deployment copied into its own repository must
|
|
73
|
-
also install the executor package before enabling the binding:
|
|
66
|
+
`src/index.ts` constructs `DynamicWorkerExecutor` from `env.LOADER` and serves
|
|
67
|
+
the seven-tool surface. A deployment copied into its own repository must also
|
|
68
|
+
install the executor package:
|
|
74
69
|
|
|
75
70
|
```sh
|
|
76
71
|
npm install @cloudflare/codemode
|
|
@@ -111,6 +106,7 @@ into `src/index.ts`**, so the example deploys without a database. To enable it:
|
|
|
111
106
|
duration_ms INTEGER NOT NULL,
|
|
112
107
|
attempts INTEGER NOT NULL,
|
|
113
108
|
error_code TEXT,
|
|
109
|
+
friction TEXT,
|
|
114
110
|
server_name TEXT NOT NULL,
|
|
115
111
|
server_version TEXT NOT NULL,
|
|
116
112
|
deployment_id TEXT
|
|
@@ -120,14 +116,21 @@ into `src/index.ts`**, so the example deploys without a database. To enable it:
|
|
|
120
116
|
ON tool_call_activity (occurred_at_ms DESC, id DESC);
|
|
121
117
|
```
|
|
122
118
|
|
|
123
|
-
**Already have this table?** `actor_namespace`
|
|
124
|
-
example, and `CREATE TABLE IF NOT EXISTS` will not add
|
|
125
|
-
already exists. Add
|
|
119
|
+
**Already have this table?** `actor_namespace` and `friction` were added
|
|
120
|
+
after the original example, and `CREATE TABLE IF NOT EXISTS` will not add
|
|
121
|
+
them to a table that already exists. Add them as migrations:
|
|
126
122
|
|
|
127
123
|
```sql
|
|
128
124
|
ALTER TABLE tool_call_activity ADD COLUMN actor_namespace TEXT;
|
|
125
|
+
ALTER TABLE tool_call_activity ADD COLUMN friction TEXT;
|
|
129
126
|
```
|
|
130
127
|
|
|
128
|
+
`friction` is stored rather than derived because one of its classes belongs
|
|
129
|
+
to a call that *succeeded*: a result too large to return inline is friction
|
|
130
|
+
for the agent and carries no error code. Rows written before the column keep
|
|
131
|
+
working — the mapping module derives their friction from `error_code` — and
|
|
132
|
+
`error_code IS NOT NULL` remains an honest count of failures.
|
|
133
|
+
|
|
131
134
|
Do this **before** deploying the updated `d1-activity.ts`: its `INSERT`
|
|
132
135
|
names the column, so against an un-migrated table every write fails with
|
|
133
136
|
`no such column`. Activity writes are best-effort by design — connecta logs
|
|
@@ -23,11 +23,44 @@ export interface ActivityEvent {
|
|
|
23
23
|
durationMs: number;
|
|
24
24
|
attempts: number;
|
|
25
25
|
errorCode?: string;
|
|
26
|
+
friction?:
|
|
27
|
+
| "tool_not_found"
|
|
28
|
+
| "schema_retry"
|
|
29
|
+
| "destructive_reroute"
|
|
30
|
+
| "auth_required"
|
|
31
|
+
| "result_too_large";
|
|
26
32
|
serverName: string;
|
|
27
33
|
serverVersion: string;
|
|
28
34
|
deploymentId?: string;
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Backfill for rows written before `friction` had a column of its own. Every
|
|
39
|
+
* arm must agree with the package's own `agentFrictionForCode`; the repository
|
|
40
|
+
* test suite pins that, because a dashboard that groups by friction should not
|
|
41
|
+
* change its answer depending on which side of a migration a row landed on.
|
|
42
|
+
* Friction that never had an error code — an oversized but successful result —
|
|
43
|
+
* is only recoverable from the column, which is why the column exists.
|
|
44
|
+
*/
|
|
45
|
+
function frictionForCode(code: string | null): ActivityEvent["friction"] {
|
|
46
|
+
switch (code) {
|
|
47
|
+
case "unknown_address":
|
|
48
|
+
case "unknown_tool":
|
|
49
|
+
case "ambiguous_tool_alias":
|
|
50
|
+
return "tool_not_found";
|
|
51
|
+
case "invalid_args":
|
|
52
|
+
return "schema_retry";
|
|
53
|
+
case "destructive_tool_requires_approval":
|
|
54
|
+
return "destructive_reroute";
|
|
55
|
+
case "auth_required":
|
|
56
|
+
return "auth_required";
|
|
57
|
+
case "result_too_large":
|
|
58
|
+
return "result_too_large";
|
|
59
|
+
default:
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
export interface ActivityRow {
|
|
32
65
|
id: string;
|
|
33
66
|
occurred_at_ms: number;
|
|
@@ -42,6 +75,7 @@ export interface ActivityRow {
|
|
|
42
75
|
duration_ms: number;
|
|
43
76
|
attempts: number;
|
|
44
77
|
error_code: string | null;
|
|
78
|
+
friction: ActivityEvent["friction"] | null;
|
|
45
79
|
server_name: string;
|
|
46
80
|
server_version: string;
|
|
47
81
|
deployment_id: string | null;
|
|
@@ -64,6 +98,10 @@ export function activityEventToRow(
|
|
|
64
98
|
duration_ms: event.durationMs,
|
|
65
99
|
attempts: event.attempts,
|
|
66
100
|
error_code: event.errorCode ?? null,
|
|
101
|
+
// Stored beside the code rather than derived from it: a truncated result is
|
|
102
|
+
// friction on a call that succeeded, so it has no error code to derive from
|
|
103
|
+
// — and `error_code IS NOT NULL` stays an honest count of failures.
|
|
104
|
+
friction: event.friction ?? null,
|
|
67
105
|
server_name: event.serverName,
|
|
68
106
|
server_version: event.serverVersion,
|
|
69
107
|
deployment_id: event.deploymentId ?? null,
|
|
@@ -73,6 +111,7 @@ export function activityEventToRow(
|
|
|
73
111
|
export function activityRowToEvent(
|
|
74
112
|
row: ActivityRow,
|
|
75
113
|
): ActivityEvent {
|
|
114
|
+
const friction = row.friction ?? frictionForCode(row.error_code);
|
|
76
115
|
return {
|
|
77
116
|
schemaVersion: 1,
|
|
78
117
|
id: row.id,
|
|
@@ -91,6 +130,7 @@ export function activityRowToEvent(
|
|
|
91
130
|
durationMs: row.duration_ms,
|
|
92
131
|
attempts: row.attempts,
|
|
93
132
|
...(row.error_code ? { errorCode: row.error_code } : {}),
|
|
133
|
+
...(friction ? { friction } : {}),
|
|
94
134
|
serverName: row.server_name,
|
|
95
135
|
serverVersion: row.server_version,
|
|
96
136
|
...(row.deployment_id
|
|
@@ -50,8 +50,8 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
|
|
|
50
50
|
id, occurred_at_ms, request_id, actor_kind, actor_id,
|
|
51
51
|
actor_namespace,
|
|
52
52
|
connector_id, tool_name, source, outcome, duration_ms, attempts,
|
|
53
|
-
error_code, server_name, server_version, deployment_id
|
|
54
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
53
|
+
error_code, friction, server_name, server_version, deployment_id
|
|
54
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
55
55
|
)
|
|
56
56
|
.bind(
|
|
57
57
|
row.id,
|
|
@@ -67,6 +67,7 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
|
|
|
67
67
|
row.duration_ms,
|
|
68
68
|
row.attempts,
|
|
69
69
|
row.error_code,
|
|
70
|
+
row.friction ?? null,
|
|
70
71
|
row.server_name,
|
|
71
72
|
row.server_version,
|
|
72
73
|
row.deployment_id,
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One MCP endpoint aggregating a downstream remote MCP and an HTTP API, guarded
|
|
5
5
|
* by Clerk OAuth *and* a static bearer token, with OAuth/cache state in a KV
|
|
6
|
-
* namespace.
|
|
7
|
-
* seven-tool
|
|
6
|
+
* namespace. The required Worker Loader binding in wrangler.jsonc backs the
|
|
7
|
+
* seven-tool surface.
|
|
8
8
|
*
|
|
9
9
|
* Setup (this example has no package.json of its own — it self-references the
|
|
10
10
|
* installed `@zackbart/connecta` package):
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
* and CLERK_PUBLISHABLE_KEY + PUBLIC_URL as plain vars in wrangler.jsonc.
|
|
20
20
|
* 4. Enable Dynamic Client Registration in the Clerk dashboard
|
|
21
21
|
* (OAuth Applications -> DCR toggle) so Claude/Cursor can self-register.
|
|
22
|
-
* 5.
|
|
23
|
-
* wrangler.jsonc. Binding presence enables execute_code automatically.
|
|
22
|
+
* 5. Use the Workers Paid plan required by the `worker_loaders` binding.
|
|
24
23
|
* 6. `wrangler deploy` from this folder (examples/worker), where wrangler.jsonc
|
|
25
24
|
* lives. Point your MCP client at `<PUBLIC_URL>/mcp`.
|
|
26
25
|
*/
|
|
@@ -46,23 +45,16 @@ interface Env {
|
|
|
46
45
|
PUBLIC_URL: string;
|
|
47
46
|
/**
|
|
48
47
|
* Worker Loader binding (wrangler.jsonc `worker_loaders`) powering
|
|
49
|
-
* execute_code
|
|
50
|
-
* the Workers Paid plan; leave the binding absent for the nine classic
|
|
51
|
-
* meta-tools on either plan.
|
|
48
|
+
* execute_code. Dynamic Workers require the Workers Paid plan.
|
|
52
49
|
*/
|
|
53
|
-
LOADER
|
|
50
|
+
LOADER: WorkerLoader;
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
function build(env: Env) {
|
|
57
54
|
return createConnecta({
|
|
58
55
|
publicUrl: env.PUBLIC_URL,
|
|
59
56
|
storage: cloudflareKvStorage(env.CONNECTA_KV),
|
|
60
|
-
|
|
61
|
-
// mode and the seven-tool code-first surface with it; leaving it absent
|
|
62
|
-
// keeps this deployment free-tier compatible on the classic surface.
|
|
63
|
-
...(env.LOADER
|
|
64
|
-
? { executor: new DynamicWorkerExecutor({ loader: env.LOADER }) }
|
|
65
|
-
: {}),
|
|
57
|
+
executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
|
|
66
58
|
auth: [
|
|
67
59
|
// Multiple credentials may identify callers in one deployment. Every
|
|
68
60
|
// admitted caller reaches this deployment's deliberate connector set.
|
|
@@ -16,11 +16,8 @@
|
|
|
16
16
|
// Create with `wrangler kv namespace create CONNECTA_KV` and paste the id.
|
|
17
17
|
"kv_namespaces": [
|
|
18
18
|
{ "binding": "CONNECTA_KV", "id": "replace-with-kv-namespace-id" }
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
// serves the seven-tool code-first surface. Leave it absent for a free-tier
|
|
24
|
-
// deployment with the nine classic meta-tools.
|
|
25
|
-
// "worker_loaders": [{ "binding": "LOADER" }]
|
|
21
|
+
// Required by execute_code; Dynamic Workers use the Workers Paid plan.
|
|
22
|
+
"worker_loaders": [{ "binding": "LOADER" }]
|
|
26
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.",
|