@zackbart/connecta 0.10.6 → 0.12.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.
Files changed (97) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +80 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/apps-shell.d.ts +38 -0
  8. package/dist/apps-shell.d.ts.map +1 -0
  9. package/dist/apps-shell.js +175 -0
  10. package/dist/apps-shell.js.map +1 -0
  11. package/dist/catalog-service.d.ts +2 -17
  12. package/dist/catalog-service.d.ts.map +1 -1
  13. package/dist/catalog-service.js +4 -6
  14. package/dist/catalog-service.js.map +1 -1
  15. package/dist/connectors/api.d.ts +2 -2
  16. package/dist/connectors/remote-mcp.d.ts +1 -1
  17. package/dist/errors.d.ts +1 -3
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +1 -1
  20. package/dist/errors.js.map +1 -1
  21. package/dist/execute.d.ts +37 -8
  22. package/dist/execute.d.ts.map +1 -1
  23. package/dist/execute.js +137 -42
  24. package/dist/execute.js.map +1 -1
  25. package/dist/executor-admission.d.ts +8 -0
  26. package/dist/executor-admission.d.ts.map +1 -1
  27. package/dist/executor-admission.js +11 -0
  28. package/dist/executor-admission.js.map +1 -1
  29. package/dist/executors/quickjs.d.ts.map +1 -1
  30. package/dist/executors/quickjs.js +2 -2
  31. package/dist/executors/quickjs.js.map +1 -1
  32. package/dist/index.d.ts +15 -31
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +23 -37
  35. package/dist/index.js.map +1 -1
  36. package/dist/invocation.js +2 -2
  37. package/dist/invocation.js.map +1 -1
  38. package/dist/meta-tools.d.ts +19 -58
  39. package/dist/meta-tools.d.ts.map +1 -1
  40. package/dist/meta-tools.js +34 -431
  41. package/dist/meta-tools.js.map +1 -1
  42. package/dist/registry.d.ts +1 -10
  43. package/dist/registry.d.ts.map +1 -1
  44. package/dist/registry.js +3 -15
  45. package/dist/registry.js.map +1 -1
  46. package/dist/routes/mcp.d.ts.map +1 -1
  47. package/dist/routes/mcp.js +67 -30
  48. package/dist/routes/mcp.js.map +1 -1
  49. package/dist/routes/shared.d.ts +5 -11
  50. package/dist/routes/shared.d.ts.map +1 -1
  51. package/dist/routes/shared.js.map +1 -1
  52. package/dist/server.js +5 -4
  53. package/dist/server.js.map +1 -1
  54. package/dist/skills.d.ts +8 -18
  55. package/dist/skills.d.ts.map +1 -1
  56. package/dist/skills.js +13 -60
  57. package/dist/skills.js.map +1 -1
  58. package/dist/types.d.ts +6 -20
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/documentation/code-first-exploration.md +16 -16
  63. package/documentation/code-mode.md +130 -41
  64. package/documentation/connectors.md +1 -1
  65. package/documentation/mcp-2026-07-28.md +1 -1
  66. package/documentation/mcp-ui-design.md +382 -0
  67. package/documentation/meta-tools.md +30 -43
  68. package/documentation/rich-output-design.md +4 -4
  69. package/ethos.md +7 -2
  70. package/examples/node/README.md +1 -2
  71. package/examples/node/src/index.ts +1 -3
  72. package/examples/worker/README.md +8 -13
  73. package/examples/worker/src/index.ts +6 -14
  74. package/examples/worker/wrangler.jsonc +3 -6
  75. package/package.json +1 -1
  76. package/src/activity.ts +5 -0
  77. package/src/apps-shell.ts +179 -0
  78. package/src/catalog-service.ts +6 -26
  79. package/src/connectors/api.ts +2 -2
  80. package/src/connectors/remote-mcp.ts +1 -1
  81. package/src/errors.ts +2 -2
  82. package/src/execute.ts +150 -49
  83. package/src/executor-admission.ts +12 -0
  84. package/src/executors/quickjs.ts +2 -1
  85. package/src/index.ts +40 -69
  86. package/src/invocation.ts +2 -2
  87. package/src/meta-tools.ts +38 -565
  88. package/src/registry.ts +2 -33
  89. package/src/routes/mcp.ts +79 -30
  90. package/src/routes/shared.ts +4 -11
  91. package/src/server.ts +7 -7
  92. package/src/skills.ts +11 -74
  93. package/src/types.ts +6 -21
  94. package/src/version.ts +1 -1
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +1 -1
@@ -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. 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.
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. Optional paid code mode: add the documented `worker_loaders` binding to
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 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.
48
+ * execute_code. Dynamic Workers require the Workers Paid plan.
52
49
  */
