@zackbart/connecta 0.12.1 → 0.13.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 (58) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +4 -1
  3. package/dist/apps-shell.d.ts +13 -11
  4. package/dist/apps-shell.d.ts.map +1 -1
  5. package/dist/apps-shell.js +221 -30
  6. package/dist/apps-shell.js.map +1 -1
  7. package/dist/catalog-service.d.ts +41 -0
  8. package/dist/catalog-service.d.ts.map +1 -1
  9. package/dist/catalog-service.js +94 -5
  10. package/dist/catalog-service.js.map +1 -1
  11. package/dist/connectors/api.d.ts +5 -4
  12. package/dist/connectors/api.d.ts.map +1 -1
  13. package/dist/connectors/api.js.map +1 -1
  14. package/dist/connectors/remote-mcp.d.ts +5 -4
  15. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  16. package/dist/connectors/remote-mcp.js.map +1 -1
  17. package/dist/execute.d.ts +12 -4
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +142 -20
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/meta-tools.d.ts.map +1 -1
  25. package/dist/meta-tools.js +14 -4
  26. package/dist/meta-tools.js.map +1 -1
  27. package/dist/providers/mixpanel.d.ts +21 -0
  28. package/dist/providers/mixpanel.d.ts.map +1 -0
  29. package/dist/providers/mixpanel.js +183 -0
  30. package/dist/providers/mixpanel.js.map +1 -0
  31. package/dist/skills.d.ts +7 -9
  32. package/dist/skills.d.ts.map +1 -1
  33. package/dist/skills.js +60 -25
  34. package/dist/skills.js.map +1 -1
  35. package/dist/types.d.ts +26 -6
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/documentation/code-mode.md +32 -20
  40. package/documentation/connectors.md +116 -4
  41. package/documentation/mcp-ui-design.md +8 -8
  42. package/documentation/meta-tools.md +80 -8
  43. package/documentation/mixpanel.md +72 -0
  44. package/documentation/program-ui-read-calls.md +213 -0
  45. package/ethos.md +10 -4
  46. package/package.json +5 -1
  47. package/src/apps-shell.ts +221 -30
  48. package/src/catalog-service.ts +139 -4
  49. package/src/connectors/api.ts +5 -3
  50. package/src/connectors/remote-mcp.ts +5 -3
  51. package/src/execute.ts +215 -21
  52. package/src/index.ts +1 -0
  53. package/src/meta-tools.ts +19 -4
  54. package/src/providers/mixpanel.ts +220 -0
  55. package/src/skills.ts +66 -24
  56. package/src/types.ts +27 -6
  57. package/src/version.ts +1 -1
  58. package/templates/node/package.json +1 -1
package/src/apps-shell.ts CHANGED
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * A build-time string constant, not a file read at startup: the core is
5
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.
6
+ * serve everywhere. It renders whatever HTML a program handed `connecta.ui`
7
+ * inside a nested `srcdoc` frame. The one-argument form forwards no channel;
8
+ * an explicitly bound view gets only named read calls through the trusted
9
+ * shell, never a raw host channel.
10
10
  *
11
11
  * The address carries a version segment because hosts are permitted to
12
- * prefetch and cache templates by URI: change these bytes, bump `v1`.
12
+ * prefetch and cache templates by URI: change these bytes, bump the version.
13
13
  */
14
14
 
15
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";
16
+ export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v2";
17
17
 
18
18
  /** The mimeType the Apps spec requires of an HTML template. */
19
19
  export const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
@@ -34,10 +34,12 @@ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
34
34
  * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
35
35
  * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
36
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.
37
+ * delivered tool result, and puts it in a frame. An optional read manifest
38
+ * installs one narrow `connecta.read(name, args)` bridge in that inner frame;
39
+ * the outer shell maps declared names to the existing `call_tool` meta-tool.
40
+ * It declares no CSP domains, so the host applies its restrictive default and
41
+ * the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
42
+ * no direct network.
41
43
  */
42
44
  export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
