@zackbart/connecta 0.10.1 → 0.10.2

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.
Files changed (69) hide show
  1. package/AGENTS.md +113 -0
  2. package/CHANGELOG.md +51 -0
  3. package/README.md +53 -9
  4. package/bin/connecta.mjs +272 -0
  5. package/dist/catalog-service.d.ts +39 -1
  6. package/dist/catalog-service.d.ts.map +1 -1
  7. package/dist/catalog-service.js +133 -11
  8. package/dist/catalog-service.js.map +1 -1
  9. package/dist/catalog.d.ts +17 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/catalog.js +113 -13
  12. package/dist/catalog.js.map +1 -1
  13. package/dist/execute.d.ts +45 -1
  14. package/dist/execute.d.ts.map +1 -1
  15. package/dist/execute.js +265 -68
  16. package/dist/execute.js.map +1 -1
  17. package/dist/invocation.d.ts.map +1 -1
  18. package/dist/invocation.js +1 -5
  19. package/dist/invocation.js.map +1 -1
  20. package/dist/meta-tools.d.ts +1 -0
  21. package/dist/meta-tools.d.ts.map +1 -1
  22. package/dist/meta-tools.js +410 -12
  23. package/dist/meta-tools.js.map +1 -1
  24. package/dist/skills.d.ts +1 -1
  25. package/dist/skills.d.ts.map +1 -1
  26. package/dist/skills.js +1 -1
  27. package/dist/tool-safety.d.ts +10 -0
  28. package/dist/tool-safety.d.ts.map +1 -0
  29. package/dist/tool-safety.js +12 -0
  30. package/dist/tool-safety.js.map +1 -0
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/documentation/architecture.md +7 -0
  34. package/documentation/auth.md +58 -0
  35. package/documentation/call-admission.md +7 -0
  36. package/documentation/code-first-exploration.md +292 -0
  37. package/documentation/code-mode.md +697 -0
  38. package/documentation/connector-guides.md +7 -0
  39. package/documentation/connectors.md +63 -0
  40. package/documentation/mcp-2026-07-28.md +46 -0
  41. package/documentation/meta-tools.md +167 -0
  42. package/documentation/operations.md +7 -0
  43. package/documentation/operator-ui.md +7 -0
  44. package/documentation/request-admission.md +7 -0
  45. package/documentation/storage-and-credentials.md +54 -0
  46. package/ethos.md +132 -0
  47. package/examples/node/README.md +53 -0
  48. package/examples/node/src/index.ts +73 -0
  49. package/examples/worker/README.md +160 -0
  50. package/examples/worker/src/cloudflare-kv.ts +43 -0
  51. package/examples/worker/src/d1-activity-row.ts +100 -0
  52. package/examples/worker/src/d1-activity.ts +144 -0
  53. package/examples/worker/src/index.ts +136 -0
  54. package/examples/worker/wrangler.jsonc +26 -0
  55. package/package.json +11 -1
  56. package/src/catalog-service.ts +177 -15
  57. package/src/catalog.ts +143 -12
  58. package/src/execute.ts +372 -96
  59. package/src/invocation.ts +1 -8
  60. package/src/meta-tools.ts +504 -11
  61. package/src/skills.ts +1 -1
  62. package/src/tool-safety.ts +15 -0
  63. package/src/version.ts +1 -1
  64. package/templates/node/.env.example +5 -0
  65. package/templates/node/AGENTS.md +19 -0
  66. package/templates/node/README.md +33 -0
  67. package/templates/node/package.json +23 -0
  68. package/templates/node/src/index.ts +43 -0
  69. package/templates/node/tsconfig.json +12 -0
