@zackbart/connecta 0.6.0 → 0.6.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 +147 -0
- package/README.md +159 -308
- package/dist/auth/clerk.d.ts +2 -2
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/credential-health.d.ts +13 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +32 -16
- package/dist/credential-health.js.map +1 -1
- package/dist/credentials.d.ts +35 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +42 -0
- package/dist/credentials.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -11
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -3
- package/dist/server.js.map +1 -1
- package/dist/skills.js +2 -2
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +30 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +77 -30
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/auth/clerk.ts +2 -2
- package/src/credential-health.ts +35 -18
- package/src/credentials.ts +71 -1
- package/src/index.ts +37 -11
- package/src/server.ts +17 -4
- package/src/skills.ts +2 -2
- package/src/types.ts +11 -0
- package/src/ui.ts +78 -31
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.6.1 — 2026-07-26
|
|
6
|
+
|
|
7
|
+
A patch release: three bug fixes and a documentation overhaul. No new
|
|
8
|
+
configuration, no new exported types, no change to the published API surface —
|
|
9
|
+
`ConnectaConfig` and every type are byte-identical to 0.6.0. Nothing here is
|
|
10
|
+
breaking. But two of the fixes *withdraw* behavior a deployment could have been
|
|
11
|
+
relying on by accident, and in both cases withdrawing it is what the fix is, not
|
|
12
|
+
a decision taken alongside it:
|
|
13
|
+
|
|
14
|
+
- **A `uiAuth.signInUrl` or `signUpUrl` that is not an absolute `https:` URL is
|
|
15
|
+
now dropped** rather than handed to `Clerk.load` (issue #56). Relative paths,
|
|
16
|
+
`http:`, protocol-relative, `javascript:` and `data:` values all fall together,
|
|
17
|
+
under the same gate `frontendApiUrl` has always passed. A rejected value reaches no part of the rendered page: the key is
|
|
18
|
+
simply absent from `/ui`'s inline `AUTH` object, so Clerk falls back to its own
|
|
19
|
+
default exactly as it does for an unset field, and `/ui` still renders and
|
|
20
|
+
still signs operators in. Construction logs one warning naming each dropped
|
|
21
|
+
field. What belongs in these fields is a hosted Account Portal address
|
|
22
|
+
(`https://accounts.<domain>`, `https://<slug>.accounts.dev`), so a deployment
|
|
23
|
+
that noticed this at all was pointing operators at a sign-in page connecta does
|
|
24
|
+
not host.
|
|
25
|
+
- **The declared credential shape now picks the credential test hook, on every
|
|
26
|
+
surface** (issue #55). Two of the four shape/hook combinations change behavior.
|
|
27
|
+
A **single-value `credential` declaring both hooks** now runs
|
|
28
|
+
`testCredential(value)` with the raw string, where the test route previously
|
|
29
|
+
preferred `testCredentials` and handed it the vault's reserved `{ value }` map
|
|
30
|
+
— the single-value hook now receives the string it was written to expect
|
|
31
|
+
instead of a one-entry map named after a storage detail. A **single-value
|
|
32
|
+
`credential` declaring only `testCredentials`** is no longer tested or probed
|
|
33
|
+
at all: /ui renders no Test button, a direct POST answers 400 naming the
|
|
34
|
+
mismatch, `createConnecta` warns at construction, and the liveness sweep added
|
|
35
|
+
in 0.6.0 reports it `not_checkable` instead of calling a hook with a shape its
|
|
36
|
+
connector never declared. Both of those are the fallback order that *was* the
|
|
37
|
+
bug, so there is no version of this fix that preserves them. Named `fields`
|
|
38
|
+
with `testCredentials`, or with both hooks, behave exactly as they did.
|
|
39
|
+
|
|
40
|
+
One textual delta reaches every deployment and no configuration turns it off, so
|
|
41
|
+
anything snapshotting agent-facing error text will diff: `resolveSkill`'s two
|
|
42
|
+
`Available:` branches now say `Available skills:` like the other three (issue
|
|
43
|
+
#50).
|
|
44
|
+
|
|
45
|
+
The docs are substantially reorganized, and one of the changes ships to npm:
|
|
46
|
+
readers of the package page get a completely different README. See Changed. The
|
|
47
|
+
next intentional breaking release stays reserved for issue #28.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **The README is rewritten around the problem connecta solves** (345 lines to
|
|
52
|
+
196). It had become a condensed reference manual — roughly 60% code blocks and
|
|
53
|
+
option tables, every one of them duplicated in `docs/documentation.md` or
|
|
54
|
+
`examples/` — with a single paragraph explaining why connecta exists. npm
|
|
55
|
+
readers see only this file, so it was the worst place for the duplication and
|
|
56
|
+
the best place for the argument. It now runs as prose: the context-window and
|
|
57
|
+
per-client configuration costs of connecting an agent to N MCP servers, what
|
|
58
|
+
connecta does about them, why it is shaped the way it is (config as code, a
|
|
59
|
+
server-side credential vault with liveness checks, fail-closed read-only,
|
|
60
|
+
toolkits and who is admitted to them, payload-free activity, a read-only
|
|
61
|
+
operator dashboard), when *not* to use it, one minimal Node quickstart, and
|
|
62
|
+
links out. Removed from it and unchanged in `docs/` and `examples/`: the
|
|
63
|
+
nine-row signature-level meta-tool table (the nine names now appear in prose
|
|
64
|
+
with their purposes), the Worker quickstart, and the toolkits, code-mode,
|
|
65
|
+
credentials, activity and branding blocks with their option semantics.
|
|
66
|
+
**Links out of the README are now absolute GitHub URLs**, because `docs/` does
|
|
67
|
+
not ship in the package and relative links are dead on npmjs.com. This is the
|
|
68
|
+
only change in the release that reaches npm as content rather than code.
|
|
69
|
+
- **`docs/design.md` is retired into a new `docs/decisions.md`.** design.md was
|
|
70
|
+
mostly a worse copy of documentation.md, and it had gone stale in at least one
|
|
71
|
+
load-bearing way: it claimed the Clerk adapter passes `authorizedParties:
|
|
72
|
+
[connectaOrigin]` to `authenticateRequest`. It does not, and must not — OAuth
|
|
73
|
+
access tokens may be JWTs with no `azp` claim, and Clerk rejects `azp ===
|
|
74
|
+
undefined` when that option is set, so passing it would reject every MCP
|
|
75
|
+
client; the `azp` pin is applied by hand after verification, for session tokens
|
|
76
|
+
only. decisions.md records that as a **rejected alternative** rather than
|
|
77
|
+
repeating the wrong version. It answers two questions documentation.md does
|
|
78
|
+
not: "may I build X?" (non-goals, rejected alternatives) and "must my change
|
|
79
|
+
preserve Y?" (invariants) — collecting the invariants documentation.md states
|
|
80
|
+
but never gathers as pointers rather than duplicated prose, alongside the four
|
|
81
|
+
that lived only in design.md: no runtime admin, nothing request-bound surviving
|
|
82
|
+
a request, single tenant, and fail-closed read-only. Two facts moved into
|
|
83
|
+
documentation.md §13 instead, next to the code they explain. Inbound references
|
|
84
|
+
in the README, documentation.md and the Docker example follow; **any external
|
|
85
|
+
link to `docs/design.md` is now dead**, and the 0.x CHANGELOG mentions of it
|
|
86
|
+
stay as historical record. The credential-test invariant added by issue #55
|
|
87
|
+
also gained the qualifier §17 already carried: a connector whose shape and hook
|
|
88
|
+
mismatch carries no verdict *from a credential hook*, but is still probed
|
|
89
|
+
through `status()` if it declares one, since that question never involves the
|
|
90
|
+
shape.
|
|
91
|
+
- **CLAUDE.md is an agent brief rather than a policy stub.** It now states what
|
|
92
|
+
gates "done" (`npm run check`, and what that runs), the two-document map
|
|
93
|
+
(documentation.md as reference manual, decisions.md as non-goals and
|
|
94
|
+
invariants) with the warning that its section numbers are linked from source
|
|
95
|
+
comments, the two CI-enforced boundaries that are invisible from inside a
|
|
96
|
+
single file (import-graph purity, the published surface) as where-new-code-goes
|
|
97
|
+
guidance, the `WORKERS_SUITES` allowlist trap that silently skips a portable
|
|
98
|
+
suite left out of it, and the commit, CHANGELOG and release conventions. The
|
|
99
|
+
roadmap-lives-in-GitHub-issues policy is unchanged, verbatim.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **One rule decides how a credential is tested, and three copies of it no
|
|
104
|
+
longer disagree** (issue #55). `/ui` offered a Test button from the mere
|
|
105
|
+
presence of a hook (`testCredential || testCredentials`); the test route made
|
|
106
|
+
its own different choice — prefer `testCredentials`, else `testCredential` on
|
|
107
|
+
the vault's reserved `value` field; and 0.6.0's credential-health prober added
|
|
108
|
+
a third copy of that same preference order. They disagreed in both mismatch
|
|
109
|
+
shapes, and the visible bug was the ugliest one: a connector declaring named
|
|
110
|
+
`credential.fields` with only `testCredential` answered **409 "configure the
|
|
111
|
+
credential before testing it" on a fully configured credential**, blaming the
|
|
112
|
+
operator for connecta's own hook selection. `credentialTestRule` in
|
|
113
|
+
`src/credentials.ts` is now the single source of truth — `buildUiData`'s
|
|
114
|
+
`testable` flag, the test route's hook selection, the credential-health
|
|
115
|
+
prober's `testHookFor`/`isCheckableConnector`, and a new construction-time
|
|
116
|
+
warning all read it, so they cannot drift apart again. **The declared shape
|
|
117
|
+
picks the hook and the other one is never substituted**: named fields are
|
|
118
|
+
tested by `testCredentials` with the whole set, a single value by
|
|
119
|
+
`testCredential` with the raw string. A connector implementing only the
|
|
120
|
+
mismatched hook is not testable rather than testable-by-accident — no button, a
|
|
121
|
+
400 sharing its wording with the boot warning, and `not_checkable` to the
|
|
122
|
+
prober, which carries no verdict rather than an invented one (handing
|
|
123
|
+
`testCredential` a `value` that named fields never wrote would test the empty
|
|
124
|
+
string and record a confident `auth_required` about a credential nothing
|
|
125
|
+
examined). `status()` is unaffected: a mismatched connector that implements it
|
|
126
|
+
is still probed through it. The two behavior deltas this produces are stated
|
|
127
|
+
above. Recorded as an invariant in `docs/decisions.md`, and stated in
|
|
128
|
+
documentation.md §4, §7, §14 and §17.
|
|
129
|
+
- **`uiAuth.signInUrl` and `signUpUrl` are gated like every other URL that
|
|
130
|
+
reaches the browser** (issue #56). They were the residual — operator config
|
|
131
|
+
that arrives in the page as a *navigation target* rather than an attribute, so
|
|
132
|
+
no gate covered them and §14's URL-position invariant had to be read with an
|
|
133
|
+
exception beside it. Both are serialized into `/ui`'s inline `AUTH` object and
|
|
134
|
+
handed to `Clerk.load`, which navigates to them when an operator signs in. They
|
|
135
|
+
now pass the same gate `frontendApiUrl` does, so `isSafeScriptSrcUrl` — named
|
|
136
|
+
for the one position it used to guard — becomes `isSafeHttpsUrl`, one predicate
|
|
137
|
+
for all three `uiAuth` URLs. Absolute `https:` only, for the typed fields as
|
|
138
|
+
for the derived one: the loose carve-outs buy nothing real, since `http:` would
|
|
139
|
+
carry a sign-in over cleartext and a path relative to this origin is
|
|
140
|
+
meaningless because connecta hosts no sign-in page of its own. The drop warning
|
|
141
|
+
decides "did the operator mean to supply this?" through the same `isSetUrlValue`
|
|
142
|
+
helper `branding` uses, so a blank-string or falsy non-string value cannot warn
|
|
143
|
+
on one path and stay silent on the other. **§14 now states the invariant over
|
|
144
|
+
every URL position — attribute and navigation target — with no carve-out left.**
|
|
145
|
+
- **`resolveSkill` enumerates its list under one label** (issue #50). Three
|
|
146
|
+
branches said `Available skills:` and two said `Available:`, so which one an
|
|
147
|
+
agent saw depended on the branch it hit rather than on any difference in
|
|
148
|
+
meaning. Agent-facing error text is interface, and two labels read as two
|
|
149
|
+
concepts where there is one. Both stragglers now say `Available skills:`, and a
|
|
150
|
+
test walks all five error branches so reverting any one of them fails.
|
|
151
|
+
|
|
5
152
|
## 0.6.0 — 2026-07-26
|
|
6
153
|
|
|
7
154
|
A feature release that makes two of 0.5.0's mechanisms protective rather than
|
package/README.md
CHANGED
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
One MCP
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
One MCP endpoint in front of all your connectors. Agents see nine tools instead
|
|
6
|
+
of hundreds, and each client is configured once instead of once per integration.
|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
An agent connected to N MCP servers pays for all N before it does anything. Every
|
|
11
|
+
server's tool list is injected into the context window at connect time — hundreds
|
|
12
|
+
of definitions and their schemas, nearly all irrelevant to the task actually at
|
|
13
|
+
hand. That budget is spent whether the model calls one of them or none, and it
|
|
14
|
+
comes out of the same window the work needs.
|
|
15
|
+
|
|
16
|
+
The second cost is configuration. Every client — Claude, Cursor, whatever comes
|
|
17
|
+
next — has to be pointed at each server separately, with its own auth for each.
|
|
18
|
+
Adding an integration means touching every client; rotating one token means
|
|
19
|
+
finding every place it was pasted.
|
|
20
|
+
|
|
21
|
+
## What connecta is
|
|
22
|
+
|
|
23
|
+
One MCP endpoint you deploy — Cloudflare Worker, Node, or Docker — that
|
|
24
|
+
aggregates your downstream connectors behind a fixed set of **nine meta-tools**.
|
|
25
|
+
A connector is either a remote MCP server that connecta proxies, or a plain HTTP
|
|
26
|
+
API with hand-written tool definitions and a fetch handler.
|
|
8
27
|
|
|
9
28
|
```
|
|
10
29
|
┌── remoteMcp("notion") → mcp.notion.com
|
|
@@ -13,14 +32,96 @@ Claude / Cursor ── MCP ──▶ connecta ───┼── remoteMcp("lin
|
|
|
13
32
|
└── api("internal") → fetch(...)
|
|
14
33
|
```
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
Rather than receiving every tool up front, the agent discovers what it needs.
|
|
36
|
+
`search_tools` returns ranked matches for a query and `describe_tools` returns
|
|
37
|
+
schemas — compact by default, raw JSON Schema on request — for only the
|
|
38
|
+
addresses it is about to call. `call_tool`,
|
|
39
|
+
`batch_call`, and `call_destructive_tool` invoke them by address
|
|
40
|
+
(`<connectorId>.<toolName>`). `list_connectors` reports what exists and whether
|
|
41
|
+
it is reachable, `authorize_connector` starts a downstream OAuth flow,
|
|
42
|
+
`get_result` pages through a result too large to return inline, and `skills`
|
|
43
|
+
hands the model a short guide to choosing among the rest.
|
|
44
|
+
|
|
45
|
+
That is the entire surface. The agent's context holds nine tool definitions
|
|
46
|
+
whether ten tools sit behind them or a thousand — and the client holds one URL
|
|
47
|
+
and one credential, no matter how many services that URL fans out to.
|
|
48
|
+
|
|
49
|
+
An optional tenth meta-tool, `execute_code`, runs model-written async JavaScript
|
|
50
|
+
in a sandbox with no network, filesystem, or environment access — only the
|
|
51
|
+
explicitly read-only tools as callable globals — turning a loop, a join across
|
|
52
|
+
connectors, or a filter over a large response into one round trip instead of a
|
|
53
|
+
dozen. Configure no `executor` and connecta is exactly the nine-tool server.
|
|
54
|
+
|
|
55
|
+
## Why it's shaped this way
|
|
56
|
+
|
|
57
|
+
**Config as code, one deployment per tenant.** Connectors are declared in
|
|
58
|
+
TypeScript. Adding one is a code change and a deploy — no database of
|
|
59
|
+
integrations, no registration API, no runtime admin. A deployment is a small
|
|
60
|
+
config file you can read in one sitting and review in a pull request, not a
|
|
61
|
+
platform to administer.
|
|
62
|
+
|
|
63
|
+
**Credentials stay server-side.** Downstream tokens live in an AES-GCM encrypted
|
|
64
|
+
vault over the deployment's own storage, with the key held outside it. A
|
|
65
|
+
connector reaches its own credential through `ctx.credential`; `/ui`, the
|
|
66
|
+
meta-tools, and the code sandbox only ever see masked metadata. Rotating a token
|
|
67
|
+
is an operator action rather than a redeploy — though writing to the vault is
|
|
68
|
+
deliberately narrower than everything else, requiring a Clerk-authenticated
|
|
69
|
+
operator on a same-origin request, so a bearer-only deployment cannot administer
|
|
70
|
+
credentials from the browser. Which tools exist is still code either way.
|
|
71
|
+
|
|
72
|
+
**Read-only is fail-closed.** Only tools explicitly annotated `readOnlyHint:
|
|
73
|
+
true` are reachable through `call_tool`, `batch_call`, and the sandbox. Missing,
|
|
74
|
+
false, or contradictory annotations do not get the benefit of the doubt: they
|
|
75
|
+
require `call_destructive_tool`, which is itself annotated so the MCP host can
|
|
76
|
+
put the question to a human. Connecta makes the boundary visible; approval is
|
|
77
|
+
the host's job.
|
|
78
|
+
|
|
79
|
+
**Toolkits scope what a team sees.** A deployment belongs to an org; a toolkit
|
|
80
|
+
is a named view over its registry for one group inside that org — support sees
|
|
81
|
+
Zendesk and Notion, exec also sees Gmail. A client selects one with
|
|
82
|
+
`?toolkit=support` on the MCP URL, and a credential can be bound so it opens
|
|
83
|
+
that view and nothing else. Inside a scoped session an out-of-scope address
|
|
84
|
+
fails exactly as a nonexistent one does. Two teams, one deployment. Who gets in
|
|
85
|
+
at all is the prior question: a static bearer token, or Clerk — where
|
|
86
|
+
`allowedDomains: ["acme.com"]` admits anyone whose verified primary email is on
|
|
87
|
+
your domain without enumerating users, and a `gate` hook handles what a domain
|
|
88
|
+
rule cannot express. Both fail closed, and each one configured must pass.
|
|
89
|
+
|
|
90
|
+
**Activity records the fact, not the payload.** The optional activity store logs
|
|
91
|
+
which resolved tool ran, for whom, and how it went — never arguments, results,
|
|
92
|
+
generated code, search text, or raw error messages. The exclusion is structural
|
|
93
|
+
rather than a redaction pass: the event type has nowhere to put a payload, which
|
|
94
|
+
is what keeps an operations log from becoming something worth stealing.
|
|
95
|
+
|
|
96
|
+
**An operator dashboard that can only look.** `GET /ui` is a read-only status
|
|
97
|
+
page with no build step: connector health, tool counts, downstream authorization
|
|
98
|
+
links, credential controls, and an activity tab when a store is configured. It
|
|
99
|
+
displays state and administers credentials; it cannot change what an agent is
|
|
100
|
+
allowed to call. Credentials connecta stores are also probed for liveness
|
|
101
|
+
proactively — using each connector's own test or status hook, never a downstream
|
|
102
|
+
tool call — so a dead token surfaces as `auth_required` with the URL to open on
|
|
103
|
+
`/ui` and in `list_connectors` before an agent's real call trips over it.
|
|
104
|
+
|
|
105
|
+
## When not to use it
|
|
106
|
+
|
|
107
|
+
Connecta is deliberately small, and declines several tempting shapes.
|
|
108
|
+
It is **not multi-tenant** — one deployment is one tenant, with one registry and
|
|
109
|
+
one credential store, and toolkits are scoped views rather than tenants. There
|
|
110
|
+
is **no policy engine**, no approvals, and no pauses — access decisions are
|
|
111
|
+
fixed ones connecta already knows how to answer (is this tool read-only, may
|
|
112
|
+
this credential open this toolkit), not rules you author. There is **no runtime
|
|
113
|
+
administration** — you cannot add a connector from a browser or an API. It
|
|
114
|
+
aggregates **tools only**, not MCP resources or prompts, and it will not ingest
|
|
115
|
+
a **GraphQL** schema, because generating hundreds of low-quality tool
|
|
116
|
+
definitions is the problem the nine meta-tools exist to solve. OpenAPI is a
|
|
117
|
+
softer no: not built in today, not refused either, and tracked as
|
|
118
|
+
[issue #26](https://github.com/zackbart/connecta/issues/26). If you want a
|
|
119
|
+
hosted multi-tenant integration platform with an approval workflow, this is the
|
|
120
|
+
wrong shape; the
|
|
121
|
+
[non-goals](https://github.com/zackbart/connecta/blob/main/docs/decisions.md#non-goals)
|
|
122
|
+
say so at more length.
|
|
123
|
+
|
|
124
|
+
## Getting started
|
|
24
125
|
|
|
25
126
|
Node deployments require Node.js 20.9 or newer.
|
|
26
127
|
|
|
@@ -28,318 +129,68 @@ Node deployments require Node.js 20.9 or newer.
|
|
|
28
129
|
npm install @zackbart/connecta
|
|
29
130
|
```
|
|
30
131
|
|
|
31
|
-
|
|
32
|
-
optional packages a deployment uses—for example:
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
npm install @clerk/backend # optional Clerk auth adapter
|
|
36
|
-
npm install quickjs-emscripten # optional Node code-mode executor
|
|
37
|
-
npm install @cloudflare/codemode # optional Worker code-mode executor
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## The nine meta-tools
|
|
41
|
-
|
|
42
|
-
A tool **address** is `<connectorId>.<toolName>` (e.g. `notion.search`).
|
|
43
|
-
|
|
44
|
-
| Tool | Input | Returns |
|
|
45
|
-
| --- | --- | --- |
|
|
46
|
-
| `list_connectors` | `{ probe? }` | live (`probe: true`, default) or cached health, tool count, recent real-call observations, and the last proactive credential check |
|
|
47
|
-
| `skills` | `{ name? }` | lists or fetches the concise `usage` guide for choosing among the meta-tools, plus any operator-authored per-connector guide (`connector:<connectorId>`) |
|
|
48
|
-
| `search_tools` | `{ query?, connector?, limit?, offset?, fullDescriptions?, includeSchemas? }` | ranked, paginated matches; optionally includes compact/raw schemas to remove a round trip |
|
|
49
|
-
| `describe_tools` | `{ addresses[], format?, fullDescriptions? }` | names, descriptions, input/output schemas, and behavior annotations |
|
|
50
|
-
| `call_tool` | `{ address, args?, fields?, resultMode?, timeoutMs?, maxRetries?, diagnostics? }` | invokes only tools explicitly annotated `readOnlyHint: true` |
|
|
51
|
-
| `call_destructive_tool` | same as `call_tool` | invokes unannotated, write-capable, or destructive tools through a host-visible approval boundary |
|
|
52
|
-
| `authorize_connector` | `{ connector, force? }` | starts (or with `force`, restarts) the downstream OAuth flow; returns the `authorizationUrl` to open |
|
|
53
|
-
| `get_result` | `{ id, offset?, maxBytes? }` | a byte-slice page of a truncated result — `{ text, offset, nextOffset?, totalBytes }`; `maxBytes` is a whole number of bytes >= 1 and `offset` a whole number of bytes >= 0, aligned back to a character boundary and echoed as the `offset` served |
|
|
54
|
-
| `batch_call` | `{ calls, resultMode?, timeoutMs?, maxRetries? }` | 1–10 parallel calls sharing request-scoped clients, with attempts/timing/errors |
|
|
55
|
-
| `execute_code` *(optional)* | `{ code }` | result + logs of bounded async JS orchestration over explicitly read-only tools; registered only when an `executor` is configured |
|
|
56
|
-
|
|
57
|
-
## Package vs. deployments
|
|
58
|
-
|
|
59
|
-
This repository owns the reusable package. Each deployment should be a small,
|
|
60
|
-
separate Worker project that pins an exact package version and owns only its
|
|
61
|
-
connector configuration, auth policy, domain, bindings, migrations, and secrets.
|
|
62
|
-
[`examples/worker/`](./examples/worker/) is the starting template.
|
|
63
|
-
|
|
64
|
-
## Quickstart — Cloudflare Worker
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
|
|
68
|
-
import { bearerToken, createConnecta, remoteMcp } from "@zackbart/connecta";
|
|
69
|
-
import { clerkAuth } from "@zackbart/connecta/auth/clerk";
|
|
70
|
-
import { cloudflareKvStorage } from "./cloudflare-kv.js";
|
|
71
|
-
|
|
72
|
-
const build = (env: Env) =>
|
|
73
|
-
createConnecta({
|
|
74
|
-
publicUrl: env.PUBLIC_URL,
|
|
75
|
-
storage: cloudflareKvStorage(env.CONNECTA_KV),
|
|
76
|
-
// Code mode (optional): needs a `worker_loaders` binding in wrangler.jsonc.
|
|
77
|
-
executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
|
|
78
|
-
auth: [
|
|
79
|
-
bearerToken(env.CONNECTA_TOKEN),
|
|
80
|
-
clerkAuth({
|
|
81
|
-
publishableKey: env.CLERK_PUBLISHABLE_KEY,
|
|
82
|
-
secretKey: env.CLERK_SECRET_KEY,
|
|
83
|
-
publicUrl: env.PUBLIC_URL,
|
|
84
|
-
// allowedDomains: ["acme.com"], // only your org's verified emails
|
|
85
|
-
}),
|
|
86
|
-
],
|
|
87
|
-
connectors: [
|
|
88
|
-
remoteMcp("notion", {
|
|
89
|
-
url: "https://mcp.notion.com/mcp",
|
|
90
|
-
auth: { type: "headers", headers: { Authorization: `Bearer ${env.NOTION_TOKEN}` } },
|
|
91
|
-
}),
|
|
92
|
-
],
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Lazy per-isolate singleton: keeps only serializable tool/catalog data warm.
|
|
96
|
-
let connecta: ReturnType<typeof build> | undefined;
|
|
97
|
-
|
|
98
|
-
export default {
|
|
99
|
-
fetch(request: Request, env: Env): Promise<Response> {
|
|
100
|
-
connecta ??= build(env);
|
|
101
|
-
return connecta.fetch(request);
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Deployable example: [`examples/worker/`](./examples/worker/).
|
|
107
|
-
Its Cloudflare KV and D1 implementations are deployment-owned examples over the
|
|
108
|
-
generic `KVStorage` and `ActivityStore` contracts; they are not package exports.
|
|
109
|
-
|
|
110
|
-
`clerkAuth` is an optional adapter. Install `@clerk/backend` and import it from
|
|
111
|
-
`@zackbart/connecta/auth/clerk` only in deployments that use Clerk. Other
|
|
112
|
-
identity providers can implement the exported `InboundAuth` interface.
|
|
113
|
-
|
|
114
|
-
## Quickstart — Node
|
|
132
|
+
A minimal server with one hand-written connector:
|
|
115
133
|
|
|
116
134
|
```ts
|
|
117
135
|
import { api, bearerToken, createConnecta } from "@zackbart/connecta";
|
|
118
136
|
import { fileStorage, listen } from "@zackbart/connecta/node";
|
|
119
|
-
import { quickJsExecutor } from "@zackbart/connecta/quickjs";
|
|
120
137
|
|
|
121
138
|
const connecta = createConnecta({
|
|
122
139
|
storage: fileStorage("./.connecta-state.json"), // or memoryStorage()
|
|
123
|
-
auth: bearerToken(process.env.CONNECTA_TOKEN
|
|
124
|
-
executor: quickJsExecutor(), // code mode (optional): QuickJS/WASM sandbox
|
|
140
|
+
auth: bearerToken(process.env.CONNECTA_TOKEN ?? "dev-token"),
|
|
125
141
|
connectors: [
|
|
126
142
|
api("time", {
|
|
127
|
-
description: "Time —
|
|
128
|
-
tools: [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
listen(connecta, 8787); // http://localhost:8787/mcp
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Example: [`examples/node/`](./examples/node/). Docker (single-service compose
|
|
143
|
-
stack): [`examples/docker/`](./examples/docker/).
|
|
144
|
-
|
|
145
|
-
## Toolkits — one deployment, many teams
|
|
146
|
-
|
|
147
|
-
A deployment belongs to an org. Optional **toolkits** give each group of team
|
|
148
|
-
members its own scoped view of the same registry — bound to that group's
|
|
149
|
-
credential — so different teams no longer need separate deployments:
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
createConnecta({
|
|
153
|
-
connectors: [zendesk, notion, gmail],
|
|
154
|
-
auth: [
|
|
155
|
-
// Each team's credential is bound to that team's view.
|
|
156
|
-
bearerToken(env.SUPPORT_TOKEN, { subjectId: "support", toolkits: ["support"] }),
|
|
157
|
-
bearerToken(env.EXEC_TOKEN, { subjectId: "exec", toolkits: ["exec"] }),
|
|
158
|
-
// The operator's: every view, plus /ui — declared, not left unbound.
|
|
159
|
-
bearerToken(env.OPS_TOKEN, {
|
|
160
|
-
subjectId: "ops",
|
|
161
|
-
toolkits: ["support", "exec"],
|
|
162
|
-
unscoped: true,
|
|
143
|
+
description: "Time — current timestamp",
|
|
144
|
+
tools: [
|
|
145
|
+
{
|
|
146
|
+
name: "get_now",
|
|
147
|
+
description: "Return the current time as an ISO 8601 timestamp.",
|
|
148
|
+
inputSchema: { type: "object", properties: {} },
|
|
149
|
+
annotations: { readOnlyHint: true },
|
|
150
|
+
handler: async () => ({ now: new Date().toISOString() }),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
163
153
|
}),
|
|
164
154
|
],
|
|
165
|
-
toolkits: {
|
|
166
|
-
support: { connectors: ["zendesk", "notion"] },
|
|
167
|
-
exec: {
|
|
168
|
-
connectors: ["zendesk", "notion", "gmail"],
|
|
169
|
-
excludeTools: ["gmail.send_message"], // finer grain than a connector id
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
155
|
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
A client picks one at connect time: `https://…/mcp?toolkit=support`. Inside a
|
|
176
|
-
scoped session **every** meta-tool — search, describe, call, batch, skills,
|
|
177
|
-
authorize, `get_result`, and `execute_code` host calls — behaves as if
|
|
178
|
-
out-of-scope connectors and tools do not exist, and an out-of-scope address
|
|
179
|
-
fails identically to a nonexistent one. No `?toolkit=` ⇒ the full registry, as
|
|
180
|
-
before; an unknown name is an error, never a silent fallback.
|
|
181
|
-
|
|
182
|
-
`toolkits: [...]` on an auth adapter **binds** a credential to its views: the
|
|
183
|
-
support token cannot open `?toolkit=exec`, cannot connect unscoped (unless it also
|
|
184
|
-
passes `unscoped: true`), and cannot read the deployment-wide operator surfaces.
|
|
185
|
-
Refusal is a flat 403 at connect time, identical whether the toolkit is another
|
|
186
|
-
team's or does not exist at all — a mapping, not a policy engine. Leave `toolkits`
|
|
187
|
-
off and that credential keeps today's self-service selection. Details:
|
|
188
|
-
[docs §16](./docs/documentation.md#16-toolkits-scoped-views).
|
|
189
|
-
|
|
190
|
-
## Code mode
|
|
191
156
|
|
|
192
|
-
|
|
193
|
-
making one `call_tool` round trip per step — loops, joins across connectors,
|
|
194
|
-
filtering big responses down in-sandbox before they hit the context window:
|
|
195
|
-
|
|
196
|
-
```js
|
|
197
|
-
async () => {
|
|
198
|
-
const pages = await notion.search({ query: "roadmap" });
|
|
199
|
-
return pages.results.map((p) => p.title);
|
|
200
|
-
}
|
|
157
|
+
listen(connecta, 8787); // MCP at http://localhost:8787/mcp, status at /ui
|
|
201
158
|
```
|
|
202
159
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
connecta
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`ctx.credential.get()`, `get(name)`, or `getAll()`; they are never returned by
|
|
223
|
-
`/ui`, MCP tools, or code mode.
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
api("example", {
|
|
227
|
-
description: "Example — authenticated API",
|
|
228
|
-
credential: {
|
|
229
|
-
label: "API token",
|
|
230
|
-
description: "Token used for outbound Example API requests.",
|
|
231
|
-
},
|
|
232
|
-
tools: [{
|
|
233
|
-
name: "get_profile",
|
|
234
|
-
description: "Get the authenticated Example profile.",
|
|
235
|
-
inputSchema: { type: "object", properties: {} },
|
|
236
|
-
annotations: { readOnlyHint: true },
|
|
237
|
-
handler: async (_args, ctx) => {
|
|
238
|
-
const token = await ctx.credential?.get();
|
|
239
|
-
if (!token) throw new Error("Example API token is not configured.");
|
|
240
|
-
return fetch("https://api.example.com/profile", {
|
|
241
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
242
|
-
}).then((response) => response.json());
|
|
243
|
-
},
|
|
244
|
-
}],
|
|
245
|
-
})
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
Set `credentialEncryptionKey` on `createConnecta` to a base64-encoded 32-byte
|
|
249
|
-
key held in the runtime's secret store (`openssl rand -base64 32`) — a connector
|
|
250
|
-
that declares a credential without one fails at construction rather than booting
|
|
251
|
-
with an unusable vault. Credential mutation routes require the configured Clerk
|
|
252
|
-
provider and a same-origin browser request; the static inbound bearer cannot
|
|
253
|
-
administer the vault.
|
|
254
|
-
|
|
255
|
-
Connecta does not bundle service-specific HTTP API connectors. Package consumers
|
|
256
|
-
define them with `api()` (or implement `Connector` directly), keeping endpoint,
|
|
257
|
-
credential, and tool choices in the consuming project.
|
|
258
|
-
|
|
259
|
-
## Credential health
|
|
260
|
-
|
|
261
|
-
Stored credentials are checked for liveness *before* an agent's call trips over a
|
|
262
|
-
dead one. Connecta asks each connector holding a credential it stores — a vault
|
|
263
|
-
credential, or a downstream-OAuth grant — whether that credential still works,
|
|
264
|
-
using the connector's own `testCredential(s)` or `status()` hook. No downstream
|
|
265
|
-
tool is ever called. A check that finds the credential *rejected* flips the
|
|
266
|
-
connector to `auth_required` in `list_connectors({ probe: false })` and on
|
|
267
|
-
`/ui`, with the URL to open; a later success (or re-authorizing) flips it back
|
|
268
|
-
with no restart. A check that merely failed to complete — a timeout, a 502 from
|
|
269
|
-
the status endpoint — is reported but never decides the status: it learned
|
|
270
|
-
nothing about the credential.
|
|
271
|
-
|
|
272
|
-
Checks are triggered two ways and share one rate limit — at most one per
|
|
273
|
-
connector per 15 minutes by default, four in flight, 30 s each:
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
// 1. Opportunistically, on authenticated traffic connecta already serves.
|
|
277
|
-
// On by default; nothing to wire. Disable with:
|
|
278
|
-
createConnecta({ connectors, credentialHealth: { onRequest: false } });
|
|
279
|
-
|
|
280
|
-
// 2. On a schedule you own — the core starts no timers, so this works the same
|
|
281
|
-
// on Workers (cron trigger) and Node.
|
|
282
|
-
async scheduled(_c, env, ctx) { ctx.waitUntil(build(env).checkCredentials()); }
|
|
283
|
-
setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Details: [docs §17](./docs/documentation.md#17-credential-health-proactive-liveness-checks).
|
|
287
|
-
|
|
288
|
-
## Payload-free tool activity
|
|
289
|
-
|
|
290
|
-
Connecta can record which resolved downstream tools were actually invoked
|
|
291
|
-
without storing their arguments, results, generated code, search text, or raw
|
|
292
|
-
errors. Supply a vendor-neutral `activity` store:
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
const events: ToolCallActivityEvent[] = [];
|
|
296
|
-
|
|
297
|
-
const connecta = createConnecta({
|
|
298
|
-
connectors,
|
|
299
|
-
activity: {
|
|
300
|
-
record(event) {
|
|
301
|
-
events.push(event);
|
|
302
|
-
},
|
|
303
|
-
async list({ limit }) {
|
|
304
|
-
return { events: events.slice(-limit).reverse() };
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
One final event is emitted for each resolved connector call made through
|
|
311
|
-
`call_tool`, `call_destructive_tool`, `batch_call`, or `execute_code`; retries
|
|
312
|
-
remain one event with an `attempts` count. Implementing `list` enables the
|
|
313
|
-
authenticated Activity tab in `/ui`; `activityReadGate` can narrow reads
|
|
314
|
-
further. Writes are best-effort and never change a tool result. Clerk calls
|
|
315
|
-
carry the Clerk user ID; shared bearer calls are honestly labeled as bearer
|
|
316
|
-
unless `bearerToken(secret, { subjectId })` assigns that credential a stable
|
|
317
|
-
subject. On Workers, pass `ctx` through to `connecta.fetch(request, env, ctx)`
|
|
318
|
-
so async writes settle on `waitUntil`.
|
|
319
|
-
[`examples/worker/src/d1-activity.ts`](./examples/worker/src/d1-activity.ts) is
|
|
320
|
-
a complete D1 implementation with keyset paging and a retention pass.
|
|
321
|
-
|
|
322
|
-
## Operator dashboard
|
|
323
|
-
|
|
324
|
-
`GET /ui` is a read-only dashboard with no build step: connector health and the
|
|
325
|
-
last credential check, tool
|
|
326
|
-
counts and descriptions with a client-side filter, downstream authorization
|
|
327
|
-
links, the credential controls above, and an Activity tab when an activity store
|
|
328
|
-
is configured. The shell is open because it carries no data; everything it shows
|
|
329
|
-
comes from `/ui/data`, behind the same auth gate as `/mcp`. With Clerk
|
|
330
|
-
configured it signs operators in through Clerk's hosted portal; a bearer-only
|
|
331
|
-
deployment falls back to a pasted token.
|
|
332
|
-
|
|
333
|
-
Every deployment-facing label and mark on `/ui` and the OAuth result pages comes
|
|
334
|
-
from `ConnectaConfig.branding` — `productName`, `ownerName`, their URLs,
|
|
335
|
-
`description`, `pageTitle`, `themeColor`, and `favicon` — each falling back to a
|
|
336
|
-
neutral Connecta default. Nothing about the operator is baked into the package.
|
|
160
|
+
Point an MCP client at `http://localhost:8787/mcp` with an
|
|
161
|
+
`Authorization: Bearer` header and it will see the nine meta-tools, with
|
|
162
|
+
`time.get_now` discoverable through `search_tools`.
|
|
163
|
+
|
|
164
|
+
Runnable deployments live in
|
|
165
|
+
[`examples/`](https://github.com/zackbart/connecta/tree/main/examples):
|
|
166
|
+
[`worker/`](https://github.com/zackbart/connecta/tree/main/examples/worker) is a
|
|
167
|
+
deployable Cloudflare Worker with KV and D1 adapters, and the template to copy
|
|
168
|
+
for a real deployment;
|
|
169
|
+
[`node/`](https://github.com/zackbart/connecta/tree/main/examples/node) adds
|
|
170
|
+
toolkits and code mode to the server above;
|
|
171
|
+
[`docker/`](https://github.com/zackbart/connecta/tree/main/examples/docker) is a
|
|
172
|
+
single-service compose stack. Anything beyond the core is installed only by the
|
|
173
|
+
deployments that use it: `@clerk/backend` and `quickjs-emscripten` are optional
|
|
174
|
+
peer dependencies, reached through the `/auth/clerk` and `/quickjs` subpaths,
|
|
175
|
+
and a Worker using code mode brings its own `@cloudflare/codemode`. Connecta
|
|
176
|
+
ships no service-specific connectors:
|
|
177
|
+
endpoint, credential, and tool choices stay in your project, declared with
|
|
178
|
+
`remoteMcp()` and `api()`.
|
|
337
179
|
|
|
338
180
|
## Learn more
|
|
339
181
|
|
|
340
|
-
- **[
|
|
341
|
-
the
|
|
342
|
-
OAuth, storage,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
182
|
+
- **[Documentation](https://github.com/zackbart/connecta/blob/main/docs/documentation.md)**
|
|
183
|
+
— the reference manual: architecture, connectors, inbound auth and downstream
|
|
184
|
+
OAuth, storage, Clerk setup, testing, and troubleshooting. Start with the
|
|
185
|
+
[meta-tools reference](https://github.com/zackbart/connecta/blob/main/docs/documentation.md#3-meta-tools-reference),
|
|
186
|
+
the [config options](https://github.com/zackbart/connecta/blob/main/docs/documentation.md#8-running-it),
|
|
187
|
+
[code mode](https://github.com/zackbart/connecta/blob/main/docs/documentation.md#13-code-mode-execute_code),
|
|
188
|
+
[toolkits](https://github.com/zackbart/connecta/blob/main/docs/documentation.md#16-toolkits-scoped-views),
|
|
189
|
+
or [credential health](https://github.com/zackbart/connecta/blob/main/docs/documentation.md#17-credential-health-proactive-liveness-checks).
|
|
190
|
+
- **[Decisions](https://github.com/zackbart/connecta/blob/main/docs/decisions.md)**
|
|
191
|
+
— what connecta refuses to be, which alternatives lost and why, and the
|
|
192
|
+
invariants a change must preserve.
|
|
193
|
+
- **[CHANGELOG](https://github.com/zackbart/connecta/blob/main/CHANGELOG.md)** —
|
|
194
|
+
what changed in each release.
|
|
195
|
+
- **[SECURITY](https://github.com/zackbart/connecta/blob/main/SECURITY.md)** —
|
|
196
|
+
supported versions and how to report a vulnerability.
|