53
- LOADER?: WorkerLoader;
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
- // 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
- : {}),
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
- // 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" }]
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.10.6",
3
+ "version": "0.12.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.",
package/src/activity.ts CHANGED
@@ -23,6 +23,11 @@ const MAX_ACTIVITY_ADDRESS_BYTES = MAX_ACTIVITY_NAME_BYTES * 2 + 1;
23
23
  export type ActivityCallSource =
24
24
  | "call_tool"
25
25
  | "call_destructive_tool"
26
+ // Read-only history. Nothing emits `batch_call` since issue #273 removed the
27
+ // tool, but activity storage is append-only: rows written by older
28
+ // deployments are still read back — the D1 example maps a stored row straight
29
+ // into this type — and an operator's timeline should not have to lie about
30
+ // where a call came from. Never widen this member back into a live source.
26
31
  | "batch_call"
27
32
  | "execute_code";
28
33
 
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The one MCP Apps template connecta serves (`U5`, `U6`).
3
+ *
4
+ * A build-time string constant, not a file read at startup: the core is
5
+ * Web-API-only so it runs unchanged on Workers, and the same bytes have to
6
+ * serve everywhere. The shell is display-only — it renders whatever HTML a
7
+ * program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
8
+ * channel back from that frame to the host, so program-authored markup is
9
+ * inert beyond its own pixels.
10
+ *
11
+ * The address carries a version segment because hosts are permitted to
12
+ * prefetch and cache templates by URI: change these bytes, bump `v1`.
13
+ */
14
+
15
+ /** The only `ui://` URI in the system. No program input reaches it. */
16
+ export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v1";
17
+
18
+ /** The mimeType the Apps spec requires of an HTML template. */
19
+ export const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
20
+
21
+ /**
22
+ * The result `_meta` key carrying the payload (`U3`). A plain single-label
23
+ * prefix rather than the reverse-DNS form MCP's SHOULD prefers: connecta has
24
+ * no domain to reverse, and fabricating one to satisfy a SHOULD is a worse
25
+ * answer than the shape the key format's MUST already permits.
26
+ */
27
+ export const PROGRAM_UI_META_KEY = "connecta/ui";
28
+
29
+ /** The one extension identifier connecta advertises (`U11`). */
30
+ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
31
+
32
+ /**
33
+ * The shell document. Dependency-free and deliberately small: it speaks the
34
+ * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
35
+ * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
36
+ * `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
37
+ * delivered tool result, and puts it in a frame. It declares no CSP domains,
38
+ * so the host applies its restrictive default and the `srcdoc` frame inherits
39
+ * `default-src 'none'` — the payload gets scripts and local interactivity,
40
+ * and no network.
41
+ */
42
+ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="utf-8" />
46
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
47
+ <title>connecta program view</title>
48
+ <style>
49
+ html,
50
+ body {
51
+ margin: 0;
52
+ padding: 0;
53
+ background: transparent;
54
+ }
55
+ #program-view {
56
+ display: block;
57
+ width: 100%;
58
+ min-height: 480px;
59
+ border: 0;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <iframe
65
+ id="program-view"
66
+ title="Program-rendered view"
67
+ sandbox="allow-scripts"
68
+ srcdoc=""
69
+ ></iframe>
70
+ <script>
71
+ (function () {
72
+ "use strict";
73
+ // The host frame is the only peer this shell speaks to, in either
74
+ // direction. The payload frame below is sandboxed to scripts alone,
75
+ // with no same-origin escape, and is never handed a reply path:
76
+ // anything it posts fails the source check and is dropped. There is
77
+ // no bridge from program HTML to the host, by construction rather
78
+ // than by validation.
79
+ var host = window.parent;
80
+ var view = document.getElementById("program-view");
81
+ var initializeId = "connecta-ui-initialize";
82
+ var lastWidth = 0;
83
+ var lastHeight = 0;
84
+
85
+ function send(message) {
86
+ if (!host || host === window) return;
87
+ host.postMessage(message, "*");
88
+ }
89
+
90
+ function notify(method, params) {
91
+ send({ jsonrpc: "2.0", method: method, params: params });
92
+ }
93
+
94
+ // Program views are fixed-height by construction. The shell has no
95
+ // bridge to the payload frame — that is the security posture, not an
96
+ // omission — so it can never learn the payload's content height, and
97
+ // what it reports here is its own box: the min-height above, unless
98
+ // the host has given it more. Taller content scrolls inside the inner
99
+ // frame rather than growing the view. Raising the min-height is the
100
+ // only lever; a content-height signal would cost the isolation.
101
+ function reportSize() {
102
+ var width = Math.ceil(document.documentElement.clientWidth);
103
+ var height = Math.ceil(document.documentElement.scrollHeight);
104
+ if (width === lastWidth && height === lastHeight) return;
105
+ lastWidth = width;
106
+ lastHeight = height;
107
+ notify("ui/notifications/size-changed", {
108
+ width: width,
109
+ height: height
110
+ });
111
+ }
112
+
113
+ function payloadHtml(result) {
114
+ if (!result || typeof result !== "object") return null;
115
+ var meta = result._meta;
116
+ if (!meta || typeof meta !== "object") return null;
117
+ var payload = meta["connecta/ui"];
118
+ if (!payload || typeof payload !== "object") return null;
119
+ var html = payload.html;
120
+ return typeof html === "string" && html.length > 0 ? html : null;
121
+ }
122
+
123
+ function render(params) {
124
+ var html =
125
+ payloadHtml(params) ||
126
+ payloadHtml(params && params.result) ||
127
+ payloadHtml(params && params.toolResult);
128
+ if (html === null) return;
129
+ view.srcdoc = html;
130
+ reportSize();
131
+ }
132
+
133
+ window.addEventListener("message", function (event) {
134
+ if (event.source !== host) return;
135
+ var message = event.data;
136
+ if (!message || message.jsonrpc !== "2.0") return;
137
+ if (message.method === "ui/notifications/tool-result") {
138
+ render(message.params);
139
+ return;
140
+ }
141
+ if (message.method === "ui/resource-teardown") {
142
+ // A host->view request, not a notification: the host waits for
143
+ // this reply before it tears the view down. There is nothing to
144
+ // release, so answer immediately rather than make it time out.
145
+ if (message.id !== undefined && message.id !== null) {
146
+ send({ jsonrpc: "2.0", id: message.id, result: {} });
147
+ }
148
+ return;
149
+ }
150
+ // Only a completed handshake earns "initialized". A JSON-RPC error
151
+ // response carries the same id, and announcing initialization on one
152
+ // would assert a handshake that never happened.
153
+ if (message.id === initializeId && message.result !== undefined) {
154
+ notify("ui/notifications/initialized", {});
155
+ }
156
+ });
157
+
158
+ window.addEventListener("resize", reportSize);
159
+ view.addEventListener("load", reportSize);
160
+
161
+ // Every field here is required by the Apps initialize schema, and a
162
+ // conforming host rejects the request outright when one is missing —
163
+ // which would strand the shell before any tool result arrives.
164
+ send({
165
+ jsonrpc: "2.0",
166
+ id: initializeId,
167
+ method: "ui/initialize",
168
+ params: {
169
+ appInfo: { name: "connecta program view", version: "1" },
170
+ appCapabilities: {},
171
+ protocolVersion: "2026-01-26"
172
+ }
173
+ });
174
+ reportSize();
175
+ })();
176
+ </script>
177
+ </body>
178
+ </html>
179
+ `;
@@ -48,23 +48,10 @@ const MAX_QUERY_ANALYSIS_TERM_LENGTH = 64;
48
48
 
49
49
  const encoder = new TextEncoder();
50
50
 
51
- /**
52
- * The tool a describe-path error should name when it tells a caller to retry.
53
- * This is the route the *caller* took, not the deployment's advertised surface:
54
- * a classic deployment with an executor serves `describe_tools` at top level
55
- * while every in-program describe still arrives through `connecta.describe`, so
56
- * one CatalogService cannot infer the answer from `surface` alone. Callers pass
57
- * the route they own.
58
- */
59
- export type DescribeRoute = "describe_tools" | "connecta.describe";
60
-
61
51
  /**
62
52
  * The discovery route a routing failure should send a caller back through. Same
63
- * rule as {@link DescribeRoute} the route the *caller* took, not the
64
- * deployment's advertised surface with one difference worth keeping the two
65
- * options separate for: `search_tools` exists on both advertised surfaces, so a
66
- * top-level handler never has to derive this one, while an in-program caller
67
- * still has to be told about `connecta.search` because it cannot call a tool.
53
+ * catalog logic serves both the top-level `search_tools` path and the
54
+ * in-program `connecta.search` path, so callers pass the route they own.
68
55
  */
69
56
  export type SearchRoute = "search_tools" | "connecta.search";
70
57
 
@@ -96,10 +83,7 @@ function discoverySearchLimit(value: unknown): number {
96
83
  }
97
84
 
98
85
  /** Normalize the single-address convenience form, then validate the bounded list. */
99
- function discoveryAddresses(
100
- args: CatalogDescribeArgs,
101
- describeRoute: DescribeRoute,
102
- ): unknown[] {
86
+ function discoveryAddresses(args: CatalogDescribeArgs): unknown[] {
103
87
  if (args.address !== undefined && args.addresses !== undefined) {
104
88
  throw new DiscoveryPolicyError(
105
89
  "invalid_args",
@@ -121,7 +105,7 @@ function discoveryAddresses(
121
105
  if (value.length > MAX_DESCRIBE_ADDRESSES) {
122
106
  throw new DiscoveryPolicyError(
123
107
  "invalid_args",
124
- `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across ${describeRoute} calls.`,
108
+ `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across connecta.describe calls.`,
125
109
  );
126
110
  }
127
111
  return value;
@@ -327,7 +311,6 @@ export class CatalogService {
327
311
  readonly requestScope: object;
328
312
  private readonly probeTimeoutMs: number;
329
313
  private readonly concurrency: number;
330
- private readonly describeRoute: DescribeRoute;
331
314
  private readonly searchRoute: SearchRoute;
332
315
  private readonly loaded = new Map<string, ToolDef[]>();
333
316
  private readonly loading = new Map<string, Promise<ToolDef[]>>();
@@ -339,8 +322,6 @@ export class CatalogService {
339
322
  requestScope?: object;
340
323
  probeTimeoutMs?: number;
341
324
  concurrency?: number;
342
- /** The tool describe-path errors name. Default `describe_tools`. */
343
- describeRoute?: DescribeRoute;
344
325
  /** The discovery route recovery records name. Default `search_tools`. */
345
326
  searchRoute?: SearchRoute;
346
327
  } = {},
@@ -349,7 +330,6 @@ export class CatalogService {
349
330
  this.probeTimeoutMs =
350
331
  normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
351
332
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
352
- this.describeRoute = options.describeRoute ?? "describe_tools";
353
333
  this.searchRoute = options.searchRoute ?? "search_tools";
354
334
  }
355
335
 
@@ -817,7 +797,7 @@ export class CatalogService {
817
797
  }
818
798
 
819
799
  async describe(args: CatalogDescribeArgs): Promise<CatalogDescription[]> {
820
- const addresses = discoveryAddresses(args, this.describeRoute);
800
+ const addresses = discoveryAddresses(args);
821
801
  const format = args.format ?? "compact";
822
802
  const resolved = addresses.map((rawAddress) => {
823
803
  const address = String(rawAddress);
@@ -834,7 +814,7 @@ export class CatalogService {
834
814
  connectorIds,
835
815
  this.concurrency,
836
816
  (id) =>
837
- this.loadForDiscovery(id, `${this.describeRoute} probe of "${id}"`),
817
+ this.loadForDiscovery(id, `connecta.describe probe of "${id}"`),
838
818
  );
839
819
  const catalogs = new Map<string, ToolDef[] | Error>();
840
820
  loaded.forEach((result, index) => {
@@ -20,7 +20,7 @@ export interface ApiTool {
20
20
  outputSchema?: JsonSchema;
21
21
  /**
22
22
  * Standard MCP-style behavior hints. Only an explicit readOnlyHint: true
23
- * admits the tool to call_tool, batch_call, and execute_code.
23
+ * admits the tool to call_tool and execute_code.
24
24
  */
25
25
  annotations?: ToolAnnotations;
26
26
  handler: (args: any, ctx: ConnectorContext) => Promise<unknown> | unknown;
@@ -32,7 +32,7 @@ export interface ApiOptions {
32
32
  description?: string;
33
33
  /**
34
34
  * Max inline result size (bytes) for this connector's tools before
35
- * call_tool/batch_call truncate and stash the full text for get_result
35
+ * call_tool truncates and stashes the full text for get_result
36
36
  * paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
37
37
  * it. Must be a whole number of bytes >= 1; anything else warns at startup
38
38
  * and is ignored.
@@ -38,7 +38,7 @@ export interface RemoteMcpOptions {
38
38
  description?: string;
39
39
  /**
40
40
  * Max inline result size (bytes) for this connector's tools before
41
- * call_tool/batch_call truncate and stash the full text for get_result
41
+ * call_tool truncates and stashes the full text for get_result
42
42
  * paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
43
43
  * it. Must be a whole number of bytes >= 1; anything else warns at startup
44
44
  * and is ignored.
package/src/errors.ts CHANGED
@@ -221,7 +221,7 @@ export class ConnectorCallError extends Error {
221
221
  }
222
222
  }
223
223
 
224
- /** The `error` object surfaced in call_tool/batch_call value-mode results. */
224
+ /** The `error` object surfaced in value-mode call results and connecta.batch entries. */
225
225
  export interface CallErrorDetails {
226
226
  code: string;
227
227
  message: string;
@@ -322,7 +322,7 @@ const RETRYABLE_MESSAGE_RE =
322
322
  const TIMEOUT_MESSAGE_RE = /timed out|timeout/i;
323
323
 
324
324
  /** Message-text fallback used when an error carries no typed classification. */
325
- export function messageLooksRetryable(message: string): boolean {
325
+ function messageLooksRetryable(message: string): boolean {
326
326
  return RETRYABLE_MESSAGE_RE.test(message);
327
327
  }
328
328