43
45
  <html lang="en">
@@ -70,17 +72,21 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
70
72
  <script>
71
73
  (function () {
72
74
  "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.
75
+ // The outer shell is the only host peer. The payload frame is
76
+ // sandboxed to scripts alone, with no same-origin escape. Its one
77
+ // optional message dialect is handled below and translated into
78
+ // bounded call_tool requests; raw JSON-RPC is never forwarded.
79
79
  var host = window.parent;
80
80
  var view = document.getElementById("program-view");
81
81
  var initializeId = "connecta-ui-initialize";
82
82
  var lastWidth = 0;
83
83
  var lastHeight = 0;
84
+ var reads = null;
85
+ var hostCanCallTools = false;
86
+ var nextHostRequestId = 0;
87
+ var pendingHostReads = Object.create(null);
88
+ var activeHostReads = 0;
89
+ var maxActiveHostReads = 8;
84
90
 
85
91
  function send(message) {
86
92
  if (!host || host === window) return;
@@ -92,8 +98,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
92
98
  }
93
99
 
94
100
  // 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
101
+ // content-height bridge to the payload frame, so it can never learn
102
+ // the payload's content height, and
97
103
  // what it reports here is its own box: the min-height above, unless
98
104
  // the host has given it more. Taller content scrolls inside the inner
99
105
  // frame rather than growing the view. Raising the min-height is the
@@ -110,30 +116,213 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
110
116
  });
111
117
  }
112
118
 
113
- function payloadHtml(result) {
119
+ function payload(result) {
114
120
  if (!result || typeof result !== "object") return null;
115
121
  var meta = result._meta;
116
122
  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;
123
+ var value = meta["connecta/ui"];
124
+ if (!value || typeof value !== "object") return null;
125
+ return typeof value.html === "string" && value.html.length > 0
126
+ ? value
127
+ : null;
128
+ }
129
+
130
+ // This function is serialized into the opaque-origin payload frame.
131
+ // It knows no addresses and has no host channel of its own: one named
132
+ // read request goes to the trusted outer shell and one correlated
133
+ // result comes back.
134
+ function payloadReadBridge() {
135
+ "use strict";
136
+ var pending = Object.create(null);
137
+ var nextId = 0;
138
+
139
+ function read(name, args) {
140
+ return new Promise(function (resolve, reject) {
141
+ var id = String(++nextId);
142
+ pending[id] = { resolve: resolve, reject: reject };
143
+ try {
144
+ window.parent.postMessage({
145
+ type: "connecta/read",
146
+ id: id,
147
+ name: name,
148
+ args: args === undefined ? {} : args
149
+ }, "*");
150
+ } catch (error) {
151
+ delete pending[id];
152
+ reject(error);
153
+ }
154
+ });
155
+ }
156
+
157
+ Object.defineProperty(globalThis, "connecta", {
158
+ value: Object.freeze({ read: read }),
159
+ configurable: false,
160
+ enumerable: true,
161
+ writable: false
162
+ });
163
+
164
+ window.addEventListener("message", function (event) {
165
+ if (event.source !== window.parent) return;
166
+ var message = event.data;
167
+ if (!message || message.type !== "connecta/read-result") return;
168
+ var waiter = pending[message.id];
169
+ if (!waiter) return;
170
+ delete pending[message.id];
171
+ if (message.ok) waiter.resolve(message.value);
172
+ else waiter.reject(new Error(message.error || "Read failed"));
173
+ });
174
+ }
175
+
176
+ function htmlWithReadBridge(html) {
177
+ var script =
178
+ "<scr" + "ipt>(" + payloadReadBridge.toString() + ")();</scr" + "ipt>";
179
+ var head = /<head(?:\\s[^>]*)?>/i.exec(html);
180
+ if (head) {
181
+ var at = (head.index || 0) + head[0].length;
182
+ return html.slice(0, at) + script + html.slice(at);
183
+ }
184
+ var document = /<html(?:\\s[^>]*)?>/i.exec(html);
185
+ if (document) {
186
+ var afterHtml = (document.index || 0) + document[0].length;
187
+ return html.slice(0, afterHtml) + "<head>" + script + "</head>" + html.slice(afterHtml);
188
+ }
189
+ return script + html;
121
190
  }
122
191
 
123
192
  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;
193
+ var value =
194
+ payload(params) ||
195
+ payload(params && params.result) ||
196
+ payload(params && params.toolResult);
197
+ if (value === null) return;
198
+ reads = value.reads && typeof value.reads === "object"
199
+ ? value.reads
200
+ : null;
201
+ view.srcdoc = reads
202
+ ? htmlWithReadBridge(value.html)
203
+ : value.html;
130
204
  reportSize();
131
205
  }