@@ -0,0 +1,160 @@
1
+ # connecta — Cloudflare Worker example
2
+
3
+ A deployable Worker that aggregates a downstream remote MCP and an in-code HTTP
4
+ API connector, guarded by Clerk OAuth *and* a static bearer token, with state in
5
+ a KV namespace. One Wrangler binding turns on paid code mode, which is also what
6
+ selects the seven-tool code-first surface; the checked-in configuration deploys
7
+ without it, serving the nine classic meta-tools on the Workers Free plan.
8
+
9
+ This is also the **starting template for a deployment**: a real deployment
10
+ should be its own repository that pins an exact `@zackbart/connecta` version and
11
+ owns only its connector configuration, auth policy, domain, bindings,
12
+ migrations, and secrets. See [deployment architecture](../../documentation/operations.md).
13
+
14
+ ## Files
15
+
16
+ | File | What it is |
17
+ | --- | --- |
18
+ | `src/index.ts` | the Worker entrypoint — connector and auth configuration |
19
+ | `src/cloudflare-kv.ts` | `KVStorage` over Workers KV (deployment-owned, not a package export) |
20
+ | `src/d1-activity.ts` | `ActivityStore` over D1 (deployment-owned; see below) |
21
+ | `wrangler.jsonc` | Worker name, vars, bindings, `compatibility_flags` |
22
+
23
+ `cloudflare-kv.ts` and `d1-activity.ts` deliberately live here rather than in
24
+ the package: storage backends are deployment-owned, so the package ships only
25
+ the generic `KVStorage` and `ActivityStore` contracts. Workers KV is eventually
26
+ consistent across locations; use a strongly consistent `KVStorage` adapter when
27
+ OAuth disconnect, credential rotation, or access-token issuance/revocation must
28
+ become globally visible immediately.
29
+
30
+ ## Deploy
31
+
32
+ This example has no `package.json` of its own — it resolves the installed
33
+ package from the repository root.
34
+
35
+ ```sh
36
+ npm install # from the package root
37
+
38
+ wrangler kv namespace create CONNECTA_KV # paste the id into wrangler.jsonc
39
+
40
+ cd examples/worker
41
+ wrangler secret put SUPPORT_TOKEN # one headless client
42
+ wrangler secret put EXEC_TOKEN # another headless client
43
+ wrangler secret put CLERK_SECRET_KEY
44
+ wrangler secret put DOWNSTREAM_TOKEN
45
+ wrangler deploy
46
+ ```
47
+
48
+ `PUBLIC_URL` and `CLERK_PUBLISHABLE_KEY` are plain vars in `wrangler.jsonc`.
49
+ Enable Dynamic Client Registration on the Clerk instance (OAuth Applications →
50
+ DCR) so Claude/Cursor can self-register — full walkthrough in
51
+ [setting up Clerk](../../documentation/auth.md).
52
+
53
+ Then point an MCP client at `<PUBLIC_URL>/mcp`, and open `<PUBLIC_URL>/` for
54
+ Connections. Credentials is at `/credentials`, named MCP access tokens are at
55
+ `/tokens`, Activity is at `/activity`, and legacy `/ui` redirects to `/`.
56
+
57
+ ## Code mode
58
+
59
+ Code mode is a deploy-time opt-in because its Dynamic Worker sandbox requires
60
+ the [Workers Paid plan](https://developers.cloudflare.com/dynamic-workers/pricing/).
61
+ The Worker Loader binding is the switch; no TypeScript change or separate
62
+ environment variable is needed. Add this block to `wrangler.jsonc` (and a comma
63
+ after the preceding property):
64
+
65
+ ```jsonc
66
+ "worker_loaders": [{ "binding": "LOADER" }]
67
+ ```
68
+
69
+ `src/index.ts` detects `env.LOADER`, constructs `DynamicWorkerExecutor`, and
70
+ serves the seven-tool code-first surface. Leave the binding absent — as it is in
71
+ the checked-in config — to deploy the same source on the Workers Free plan with
72
+ the nine classic meta-tools. A deployment copied into its own repository must
73
+ also install the executor package before enabling the binding:
74
+
75
+ ```sh
76
+ npm install @cloudflare/codemode
77
+ ```
78
+
79
+ ## Activity history (optional)
80
+
81
+ `src/d1-activity.ts` is a complete `ActivityStore` over D1 — keyset paging on
82
+ `(occurred_at_ms, id)` plus a batched retention pass — but it is **not wired
83
+ into `src/index.ts`**, so the example deploys without a database. To enable it:
84
+
85
+ 1. Create the database and bind it in `wrangler.jsonc`:
86
+
87
+ ```sh
88
+ wrangler d1 create connecta-activity
89
+ ```
90
+
91
+ ```jsonc
92
+ "d1_databases": [
93
+ { "binding": "ACTIVITY_DB", "database_name": "connecta-activity", "database_id": "…" }
94
+ ]
95
+ ```
96
+
97
+ 2. Apply the schema (keep it in a deployment-owned `migrations/` directory):
98
+
99
+ ```sql
100
+ CREATE TABLE IF NOT EXISTS tool_call_activity (
101
+ id TEXT PRIMARY KEY,
102
+ occurred_at_ms INTEGER NOT NULL,
103
+ request_id TEXT NOT NULL,
104
+ actor_kind TEXT NOT NULL,
105
+ actor_id TEXT,
106
+ actor_namespace TEXT,
107
+ connector_id TEXT NOT NULL,
108
+ tool_name TEXT NOT NULL,
109
+ source TEXT NOT NULL,
110
+ outcome TEXT NOT NULL,
111
+ duration_ms INTEGER NOT NULL,
112
+ attempts INTEGER NOT NULL,
113
+ error_code TEXT,
114
+ server_name TEXT NOT NULL,
115
+ server_version TEXT NOT NULL,
116
+ deployment_id TEXT
117
+ );
118
+
119
+ CREATE INDEX IF NOT EXISTS tool_call_activity_recent
120
+ ON tool_call_activity (occurred_at_ms DESC, id DESC);
121
+ ```
122
+
123
+ **Already have this table?** `actor_namespace` was added after the original
124
+ example, and `CREATE TABLE IF NOT EXISTS` will not add it to a table that
125
+ already exists. Add it as a migration:
126
+
127
+ ```sql
128
+ ALTER TABLE tool_call_activity ADD COLUMN actor_namespace TEXT;
129
+ ```
130
+
131
+ Do this **before** deploying the updated `d1-activity.ts`: its `INSERT`
132
+ names the column, so against an un-migrated table every write fails with
133
+ `no such column`. Activity writes are best-effort by design — connecta logs
134
+ the failure and returns the tool result unharmed — so the symptom is not an
135
+ error your agent sees, it is an activity log that quietly stops recording.
136
+
137
+ 3. Pass the store to `createConnecta`:
138
+
139
+ ```ts
140
+ import { d1ActivityStore } from "./d1-activity.js";
141
+
142
+ createConnecta({
143
+ // …
144
+ activity: {
145
+ store: d1ActivityStore(env.ACTIVITY_DB),
146
+ deploymentId: "production",
147
+ },
148
+ });
149
+ ```
150
+
151
+ Events carry no arguments, results, generated code, or raw error messages — see
152
+ [activity history](../../documentation/operator-ui.md).
153
+ The Worker entrypoint already forwards `ctx` to `connecta.fetch`, which lets
154
+ async activity writes settle on `waitUntil`.
155
+
156
+ For retention, add a
157
+ [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/)
158
+ (`triggers.crons` in `wrangler.jsonc` plus a `scheduled` handler — this example
159
+ no longer ships one) and call `pruneActivity(env.ACTIVITY_DB, retentionDays)`
160
+ from it.
@@ -0,0 +1,43 @@
1
+ import type { KVStorage } from "@zackbart/connecta";
2
+
3
+ /**
4
+ * KVStorage backed by a Cloudflare Workers KV namespace binding.
5
+ * Note: Workers KV enforces a 60s minimum TTL; shorter TTLs are dropped
6
+ * (stored without expiry) rather than rejected.
7
+ *
8
+ * Workers KV is eventually consistent across locations. It is suitable for
9
+ * this example's durable state, but cannot promise immediate global OAuth
10
+ * disconnect, credential rotation, or access-token issuance/revocation; use a
11
+ * strongly consistent adapter when that is required.
12
+ */
13
+ export function cloudflareKvStorage(namespace: KVNamespace): KVStorage {
14
+ return {
15
+ async get(key) {
16
+ return namespace.get(key);
17
+ },
18
+ async set(key, value, opts) {
19
+ const ttl = opts?.ttlSeconds;
20
+ await namespace.put(
21
+ key,
22
+ value,
23
+ ttl && ttl >= 60 ? { expirationTtl: ttl } : undefined,
24
+ );
25
+ },
26
+ async delete(key) {
27
+ await namespace.delete(key);
28
+ },
29
+ async list(prefix) {
30
+ const keys: string[] = [];
31
+ let cursor: string | undefined;
32
+ do {
33
+ const page = await namespace.list({
34
+ prefix,
35
+ ...(cursor ? { cursor } : {}),
36
+ });
37
+ keys.push(...page.keys.map((key) => key.name));
38
+ cursor = page.list_complete ? undefined : page.cursor;
39
+ } while (cursor);
40
+ return keys.sort();
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,100 @@
1
+ // Keep this pure mapping module dependency-free so the repository's tests can
2
+ // exercise the example before the package's dist/ entrypoint has been built.
3
+ // The public adapter in d1-activity.ts remains checked against ActivityStore.
4
+ export interface ActivityEvent {
5
+ schemaVersion: 1;
6
+ id: string;
7
+ occurredAt: string;
8
+ requestId: string;
9
+ actor: {
10
+ kind: string;
11
+ id?: string;
12
+ namespace?: string;
13
+ };
14
+ connectorId: string;
15
+ toolName: string;
16
+ address: string;
17
+ source:
18
+ | "call_tool"
19
+ | "call_destructive_tool"
20
+ | "batch_call"
21
+ | "execute_code";
22
+ outcome: "success" | "error" | "timeout" | "cancelled";
23
+ durationMs: number;
24
+ attempts: number;
25
+ errorCode?: string;
26
+ serverName: string;
27
+ serverVersion: string;
28
+ deploymentId?: string;
29
+ }
30
+
31
+ export interface ActivityRow {
32
+ id: string;
33
+ occurred_at_ms: number;
34
+ request_id: string;
35
+ actor_kind: string;
36
+ actor_id: string | null;
37
+ actor_namespace: string | null;
38
+ connector_id: string;
39
+ tool_name: string;
40
+ source: ActivityEvent["source"];
41
+ outcome: ActivityEvent["outcome"];
42
+ duration_ms: number;
43
+ attempts: number;
44
+ error_code: string | null;
45
+ server_name: string;
46
+ server_version: string;
47
+ deployment_id: string | null;
48
+ }
49
+
50
+ export function activityEventToRow(
51
+ event: ActivityEvent,
52
+ ): ActivityRow {
53
+ return {
54
+ id: event.id,
55
+ occurred_at_ms: Date.parse(event.occurredAt),
56
+ request_id: event.requestId,
57
+ actor_kind: event.actor.kind,
58
+ actor_id: event.actor.id ?? null,
59
+ actor_namespace: event.actor.namespace ?? null,
60
+ connector_id: event.connectorId,
61
+ tool_name: event.toolName,
62
+ source: event.source,
63
+ outcome: event.outcome,
64
+ duration_ms: event.durationMs,
65
+ attempts: event.attempts,
66
+ error_code: event.errorCode ?? null,
67
+ server_name: event.serverName,
68
+ server_version: event.serverVersion,
69
+ deployment_id: event.deploymentId ?? null,
70
+ };
71
+ }
72
+
73
+ export function activityRowToEvent(
74
+ row: ActivityRow,
75
+ ): ActivityEvent {
76
+ return {
77
+ schemaVersion: 1,
78
+ id: row.id,
79
+ occurredAt: new Date(row.occurred_at_ms).toISOString(),
80
+ requestId: row.request_id,
81
+ actor: {
82
+ kind: row.actor_kind,
83
+ ...(row.actor_id ? { id: row.actor_id } : {}),
84
+ ...(row.actor_namespace ? { namespace: row.actor_namespace } : {}),
85
+ },
86
+ connectorId: row.connector_id,
87
+ toolName: row.tool_name,
88
+ address: `${row.connector_id}.${row.tool_name}`,
89
+ source: row.source,
90
+ outcome: row.outcome,
91
+ durationMs: row.duration_ms,
92
+ attempts: row.attempts,
93
+ ...(row.error_code ? { errorCode: row.error_code } : {}),
94
+ serverName: row.server_name,
95
+ serverVersion: row.server_version,
96
+ ...(row.deployment_id
97
+ ? { deploymentId: row.deployment_id }
98
+ : {}),
99
+ };
100
+ }
@@ -0,0 +1,144 @@
1
+ import type {
2
+ ActivityPage,
3
+ ActivityStore,
4
+ } from "@zackbart/connecta";
5
+ import { InvalidActivityCursorError } from "@zackbart/connecta";
6
+ import {
7
+ activityEventToRow,
8
+ activityRowToEvent,
9
+ type ActivityRow,
10
+ } from "./d1-activity-row.js";
11
+
12
+ interface Cursor {
13
+ occurredAtMs: number;
14
+ id: string;
15
+ }
16
+
17
+ function encodeCursor(row: ActivityRow): string {
18
+ return btoa(`${row.occurred_at_ms}:${row.id}`);
19
+ }
20
+
21
+ function decodeCursor(value: string): Cursor {
22
+ let decoded: string;
23
+ try {
24
+ decoded = atob(value);
25
+ } catch {
26
+ throw new InvalidActivityCursorError();
27
+ }
28
+ const separator = decoded.indexOf(":");
29
+ const occurredAtMs = Number(decoded.slice(0, separator));
30
+ const id = decoded.slice(separator + 1);
31
+ if (
32
+ separator < 1 ||
33
+ !Number.isSafeInteger(occurredAtMs) ||
34
+ occurredAtMs < 0 ||
35
+ !/^[0-9a-f-]{36}$/i.test(id)
36
+ ) {
37
+ throw new InvalidActivityCursorError();
38
+ }
39
+ return { occurredAtMs, id };
40
+ }
41
+
42
+ /** One append-only row per completed downstream call; no arguments or results. */
43
+ export function d1ActivityStore(db: D1Database): ActivityStore {
44
+ return {
45
+ async record(event) {
46
+ const row = activityEventToRow(event);
47
+ await db
48
+ .prepare(
49
+ `INSERT INTO tool_call_activity (
50
+ id, occurred_at_ms, request_id, actor_kind, actor_id,
51
+ actor_namespace,
52
+ connector_id, tool_name, source, outcome, duration_ms, attempts,
53
+ error_code, server_name, server_version, deployment_id
54
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
55
+ )
56
+ .bind(
57
+ row.id,
58
+ row.occurred_at_ms,
59
+ row.request_id,
60
+ row.actor_kind,
61
+ row.actor_id,
62
+ row.actor_namespace,
63
+ row.connector_id,
64
+ row.tool_name,
65
+ row.source,
66
+ row.outcome,
67
+ row.duration_ms,
68
+ row.attempts,
69
+ row.error_code,
70
+ row.server_name,
71
+ row.server_version,
72
+ row.deployment_id,
73
+ )
74
+ .run();
75
+ },
76
+
77
+ async list({ cursor, limit }): Promise<ActivityPage> {
78
+ const boundedLimit = Math.min(100, Math.max(1, Math.trunc(limit)));
79
+ const pageSize = boundedLimit + 1;
80
+ const position = cursor ? decodeCursor(cursor) : undefined;
81
+ const statement = position
82
+ ? db
83
+ .prepare(
84
+ `SELECT * FROM tool_call_activity
85
+ WHERE occurred_at_ms < ?
86
+ OR (occurred_at_ms = ? AND id < ?)
87
+ ORDER BY occurred_at_ms DESC, id DESC
88
+ LIMIT ?`,
89
+ )
90
+ .bind(
91
+ position.occurredAtMs,
92
+ position.occurredAtMs,
93
+ position.id,
94
+ pageSize,
95
+ )
96
+ : db
97
+ .prepare(
98
+ `SELECT * FROM tool_call_activity
99
+ ORDER BY occurred_at_ms DESC, id DESC
100
+ LIMIT ?`,
101
+ )
102
+ .bind(pageSize);
103
+ const result = await statement.all<ActivityRow>();
104
+ const rows = result.results ?? [];
105
+ const hasMore = rows.length > boundedLimit;
106
+ const visible = hasMore ? rows.slice(0, boundedLimit) : rows;
107
+ const last = visible.at(-1);
108
+ return {
109
+ events: visible.map(activityRowToEvent),
110
+ ...(hasMore && last ? { nextCursor: encodeCursor(last) } : {}),
111
+ };
112
+ },
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Daily retention pass. Each statement is capped at 5,000 rows, then repeats
118
+ * up to a per-run ceiling so normal traffic catches up without allowing one
119
+ * scheduled invocation to consume unbounded queries.
120
+ */
121
+ export async function pruneActivity(
122
+ db: D1Database,
123
+ retentionDays: number,
124
+ maxBatches = 20,
125
+ ): Promise<void> {
126
+ const cutoff = Date.now() -
127
+ Math.max(1, Math.trunc(retentionDays)) * 24 * 60 * 60 * 1_000;
128
+ const batchLimit = Math.max(1, Math.trunc(maxBatches));
129
+ for (let batch = 0; batch < batchLimit; batch++) {
130
+ const result = await db
131
+ .prepare(
132
+ `DELETE FROM tool_call_activity
133
+ WHERE id IN (
134
+ SELECT id FROM tool_call_activity
135
+ WHERE occurred_at_ms < ?
136
+ ORDER BY occurred_at_ms ASC
137
+ LIMIT 5000
138
+ )`,
139
+ )
140
+ .bind(cutoff)
141
+ .run();
142
+ if ((result.meta.changes ?? 0) < 5_000) return;
143
+ }
144
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * connecta on Cloudflare Workers.
3
+ *
4
+ * One MCP endpoint aggregating a downstream remote MCP and an HTTP API, guarded
5
+ * by Clerk OAuth *and* a static bearer token, with OAuth/cache state in a KV
6
+ * namespace. Add the optional Worker Loader binding in wrangler.jsonc for the
7
+ * seven-tool code-first surface; without it this serves the nine classic tools.
8
+ *
9
+ * Setup (this example has no package.json of its own — it self-references the
10
+ * installed `@zackbart/connecta` package):
11
+ * 1. `npm install` in the connecta package root (../../ from here) so the
12
+ * package import and wrangler resolve.
13
+ * 2. Create a KV namespace and put its id in wrangler.jsonc under `kv_namespaces`.
14
+ * 3. Set secrets:
15
+ * wrangler secret put SUPPORT_TOKEN
16
+ * wrangler secret put EXEC_TOKEN
17
+ * wrangler secret put CLERK_SECRET_KEY
18
+ * wrangler secret put DOWNSTREAM_TOKEN
19
+ * and CLERK_PUBLISHABLE_KEY + PUBLIC_URL as plain vars in wrangler.jsonc.
20
+ * 4. Enable Dynamic Client Registration in the Clerk dashboard
21
+ * (OAuth Applications -> DCR toggle) so Claude/Cursor can self-register.
22
+ * 5. Optional paid code mode: add the documented `worker_loaders` binding to
23
+ * wrangler.jsonc. Binding presence enables execute_code automatically.
24
+ * 6. `wrangler deploy` from this folder (examples/worker), where wrangler.jsonc
25
+ * lives. Point your MCP client at `<PUBLIC_URL>/mcp`.
26
+ */
27
+ import { DynamicWorkerExecutor } from "@cloudflare/codemode";
28
+ import {
29
+ api,
30
+ bearerToken,
31
+ createConnecta,
32
+ remoteMcp,
33
+ } from "@zackbart/connecta";
34
+ import { clerkAuth } from "@zackbart/connecta/auth/clerk";
35
+ import { cloudflareKvStorage } from "./cloudflare-kv.js";
36
+
37
+ interface Env {
38
+ CONNECTA_KV: KVNamespace;
39
+ /** Bearer token for one headless client in this deployment's audience. */
40
+ SUPPORT_TOKEN: string;
41
+ /** Bearer token for another headless client in the same audience. */
42
+ EXEC_TOKEN: string;
43
+ CLERK_PUBLISHABLE_KEY: string;
44
+ CLERK_SECRET_KEY: string;
45
+ DOWNSTREAM_TOKEN: string;
46
+ PUBLIC_URL: string;
47
+ /**
48
+ * Worker Loader binding (wrangler.jsonc `worker_loaders`) powering
49
+ * execute_code and, with it, the code-first surface. Dynamic Workers require
50
+ * the Workers Paid plan; leave the binding absent for the nine classic
51
+ * meta-tools on either plan.
52
+ */
53
+ LOADER?: WorkerLoader;
54
+ }
55
+
56
+ function build(env: Env) {
57
+ return createConnecta({
58
+ publicUrl: env.PUBLIC_URL,
59
+ storage: cloudflareKvStorage(env.CONNECTA_KV),
60
+ // Binding-as-switch: adding worker_loaders in wrangler.jsonc enables code
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
+ : {}),
66
+ auth: [
67
+ // Multiple credentials may identify callers in one deployment. Every
68
+ // admitted caller reaches this deployment's deliberate connector set.
69
+ bearerToken(env.SUPPORT_TOKEN, {
70
+ subjectId: "support-team",
71
+ }),
72
+ bearerToken(env.EXEC_TOKEN, {
73
+ subjectId: "exec-team",
74
+ }),
75
+ // The operator signs in with Clerk. Restrict who may sign in with
76
+ // `allowedDomains` (or a `gate`, for anything a domain cannot express).
77
+ clerkAuth({
78
+ publishableKey: env.CLERK_PUBLISHABLE_KEY,
79
+ secretKey: env.CLERK_SECRET_KEY,
80
+ publicUrl: env.PUBLIC_URL,
81
+ // allowedDomains: ["acme.com"],
82
+ }),
83
+ ],
84
+ // Eligible Clerk operators can create named, revocable MCP Bearer tokens
85
+ // at /tokens. Secrets are shown once; only their hashes enter KV.
86
+ accessTokens: {},
87
+ connectors: [
88
+ remoteMcp("notion", {
89
+ url: "https://mcp.notion.com/mcp",
90
+ description: "Notion — pages, databases, comments (static token)",
91
+ auth: {
92
+ type: "headers",
93
+ headers: { Authorization: `Bearer ${env.DOWNSTREAM_TOKEN}` },
94
+ },
95
+ }),
96
+ api("echo", {
97
+ description: "Echo — text transforms",
98
+ tools: [
99
+ {
100
+ name: "shout",
101
+ description: "Uppercase the given text.",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ text: { type: "string", description: "Text to uppercase." },
106
+ },
107
+ required: ["text"],
108
+ },
109
+ annotations: { readOnlyHint: true },
110
+ handler: async (args: { text: string }) => ({
111
+ shouted: args.text.toUpperCase(),
112
+ }),
113
+ },
114
+ ],
115
+ }),
116
+ ],
117
+ });
118
+ }
119
+
120
+ // Lazy per-isolate singleton: reuses the plain-data tool cache. Downstream MCP
121
+ // clients are request-scoped internally so Worker I/O never crosses requests.
122
+ let connecta: ReturnType<typeof build> | undefined;
123
+
124
+ export default {
125
+ // Pass `ctx` through: connecta hands deferred work (activity sinks) to
126
+ // ctx.waitUntil so it settles after the response is returned instead of
127
+ // being cancelled with the request.
128
+ async fetch(
129
+ request: Request,
130
+ env: Env,
131
+ ctx: ExecutionContext,
132
+ ): Promise<Response> {
133
+ connecta ??= build(env);
134
+ return connecta.fetch(request, env, ctx);
135
+ },
136
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "connecta-example",
4
+ "main": "src/index.ts",
5
+ "compatibility_date": "2025-01-01",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "observability": { "enabled": true },
8
+
9
+ // Plain vars. Secrets (SUPPORT_TOKEN, EXEC_TOKEN, CLERK_SECRET_KEY,
10
+ // DOWNSTREAM_TOKEN) are set with `wrangler secret put <NAME>`, not here.
11
+ "vars": {
12
+ "PUBLIC_URL": "https://connecta.example.workers.dev",
13
+ "CLERK_PUBLISHABLE_KEY": "pk_test_replace-me"
14
+ },
15
+
16
+ // Create with `wrangler kv namespace create CONNECTA_KV` and paste the id.
17
+ "kv_namespaces": [
18
+ { "binding": "CONNECTA_KV", "id": "replace-with-kv-namespace-id" }
19
+ ]
20
+
21
+ // Optional paid code mode: add a comma above, then uncomment this binding.
22
+ // Its presence is the entire switch — src/index.ts detects env.LOADER and
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" }]
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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.",
@@ -11,6 +11,9 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "bin": {
15
+ "connecta": "./bin/connecta.mjs"
16
+ },
14
17
  "repository": {
15
18
  "type": "git",
16
19
  "url": "git+https://github.com/zackbart/connecta.git"
@@ -26,9 +29,16 @@
26
29
  "connectors"
27
30
  ],
28
31
  "files": [
32
+ "bin",
29
33
  "dist",
30
34
  "src",
31
35
  "assets",
36
+ "documentation",
37
+ "examples/node",
38
+ "examples/worker",
39
+ "templates",
40
+ "AGENTS.md",
41
+ "ethos.md",
32
42
  "README.md",
33
43
  "CHANGELOG.md",
34
44
  "SECURITY.md",