132
206
 
207
+ function readError(message, fallback) {
208
+ if (message && typeof message.message === "string") return message.message;
209
+ if (message && message.data && typeof message.data.message === "string") {
210
+ return message.data.message;
211
+ }
212
+ return fallback;
213
+ }
214
+
215
+ function finishInnerRead(innerId, ok, value) {
216
+ if (!view.contentWindow) return;
217
+ view.contentWindow.postMessage(ok
218
+ ? { type: "connecta/read-result", id: innerId, ok: true, value: value }
219
+ : { type: "connecta/read-result", id: innerId, ok: false, error: value }, "*");
220
+ }
221
+
222
+ function beginInnerRead(message) {
223
+ if (!hostCanCallTools) {
224
+ finishInnerRead(message && message.id, false, "This host does not support app-initiated server tool calls");
225
+ return;
226
+ }
227
+ if (!message || typeof message.id !== "string" || typeof message.name !== "string") return;
228
+ if (!reads || !Object.prototype.hasOwnProperty.call(reads, message.name)) {
229
+ finishInnerRead(message.id, false, "Unknown read binding");
230
+ return;
231
+ }
232
+ if (activeHostReads >= maxActiveHostReads) {
233
+ finishInnerRead(message.id, false, "Too many concurrent reads");
234
+ return;
235
+ }
236
+ var binding = reads[message.name];
237
+ if (!binding || typeof binding !== "object" || typeof binding.address !== "string") {
238
+ finishInnerRead(message.id, false, "Invalid read binding");
239
+ return;
240
+ }
241
+ var supplied = message.args;
242
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) {
243
+ finishInnerRead(message.id, false, "Read arguments must be an object");
244
+ return;
245
+ }
246
+ var allowed = Array.isArray(binding.viewArgs) ? binding.viewArgs : [];
247
+ var suppliedKeys = Object.keys(supplied);
248
+ for (var i = 0; i < suppliedKeys.length; i++) {
249
+ var key = suppliedKeys[i];
250
+ if (allowed.indexOf(key) === -1) {
251
+ finishInnerRead(message.id, false, "Undeclared read argument " + JSON.stringify(key));
252
+ return;
253
+ }
254
+ }
255
+ var args = Object.create(null);
256
+ var fixed = binding.fixedArgs && typeof binding.fixedArgs === "object"
257
+ ? binding.fixedArgs
258
+ : {};
259
+ Object.keys(fixed).forEach(function (key) { args[key] = fixed[key]; });
260
+ suppliedKeys.forEach(function (key) { args[key] = supplied[key]; });
261
+
262
+ var hostId = "connecta-ui-read-" + String(++nextHostRequestId);
263
+ pendingHostReads[hostId] = { innerId: message.id };
264
+ activeHostReads++;
265
+ send({
266
+ jsonrpc: "2.0",
267
+ id: hostId,
268
+ method: "tools/call",
269
+ params: {
270
+ name: "call_tool",
271
+ arguments: {
272
+ address: binding.address,
273
+ args: args,
274
+ resultMode: "value"
275
+ }
276
+ }
277
+ });
278
+ }
279
+
280
+ function finishHostRead(message) {
281
+ var pending = pendingHostReads[message.id];
282
+ if (!pending) return false;
283
+ delete pendingHostReads[message.id];
284
+ activeHostReads--;
285
+ if (message.error) {
286
+ finishInnerRead(pending.innerId, false, readError(message.error, "Host rejected read"));
287
+ return true;
288
+ }
289
+ var toolResult = message.result;
290
+ if (!toolResult || typeof toolResult !== "object") {
291
+ finishInnerRead(pending.innerId, false, "Host returned an invalid tool result");
292
+ return true;
293
+ }
294
+ var structured = toolResult.structuredContent;
295
+ if (toolResult.isError || (structured && structured.ok === false)) {
296
+ var detail = structured && structured.error;
297
+ var content = Array.isArray(toolResult.content)
298
+ ? toolResult.content.find(function (block) { return block && block.type === "text"; })
299
+ : null;
300
+ finishInnerRead(
301
+ pending.innerId,
302
+ false,
303
+ readError(detail, content && content.text ? content.text : "Read failed")
304
+ );
305
+ return true;
306
+ }
307
+ var value = structured && structured.ok === true &&
308
+ Object.prototype.hasOwnProperty.call(structured, "data")
309
+ ? structured.data
310
+ : structured !== undefined
311
+ ? structured
312
+ : toolResult;
313
+ finishInnerRead(pending.innerId, true, value);
314
+ return true;
315
+ }
316
+
133
317
  window.addEventListener("message", function (event) {
134
- if (event.source !== host) return;
135
318
  var message = event.data;
319
+ if (event.source === view.contentWindow) {
320
+ if (message && message.type === "connecta/read") beginInnerRead(message);
321
+ return;
322
+ }
323
+ if (event.source !== host) return;
136
324
  if (!message || message.jsonrpc !== "2.0") return;
325
+ if (message.id !== undefined && finishHostRead(message)) return;
137
326
  if (message.method === "ui/notifications/tool-result") {
138
327
  render(message.params);
139
328
  return;
@@ -151,6 +340,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
151
340
  // response carries the same id, and announcing initialization on one
152
341
  // would assert a handshake that never happened.
153
342
  if (message.id === initializeId && message.result !== undefined) {
343
+ var capabilities = message.result.hostCapabilities;
344
+ hostCanCallTools = Boolean(capabilities && capabilities.serverTools);
154
345
  notify("ui/notifications/initialized", {});
155
346
  }
156
347
  });
@@ -25,6 +25,8 @@ import type {
25
25
  } from "./registry.js";
26
26
  import {
27
27
  connectorGuide,
28
+ connectorGuideRequired,
29
+ connectorGuideSummary,
28
30
  connectorSkillName,
29
31
  } from "./skills.js";
30
32
  import {
@@ -196,6 +198,7 @@ export interface CatalogDescribeArgs {
196
198
  interface CatalogSearchEntry {
197
199
  connector: Connector;
198
200
  guide?: string;
201
+ guideSummary?: string;
199
202
  tool: {
200
203
  name: string;
201
204
  address: string;
@@ -208,9 +211,34 @@ interface CatalogSearchEntry {
208
211
  requiredInputKeys?: string[];
209
212
  outputKeys?: string[];
210
213
  annotations?: ToolDef["annotations"];
214
+ guideRequired?: true;
215
+ guideRequiredReasons?: GuideRequiredReason[];
211
216
  };
212
217
  }
213
218
 
219
+ type GuideRequiredReason =
220
+ | "connector_required"
221
+ | "approval_required"
222
+ | "schema_truncated";
223
+
224
+ /**
225
+ * Reasons discovery can determine without reading arguments or guessing at a
226
+ * task. Summary-only conventions remain an agent decision; hard requirements
227
+ * are explicit and machine-readable.
228
+ */
229
+ function guideRequiredReasons(
230
+ connector: Connector,
231
+ tool: ToolDef,
232
+ schemaTruncated: boolean,
233
+ ): GuideRequiredReason[] | undefined {
234
+ if (!connectorGuide(connector)) return undefined;
235
+ const reasons: GuideRequiredReason[] = [];
236
+ if (connectorGuideRequired(connector)) reasons.push("connector_required");
237
+ if (!isExplicitlyReadOnly(tool)) reasons.push("approval_required");
238
+ if (schemaTruncated) reasons.push("schema_truncated");
239
+ return reasons.length > 0 ? reasons : undefined;
240
+ }
241
+
214
242
  /**
215
243
  * Code-mode key metadata for one match. Each half is omitted when its schema
216
244
  * does not resolve to an object shape, so a program reads "no metadata, use the
@@ -237,6 +265,19 @@ function schemaKeyMetadata(
237
265
  };
238
266
  }
239
267
 
268
+ /**
269
+ * The classified-failure subset a scoped search may echo: enough to tell a
270
+ * transient outage from one an operator must clear, and nothing more. Kept as
271
+ * its own type rather than `CallErrorDetails` so widening the call-path
272
+ * classifier cannot widen this discovery-surface field by accident.
273
+ */
274
+ interface CatalogFailureDetail {
275
+ code: string;
276
+ message: string;
277
+ retryable: boolean;
278
+ retryAfterMs?: number;
279
+ }
280
+
240
281
  export interface CatalogSearchPage {
241
282
  entries: CatalogSearchEntry[];
242
283
  total: number;
@@ -253,6 +294,12 @@ export interface CatalogSearchPage {
253
294
  connectorScope?: string;
254
295
  unknownConnector?: true;
255
296
  unavailableConnectorCount?: number;
297
+ /** Bounded typed failure for an explicitly scoped unavailable catalog. */
298
+ catalogError?: CatalogFailureDetail;
299
+ guide?: string;
300
+ guideSummary?: string;
301
+ guideRequired?: true;
302
+ guideRequiredReasons?: GuideRequiredReason[];
256
303
  guidance?: string;
257
304
  };
258
305
  }
@@ -262,6 +309,9 @@ export interface CatalogDescription {
262
309
  name?: string;
263
310
  description?: string;
264
311
  guide?: string;
312
+ guideSummary?: string;
313
+ guideRequired?: true;
314
+ guideRequiredReasons?: GuideRequiredReason[];
265
315
  inputSchema?: unknown;
266
316
  outputSchema?: unknown;
267
317
  annotations?: ToolDef["annotations"];
@@ -656,10 +706,19 @@ export class CatalogService {
656
706
  match.tool.description,
657
707
  args.fullDescriptions === true,
658
708
  );
709
+ const requiredReasons = guideRequiredReasons(
710
+ match.connector,
711
+ match.tool,
712
+ renderedInput?.truncated === true || renderedOutput?.truncated === true,
713
+ );
714
+ const guideSummary = connectorGuideSummary(match.connector);
659
715
  return {
660
716
  connector: match.connector,
661
717
  ...(connectorGuide(match.connector)
662
- ? { guide: connectorSkillName(match.connector.id) }
718
+ ? {
719
+ guide: connectorSkillName(match.connector.id),
720
+ ...(guideSummary ? { guideSummary } : {}),
721
+ }
663
722
  : {}),
664
723
  tool: {
665
724
  name: match.tool.name,
@@ -697,6 +756,12 @@ export class CatalogService {
697
756
  ...(match.tool.annotations
698
757
  ? { annotations: match.tool.annotations }
699
758
  : {}),
759
+ ...(requiredReasons
760
+ ? {
761
+ guideRequired: true as const,
762
+ guideRequiredReasons: requiredReasons,
763
+ }
764
+ : {}),
700
765
  },
701
766
  };
702
767
  });
@@ -732,6 +797,27 @@ export class CatalogService {
732
797
  const unavailableCatalogs = catalogs.filter(
733
798
  (catalog) => catalog.status === "rejected",
734
799
  ).length;
800
+ // Named field by field rather than spread: `CallErrorDetails` also carries
801
+ // connector, operation, recovery, and nextAction, and a discovery read is
802
+ // not a call — widening the classifier must not silently widen what a
803
+ // catalog search hands back.
804
+ const scopedCatalogError = ((): CatalogFailureDetail | undefined => {
805
+ if (!scopedConnector || catalogs[0]?.status !== "rejected") {
806
+ return undefined;
807
+ }
808
+ const error = classifyCallError(
809
+ catalogs[0].reason,
810
+ "catalog_lookup_failed",
811
+ );
812
+ return {
813
+ code: error.code,
814
+ message: boundedEchoText(error.message),
815
+ retryable: error.retryable,
816
+ ...(error.retryAfterMs === undefined
817
+ ? {}
818
+ : { retryAfterMs: error.retryAfterMs }),
819
+ };
820
+ })();
735
821
  const safetyLabel =
736
822
  safety === "readOnly"
737
823
  ? "read-only "
@@ -740,6 +826,14 @@ export class CatalogService {
740
826
  : "";
741
827
  const filterRecovery =
742
828
  safety === "all" ? "" : " Change safety to inspect the other tools.";
829
+ const scopedGuide =
830
+ matches.length === 0 && scopedConnector && connectorGuide(scopedConnector)
831
+ ? {
832
+ guide: connectorSkillName(scopedConnector.id),
833
+ guideSummary: connectorGuideSummary(scopedConnector),
834
+ required: connectorGuideRequired(scopedConnector),
835
+ }
836
+ : undefined;
743
837
  const guidance =
744
838
  queryTerms.length === 0
745
839
  ? undefined
@@ -748,8 +842,10 @@ export class CatalogService {
748
842
  ? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
749
843
  : scopedConnector
750
844
  ? unavailableCatalogs > 0
751
- ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Retry later.`
752
- : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
845
+ ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
846
+ : scopedGuide?.required
847
+ ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
848
+ : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
753
849
  : unavailableCatalogs === 0
754
850
  ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
755
851
  : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
@@ -789,6 +885,23 @@ export class CatalogService {
789
885
  ...(unavailableCatalogs > 0
790
886
  ? { unavailableConnectorCount: unavailableCatalogs }
791
887
  : {}),
888
+ ...(scopedCatalogError ? { catalogError: scopedCatalogError } : {}),
889
+ ...(scopedGuide
890
+ ? {
891
+ guide: scopedGuide.guide,
892
+ ...(scopedGuide.guideSummary
893
+ ? { guideSummary: scopedGuide.guideSummary }
894
+ : {}),
895
+ ...(scopedGuide.required
896
+ ? {
897
+ guideRequired: true as const,
898
+ guideRequiredReasons: [
899
+ "connector_required" as const,
900
+ ],
901
+ }
902
+ : {}),
903
+ }
904
+ : {}),
792
905
  ...(guidance ? { guidance } : {}),
793
906
  },
794
907
  }
@@ -853,12 +966,27 @@ export class CatalogService {
853
966
  tool.description,
854
967
  args.fullDescriptions === true,
855
968
  );
969
+ const requiredReasons = guideRequiredReasons(
970
+ addressResolution.connector,
971
+ tool,
972
+ false,
973
+ );
974
+ const guideSummary = connectorGuideSummary(addressResolution.connector);
856
975
  return {
857
976
  address,
858
977
  name: tool.name,
859
978
  ...(description !== undefined ? { description } : {}),
860
979
  ...(connectorGuide(addressResolution.connector)
861
- ? { guide: connectorSkillName(addressResolution.connector.id) }
980
+ ? {
981
+ guide: connectorSkillName(addressResolution.connector.id),
982
+ ...(guideSummary ? { guideSummary } : {}),
983
+ }
984
+ : {}),
985
+ ...(requiredReasons
986
+ ? {
987
+ guideRequired: true as const,
988
+ guideRequiredReasons: requiredReasons,
989
+ }
862
990
  : {}),
863
991
  inputSchema: renderSchema(input, format),
864
992
  ...(tool.outputSchema
@@ -877,6 +1005,7 @@ export function groupedSearchResult(page: CatalogSearchPage) {
877
1005
  id: string;
878
1006
  title?: string;
879
1007
  guide?: string;
1008
+ guideSummary?: string;
880
1009
  tools: CatalogSearchEntry["tool"][];
881
1010
  }> = [];
882
1011
  const byConnector = new Map<string, (typeof groups)[number]>();
@@ -889,6 +1018,9 @@ export function groupedSearchResult(page: CatalogSearchPage) {
889
1018
  id: entry.connector.id,
890
1019
  ...(entry.connector.title ? { title: entry.connector.title } : {}),
891
1020
  ...(entry.guide ? { guide: entry.guide } : {}),
1021
+ ...(entry.guideSummary
1022
+ ? { guideSummary: entry.guideSummary }
1023
+ : {}),
892
1024
  tools: [],
893
1025
  };
894
1026
  byConnector.set(entry.connector.id, group);
@@ -913,6 +1045,9 @@ export function flatSearchResult(page: CatalogSearchPage) {
913
1045
  tools: page.entries.map((entry) => ({
914
1046
  ...entry.tool,
915
1047
  ...(entry.guide ? { guide: entry.guide } : {}),
1048
+ ...(entry.guideSummary
1049
+ ? { guideSummary: entry.guideSummary }
1050
+ : {}),
916
1051
  })),
917
1052
  total: page.total,
918
1053
  offset: page.offset,
@@ -5,6 +5,7 @@ import type {
5
5
  ConnectorCredentialConfig,
6
6
  ConnectorCredentialValues,
7
7
  ConnectorContext,
8
+ ConnectorUsageGuide,
8
9
  CredentialTestResult,
9
10
  JsonSchema,
10
11
  ToolAnnotations,
@@ -41,10 +42,11 @@ export interface ApiOptions {
41
42
  /** Optional per-runtime downstream call-admission policy. */
42
43
  callAdmission?: ConnectorCallAdmissionPolicy;
43
44
  /**
44
- * Optional agent-facing usage guide (markdown) served by the `skills`
45
- * meta-tool as `connector:<id>`. See `Connector.usageGuide`.
45
+ * Optional agent-facing usage guide served by `skills` as
46
+ * `connector:<id>`. A string is markdown; the structured form adds bounded
47
+ * discovery metadata. See `Connector.usageGuide`.
46
48
  */
47
- usageGuide?: string;
49
+ usageGuide?: string | ConnectorUsageGuide;
48
50
  /** Optional operator-managed credential exposed through ctx.credential and /credentials. */
49
51
  credential?: ConnectorCredentialConfig;
50
52
  /** Optional validation behind /credentials' Test action. */
@@ -21,6 +21,7 @@ import type {
21
21
  ConnectorCallAdmissionPolicy,
22
22
  ConnectorContext,
23
23
  ConnectorStatus,
24
+ ConnectorUsageGuide,
24
25
  Logger,
25
26
  ToolDef,
26
27
  } from "../types.js";
@@ -47,10 +48,11 @@ export interface RemoteMcpOptions {
47
48
  /** Optional per-runtime downstream call-admission policy. */
48
49
  callAdmission?: ConnectorCallAdmissionPolicy;
49
50
  /**
50
- * Optional agent-facing usage guide (markdown) served by the `skills`
51
- * meta-tool as `connector:<id>`. See `Connector.usageGuide`.
51
+ * Optional agent-facing usage guide served by `skills` as
52
+ * `connector:<id>`. A string is markdown; the structured form adds bounded
53
+ * discovery metadata. See `Connector.usageGuide`.
52
54
  */
53
- usageGuide?: string;
55
+ usageGuide?: string | ConnectorUsageGuide;
54
56
  auth?: RemoteMcpAuth;
55
57
  /**
56
58
  * Downstream HTTP redirect policy. Defaults to `"none"`: every redirect is