@databricks/appkit 0.32.0 → 0.33.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 (153) hide show
  1. package/CLAUDE.md +53 -1
  2. package/NOTICE.md +1 -0
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +8 -3
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/appkit/package.js +1 -1
  7. package/dist/beta.d.ts +5 -3
  8. package/dist/beta.js +3 -1
  9. package/dist/connectors/mcp/client.d.ts +27 -2
  10. package/dist/connectors/mcp/client.d.ts.map +1 -1
  11. package/dist/connectors/mcp/client.js +117 -18
  12. package/dist/connectors/mcp/client.js.map +1 -1
  13. package/dist/connectors/mcp/index.d.ts +1 -1
  14. package/dist/connectors/mcp/types.d.ts +1 -1
  15. package/dist/core/agent/build-toolkit.js +3 -8
  16. package/dist/core/agent/build-toolkit.js.map +1 -1
  17. package/dist/core/agent/load-agents.d.ts +6 -1
  18. package/dist/core/agent/load-agents.d.ts.map +1 -1
  19. package/dist/core/agent/load-agents.js +67 -27
  20. package/dist/core/agent/load-agents.js.map +1 -1
  21. package/dist/core/agent/plugins-map.js +44 -0
  22. package/dist/core/agent/plugins-map.js.map +1 -0
  23. package/dist/core/agent/run-agent.d.ts +31 -7
  24. package/dist/core/agent/run-agent.d.ts.map +1 -1
  25. package/dist/core/agent/run-agent.js +138 -27
  26. package/dist/core/agent/run-agent.js.map +1 -1
  27. package/dist/core/agent/toolkit-options.js +28 -0
  28. package/dist/core/agent/toolkit-options.js.map +1 -0
  29. package/dist/core/agent/toolkit-resolver.js +44 -0
  30. package/dist/core/agent/toolkit-resolver.js.map +1 -0
  31. package/dist/core/agent/tools/define-tool.d.ts +14 -2
  32. package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
  33. package/dist/core/agent/tools/define-tool.js +1 -1
  34. package/dist/core/agent/tools/define-tool.js.map +1 -1
  35. package/dist/core/agent/tools/function-tool.d.ts +13 -2
  36. package/dist/core/agent/tools/function-tool.d.ts.map +1 -1
  37. package/dist/core/agent/tools/function-tool.js +4 -3
  38. package/dist/core/agent/tools/function-tool.js.map +1 -1
  39. package/dist/core/agent/tools/index.d.ts +1 -1
  40. package/dist/core/agent/tools/tool.d.ts +32 -3
  41. package/dist/core/agent/tools/tool.d.ts.map +1 -1
  42. package/dist/core/agent/tools/tool.js +4 -3
  43. package/dist/core/agent/tools/tool.js.map +1 -1
  44. package/dist/core/agent/types.d.ts +95 -10
  45. package/dist/core/agent/types.d.ts.map +1 -1
  46. package/dist/core/agent/types.js.map +1 -1
  47. package/dist/plugin/index.d.ts +1 -1
  48. package/dist/plugin/to-plugin.d.ts +3 -13
  49. package/dist/plugin/to-plugin.d.ts.map +1 -1
  50. package/dist/plugin/to-plugin.js +1 -8
  51. package/dist/plugin/to-plugin.js.map +1 -1
  52. package/dist/plugins/agents/agents.d.ts +184 -2
  53. package/dist/plugins/agents/agents.d.ts.map +1 -0
  54. package/dist/plugins/agents/agents.js +169 -72
  55. package/dist/plugins/agents/agents.js.map +1 -1
  56. package/dist/plugins/agents/index.d.ts +2 -2
  57. package/dist/plugins/agents/index.js +1 -1
  58. package/dist/plugins/agents/manifest.js +4 -5
  59. package/dist/plugins/agents/tool-approval-gate.js.map +1 -1
  60. package/dist/plugins/analytics/analytics.d.ts +3 -4
  61. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  62. package/dist/plugins/analytics/analytics.js +8 -6
  63. package/dist/plugins/analytics/analytics.js.map +1 -1
  64. package/dist/plugins/analytics/index.js +1 -0
  65. package/dist/plugins/analytics/types.js +15 -0
  66. package/dist/plugins/analytics/types.js.map +1 -0
  67. package/dist/plugins/beta-exports.generated.d.ts +2 -0
  68. package/dist/plugins/beta-exports.generated.js +4 -0
  69. package/dist/plugins/files/plugin.d.ts +1 -2
  70. package/dist/plugins/files/plugin.d.ts.map +1 -1
  71. package/dist/plugins/files/plugin.js +30 -12
  72. package/dist/plugins/files/plugin.js.map +1 -1
  73. package/dist/plugins/genie/genie.d.ts +5 -4
  74. package/dist/plugins/genie/genie.d.ts.map +1 -1
  75. package/dist/plugins/genie/genie.js +22 -8
  76. package/dist/plugins/genie/genie.js.map +1 -1
  77. package/dist/plugins/genie/types.d.ts +10 -2
  78. package/dist/plugins/genie/types.d.ts.map +1 -1
  79. package/dist/plugins/jobs/plugin.d.ts +1 -2
  80. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  81. package/dist/plugins/lakebase/lakebase.d.ts +1 -2
  82. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  83. package/dist/plugins/lakebase/lakebase.js +3 -3
  84. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  85. package/dist/plugins/lakebase/types.d.ts +5 -4
  86. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  87. package/dist/plugins/server/index.d.ts +3 -2
  88. package/dist/plugins/server/index.d.ts.map +1 -1
  89. package/dist/plugins/server/index.js +8 -5
  90. package/dist/plugins/server/index.js.map +1 -1
  91. package/dist/plugins/server/types.d.ts +11 -0
  92. package/dist/plugins/server/types.d.ts.map +1 -1
  93. package/dist/plugins/serving/serving.d.ts +1 -2
  94. package/dist/plugins/serving/serving.d.ts.map +1 -1
  95. package/dist/shared/src/agent.d.ts +16 -4
  96. package/dist/shared/src/agent.d.ts.map +1 -1
  97. package/docs/api/appkit/Class.AppKitMcpClient.md +157 -0
  98. package/docs/api/appkit/Class.DatabricksAdapter.md +151 -0
  99. package/docs/api/appkit/Function.agentIdFromMarkdownPath.md +18 -0
  100. package/docs/api/appkit/Function.createAgent.md +33 -0
  101. package/docs/api/appkit/Function.defineTool.md +26 -0
  102. package/docs/api/appkit/Function.executeFromRegistry.md +25 -0
  103. package/docs/api/appkit/Function.functionToolToDefinition.md +16 -0
  104. package/docs/api/appkit/Function.isFunctionTool.md +16 -0
  105. package/docs/api/appkit/Function.isHostedTool.md +16 -0
  106. package/docs/api/appkit/Function.isToolkitEntry.md +18 -0
  107. package/docs/api/appkit/Function.loadAgentFromFile.md +21 -0
  108. package/docs/api/appkit/Function.loadAgentsFromDir.md +26 -0
  109. package/docs/api/appkit/Function.mcpServer.md +28 -0
  110. package/docs/api/appkit/Function.parseTextToolCalls.md +26 -0
  111. package/docs/api/appkit/Function.resolveHostedTools.md +16 -0
  112. package/docs/api/appkit/Function.runAgent.md +26 -0
  113. package/docs/api/appkit/Function.tool.md +28 -0
  114. package/docs/api/appkit/Function.toolsFromRegistry.md +20 -0
  115. package/docs/api/appkit/Interface.AgentAdapter.md +21 -0
  116. package/docs/api/appkit/Interface.AgentDefinition.md +112 -0
  117. package/docs/api/appkit/Interface.AgentInput.md +37 -0
  118. package/docs/api/appkit/Interface.AgentRunContext.md +32 -0
  119. package/docs/api/appkit/Interface.AgentToolDefinition.md +37 -0
  120. package/docs/api/appkit/Interface.AgentsPluginConfig.md +241 -0
  121. package/docs/api/appkit/Interface.AutoInheritToolsConfig.md +27 -0
  122. package/docs/api/appkit/Interface.BasePluginConfig.md +1 -0
  123. package/docs/api/appkit/Interface.FunctionTool.md +80 -0
  124. package/docs/api/appkit/Interface.McpConnectAllResult.md +38 -0
  125. package/docs/api/appkit/Interface.Message.md +55 -0
  126. package/docs/api/appkit/Interface.PluginToolkitProvider.md +22 -0
  127. package/docs/api/appkit/Interface.PromptContext.md +30 -0
  128. package/docs/api/appkit/Interface.RegisteredAgent.md +75 -0
  129. package/docs/api/appkit/Interface.RunAgentInput.md +34 -0
  130. package/docs/api/appkit/Interface.RunAgentResult.md +23 -0
  131. package/docs/api/appkit/Interface.Thread.md +46 -0
  132. package/docs/api/appkit/Interface.ThreadStore.md +103 -0
  133. package/docs/api/appkit/Interface.ToolAnnotations.md +56 -0
  134. package/docs/api/appkit/Interface.ToolConfig.md +72 -0
  135. package/docs/api/appkit/Interface.ToolEntry.md +73 -0
  136. package/docs/api/appkit/Interface.ToolProvider.md +38 -0
  137. package/docs/api/appkit/Interface.ToolkitEntry.md +59 -0
  138. package/docs/api/appkit/Interface.ToolkitOptions.md +45 -0
  139. package/docs/api/appkit/TypeAlias.AgentEvent.md +299 -0
  140. package/docs/api/appkit/TypeAlias.AgentTool.md +11 -0
  141. package/docs/api/appkit/TypeAlias.AgentTools.md +8 -0
  142. package/docs/api/appkit/TypeAlias.AgentToolsFn.md +20 -0
  143. package/docs/api/appkit/TypeAlias.BaseSystemPromptOption.md +9 -0
  144. package/docs/api/appkit/TypeAlias.HostedTool.md +10 -0
  145. package/docs/api/appkit/TypeAlias.Plugins.md +26 -0
  146. package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +29 -0
  147. package/docs/api/appkit/TypeAlias.ToolRegistry.md +6 -0
  148. package/docs/api/appkit/Variable.agents.md +19 -0
  149. package/docs/api/appkit.md +113 -62
  150. package/docs/plugins/agents.md +441 -0
  151. package/llms.txt +53 -1
  152. package/package.json +1 -1
  153. package/sbom.cdx.json +1 -1
@@ -4,6 +4,67 @@ import { assertResolvedHostSafe, checkMcpUrl } from "./host-policy.js";
4
4
  //#region src/connectors/mcp/client.ts
5
5
  const logger = createLogger("connector:mcp");
6
6
  /**
7
+ * Hard cap on the size of a single MCP response body, including SSE
8
+ * frames bundled into one HTTP response. MCP `initialize` / `tools/list`
9
+ * / `tools/call` responses are JSON-RPC payloads — single-digit kilobytes
10
+ * in normal use. A response anywhere near this size signals either a
11
+ * misbehaving server or an attempt to exhaust client memory; we'd rather
12
+ * fail loudly than allocate unbounded buffers from a remote.
13
+ */
14
+ const MCP_RESPONSE_BODY_LIMIT_BYTES = 1024 * 1024;
15
+ /**
16
+ * Read a fetch Response body into a string with a hard size cap. Aborts
17
+ * and throws if the cumulative bytes read cross {@link
18
+ * MCP_RESPONSE_BODY_LIMIT_BYTES}, so a remote server cannot keep
19
+ * streaming data past the limit. Returns the empty string when the
20
+ * response has no readable body.
21
+ */
22
+ /**
23
+ * Empty-object fallback used when an MCP server ships a missing or
24
+ * malformed `inputSchema`. Matches the shape downstream adapters expect
25
+ * for a function tool that takes no arguments.
26
+ */
27
+ const EMPTY_TOOL_PARAMETERS = {
28
+ type: "object",
29
+ properties: {}
30
+ };
31
+ /**
32
+ * Coerce a remote MCP server's reported `inputSchema` into the
33
+ * JSONSchema7 shape AppKit's adapters expect for a function tool's
34
+ * `parameters`. The MCP wire type is `Record<string, unknown>`, so a
35
+ * misbehaving (or malicious) server could ship arbitrary JSON. We accept
36
+ * only the standard `{ type: "object", properties: {...} }` shape and
37
+ * fall back to an empty-parameters schema otherwise — the tool still
38
+ * registers, it just can't accept arguments.
39
+ */
40
+ function coerceToolParameters(inputSchema) {
41
+ if (!inputSchema || typeof inputSchema !== "object") return EMPTY_TOOL_PARAMETERS;
42
+ const { type, properties } = inputSchema;
43
+ if (type !== "object") return EMPTY_TOOL_PARAMETERS;
44
+ if (properties !== void 0 && (typeof properties !== "object" || properties === null || Array.isArray(properties))) return EMPTY_TOOL_PARAMETERS;
45
+ return inputSchema;
46
+ }
47
+ async function readResponseTextCapped(response, maxBytes, contextLabel) {
48
+ if (!response.body) return "";
49
+ const reader = response.body.getReader();
50
+ const decoder = new TextDecoder("utf-8");
51
+ let total = 0;
52
+ let out = "";
53
+ try {
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ total += value.byteLength;
58
+ if (total > maxBytes) throw new Error(`MCP ${contextLabel}: response body exceeded ${maxBytes} bytes — refusing to allocate unbounded buffer from a remote server.`);
59
+ out += decoder.decode(value, { stream: true });
60
+ }
61
+ out += decoder.decode();
62
+ } finally {
63
+ reader.releaseLock();
64
+ }
65
+ return out;
66
+ }
67
+ /**
7
68
  * Lightweight MCP client for Databricks-hosted MCP servers.
8
69
  *
9
70
  * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk
@@ -30,9 +91,39 @@ var AppKitMcpClient = class {
30
91
  this.policy = policy;
31
92
  this.options = options;
32
93
  }
94
+ /**
95
+ * Connects every endpoint in parallel and returns a structured summary so
96
+ * callers can distinguish "all connected" from "some failed".
97
+ *
98
+ * Returning the result instead of throwing is deliberate: one
99
+ * misconfigured MCP server should not take down the entire agents plugin
100
+ * at boot, and the agents plugin uses the summary to warn at startup with
101
+ * the failed-endpoint names. Errors are also logged here so a caller
102
+ * that ignores the return still gets per-endpoint diagnostics.
103
+ *
104
+ * @returns `connected` lists the endpoint names that initialised
105
+ * successfully; `failed` carries `{ name, error }` for the rest.
106
+ */
33
107
  async connectAll(endpoints) {
34
108
  const results = await Promise.allSettled(endpoints.map((ep) => this.connect(ep)));
35
- for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") logger.error("Failed to connect MCP server %s: %O", endpoints[i].name, results[i].reason);
109
+ const out = {
110
+ connected: [],
111
+ failed: []
112
+ };
113
+ for (let i = 0; i < results.length; i++) {
114
+ const r = results[i];
115
+ const name = endpoints[i].name;
116
+ if (r.status === "fulfilled") out.connected.push(name);
117
+ else {
118
+ const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
119
+ logger.error("Failed to connect MCP server %s: %O", name, error);
120
+ out.failed.push({
121
+ name,
122
+ error
123
+ });
124
+ }
125
+ }
126
+ return out;
36
127
  }
37
128
  resolveUrl(endpoint) {
38
129
  if (endpoint.url.startsWith("http://") || endpoint.url.startsWith("https://")) return endpoint.url;
@@ -77,10 +168,7 @@ var AppKitMcpClient = class {
77
168
  for (const [serverName, conn] of this.connections) for (const [toolName, schema] of conn.tools) defs.push({
78
169
  name: `mcp.${serverName}.${toolName}`,
79
170
  description: schema.description ?? toolName,
80
- parameters: schema.inputSchema ?? {
81
- type: "object",
82
- properties: {}
83
- }
171
+ parameters: coerceToolParameters(schema.inputSchema)
84
172
  });
85
173
  return defs;
86
174
  }
@@ -111,11 +199,12 @@ var AppKitMcpClient = class {
111
199
  forwardWorkspaceAuth: conn.forwardWorkspaceAuth,
112
200
  callerSignal
113
201
  })).result;
202
+ const textContent = (result.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string");
114
203
  if (result.isError) {
115
- const errText = (result.content ?? []).filter((c) => c.type === "text").map((c) => c.text).join("\n");
204
+ const errText = textContent.map((c) => c.text).join("\n");
116
205
  throw new Error(errText || "MCP tool call failed");
117
206
  }
118
- return (result.content ?? []).filter((c) => c.type === "text").map((c) => c.text).join("\n");
207
+ return textContent.map((c) => c.text).join("\n");
119
208
  }
120
209
  async close() {
121
210
  this.closed = true;
@@ -148,12 +237,16 @@ var AppKitMcpClient = class {
148
237
  });
149
238
  if (!response.ok) throw new Error(`MCP request to ${method} failed: ${response.status} ${response.statusText}`);
150
239
  const contentType = response.headers.get("content-type") ?? "";
240
+ const bodyText = await readResponseTextCapped(response, MCP_RESPONSE_BODY_LIMIT_BYTES, method);
151
241
  let json;
152
242
  if (contentType.includes("text/event-stream")) {
153
- const lastData = (await response.text()).split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).pop();
243
+ const lastData = bodyText.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).pop();
154
244
  if (!lastData) throw new Error(`MCP SSE response for ${method} contained no data`);
155
245
  json = JSON.parse(lastData);
156
- } else json = await response.json();
246
+ } else {
247
+ if (bodyText.length === 0) throw new Error(`MCP response for ${method} had an empty body`);
248
+ json = JSON.parse(bodyText);
249
+ }
157
250
  if (json.error) throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);
158
251
  const sid = response.headers.get("mcp-session-id") ?? void 0;
159
252
  return {
@@ -170,15 +263,21 @@ var AppKitMcpClient = class {
170
263
  ...authHeaders
171
264
  };
172
265
  if (options?.sessionId) headers["Mcp-Session-Id"] = options.sessionId;
173
- await (this.options.fetchImpl ?? fetch)(url, {
174
- method: "POST",
175
- headers,
176
- body: JSON.stringify({
177
- jsonrpc: "2.0",
178
- method
179
- }),
180
- signal: AbortSignal.timeout(3e4)
181
- });
266
+ const fetchImpl = this.options.fetchImpl ?? fetch;
267
+ try {
268
+ const response = await fetchImpl(url, {
269
+ method: "POST",
270
+ headers,
271
+ body: JSON.stringify({
272
+ jsonrpc: "2.0",
273
+ method
274
+ }),
275
+ signal: AbortSignal.timeout(3e4)
276
+ });
277
+ if (!response.ok) logger.warn("MCP notification %s to %s returned %d %s — the server may have rejected the request, but per MCP spec notifications are fire-and-forget and the connection is considered established.", method, url, response.status, response.statusText);
278
+ } catch (err) {
279
+ logger.warn("MCP notification %s to %s failed before a response was received: %O", method, url, err);
280
+ }
182
281
  }
183
282
  /**
184
283
  * Return the auth headers to send on an outbound request. Workspace auth
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"sourcesContent":["/**\n * Custom MCP over HTTP (Streamable) — not `@modelcontextprotocol/sdk`\n *\n * This module implements a tiny JSON-RPC 2.0 client on `fetch` for the subset\n * of MCP we need: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call` over a single JSON request/response. We do not use the official\n * SDK because:\n *\n * - **Policy and auth are the product** — every outbound URL is checked with\n * {@link McpHostPolicy} (allowlist, DNS, private/blocked IP ranges) before\n * the first byte is sent, and workspace tokens are only forwarded when\n * `forwardWorkspaceAuth` is true for that destination. A generic transport\n * from the SDK would still need the same hooks; re-wrapping it would be\n * about as much code, with a larger third-party surface to audit.\n * - **Narrow scope** — we only target Databricks-hosted MCP over Streamable\n * HTTP, not stdio, full SSE sessions, or the rest of the protocol. A\n * hand-rolled path keeps the call graph obvious in code review.\n * - **Zero extra runtime dependency** for this path, consistent with other\n * small, security-sensitive AppKit pieces.\n *\n * Revisit if we add more transports, or if the SDK ships a first-class way to\n * inject our host policy and per-URL auth without fighting the default\n * transport.\n */\nimport type { AgentToolDefinition } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n assertResolvedHostSafe,\n checkMcpUrl,\n type DnsLookup,\n type McpHostPolicy,\n} from \"./host-policy\";\nimport type { McpEndpointConfig } from \"./types\";\n\nconst logger = createLogger(\"connector:mcp\");\n\ninterface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: number;\n method: string;\n params?: Record<string, unknown>;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: number;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\ninterface McpToolSchema {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n}\n\ninterface McpToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\ninterface McpServerConnection {\n config: McpEndpointConfig;\n resolvedUrl: string;\n /**\n * Whether workspace auth (SP / OBO) may be forwarded to this endpoint's URL.\n * Decided at `connect()` time via {@link McpHostPolicy} and cached for the\n * lifetime of the connection.\n */\n forwardWorkspaceAuth: boolean;\n tools: Map<string, McpToolSchema>;\n}\n\n/**\n * Lightweight MCP client for Databricks-hosted MCP servers.\n *\n * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk\n * or LangChain dependency. Supports the Streamable HTTP transport only\n * (POST with JSON-RPC request, single JSON-RPC response). Implements exactly\n * four methods: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call`. No prompts/resources/completion/sampling.\n *\n * All outbound URLs are gated by an {@link McpHostPolicy}: unallowlisted hosts\n * are rejected before the first byte is sent, and workspace credentials are\n * only forwarded to the same-origin workspace. See `mcp-host-policy.ts`.\n *\n * Rationale for hand-rolling JSON-RPC instead of `@modelcontextprotocol/sdk`:\n * see the file-level comment at the top of this module.\n */\nexport class AppKitMcpClient {\n private connections = new Map<string, McpServerConnection>();\n private sessionIds = new Map<string, string>();\n private requestId = 0;\n private closed = false;\n\n constructor(\n private workspaceHost: string,\n private authenticate: () => Promise<Record<string, string>>,\n private policy: McpHostPolicy,\n private options: { dnsLookup?: DnsLookup; fetchImpl?: typeof fetch } = {},\n ) {}\n\n async connectAll(endpoints: McpEndpointConfig[]): Promise<void> {\n const results = await Promise.allSettled(\n endpoints.map((ep) => this.connect(ep)),\n );\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n logger.error(\n \"Failed to connect MCP server %s: %O\",\n endpoints[i].name,\n (results[i] as PromiseRejectedResult).reason,\n );\n }\n }\n }\n\n private resolveUrl(endpoint: McpEndpointConfig): string {\n if (\n endpoint.url.startsWith(\"http://\") ||\n endpoint.url.startsWith(\"https://\")\n ) {\n return endpoint.url;\n }\n return `${this.workspaceHost}${endpoint.url}`;\n }\n\n async connect(endpoint: McpEndpointConfig): Promise<void> {\n const resolvedUrl = this.resolveUrl(endpoint);\n const check = checkMcpUrl(resolvedUrl, this.policy);\n if (!check.ok) {\n throw new Error(\n `MCP endpoint '${endpoint.name}' refused at connect: ${check.reason}`,\n );\n }\n await assertResolvedHostSafe(\n check.url.hostname,\n this.policy,\n this.options.dnsLookup,\n );\n\n logger.info(\n \"Connecting to MCP server: %s at %s (forwardWorkspaceAuth=%s)\",\n endpoint.name,\n resolvedUrl,\n check.forwardWorkspaceAuth,\n );\n\n const initResponse = await this.sendRpc(\n resolvedUrl,\n \"initialize\",\n {\n protocolVersion: \"2025-03-26\",\n capabilities: {},\n clientInfo: { name: \"appkit-agent\", version: \"0.1.0\" },\n },\n { forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n\n if (initResponse.sessionId) {\n this.sessionIds.set(endpoint.name, initResponse.sessionId);\n }\n const sessionId = this.sessionIds.get(endpoint.name);\n\n await this.sendNotification(resolvedUrl, \"notifications/initialized\", {\n sessionId,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n });\n\n const listResponse = await this.sendRpc(\n resolvedUrl,\n \"tools/list\",\n {},\n { sessionId, forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n const toolList =\n (listResponse.result as { tools?: McpToolSchema[] })?.tools ?? [];\n\n const tools = new Map<string, McpToolSchema>();\n for (const tool of toolList) {\n tools.set(tool.name, tool);\n }\n\n this.connections.set(endpoint.name, {\n config: endpoint,\n resolvedUrl,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n tools,\n });\n logger.info(\n \"Connected to MCP server %s: %d tools available\",\n endpoint.name,\n tools.size,\n );\n }\n\n getAllToolDefinitions(): AgentToolDefinition[] {\n const defs: AgentToolDefinition[] = [];\n for (const [serverName, conn] of this.connections) {\n for (const [toolName, schema] of conn.tools) {\n defs.push({\n name: `mcp.${serverName}.${toolName}`,\n description: schema.description ?? toolName,\n parameters:\n (schema.inputSchema as AgentToolDefinition[\"parameters\"]) ?? {\n type: \"object\",\n properties: {},\n },\n });\n }\n }\n return defs;\n }\n\n /**\n * Whether the named MCP server may receive workspace-scoped auth headers\n * (e.g., an OBO bearer token from an end-user request). Callers should gate\n * auth-forwarding decisions on this to prevent credential exfiltration to\n * non-workspace hosts.\n */\n canForwardWorkspaceAuth(serverName: string): boolean {\n return this.connections.get(serverName)?.forwardWorkspaceAuth ?? false;\n }\n\n async callTool(\n qualifiedName: string,\n args: unknown,\n authHeaders?: Record<string, string>,\n callerSignal?: AbortSignal,\n ): Promise<string> {\n const parts = qualifiedName.split(\".\");\n if (parts.length < 3 || parts[0] !== \"mcp\") {\n throw new Error(`Invalid MCP tool name: ${qualifiedName}`);\n }\n const serverName = parts[1];\n const toolName = parts.slice(2).join(\".\");\n\n const conn = this.connections.get(serverName);\n if (!conn) {\n throw new Error(`MCP server not connected: ${serverName}`);\n }\n\n const sessionId = this.sessionIds.get(serverName);\n // authHeaders are caller-supplied credentials (typically the OBO token).\n // Only honor them if the destination URL was admitted with\n // forwardWorkspaceAuth=true at connect time.\n const scopedAuthOverride = conn.forwardWorkspaceAuth\n ? authHeaders\n : undefined;\n\n const rpcResult = await this.sendRpc(\n conn.resolvedUrl,\n \"tools/call\",\n { name: toolName, arguments: args },\n {\n authOverride: scopedAuthOverride,\n sessionId,\n forwardWorkspaceAuth: conn.forwardWorkspaceAuth,\n callerSignal,\n },\n );\n const result = rpcResult.result as McpToolCallResult;\n\n if (result.isError) {\n const errText = (result.content ?? [])\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n throw new Error(errText || \"MCP tool call failed\");\n }\n\n return (result.content ?? [])\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n }\n\n async close(): Promise<void> {\n this.closed = true;\n this.connections.clear();\n this.sessionIds.clear();\n }\n\n private async sendRpc(\n url: string,\n method: string,\n params?: Record<string, unknown>,\n options?: {\n authOverride?: Record<string, string>;\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n /**\n * Optional external abort signal (typically the agent's stream signal).\n * Composed with the built-in 30 s timeout so `/cancel` or agent-run\n * shutdown immediately propagates to the MCP fetch rather than waiting\n * for the remote server to respond.\n */\n callerSignal?: AbortSignal;\n },\n ): Promise<{ result: unknown; sessionId?: string }> {\n if (this.closed) throw new Error(\"MCP client is closed\");\n\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: ++this.requestId,\n method,\n ...(params && { params }),\n };\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n const signals: AbortSignal[] = [AbortSignal.timeout(30_000)];\n if (options?.callerSignal) signals.push(options.callerSignal);\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n\n if (!response.ok) {\n throw new Error(\n `MCP request to ${method} failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n let json: JsonRpcResponse;\n\n if (contentType.includes(\"text/event-stream\")) {\n const text = await response.text();\n const lastData = text\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data: \"))\n .map((line) => line.slice(6))\n .pop();\n if (!lastData) {\n throw new Error(`MCP SSE response for ${method} contained no data`);\n }\n json = JSON.parse(lastData) as JsonRpcResponse;\n } else {\n json = (await response.json()) as JsonRpcResponse;\n }\n\n if (json.error) {\n throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);\n }\n\n const sid = response.headers.get(\"mcp-session-id\") ?? undefined;\n return { result: json.result, sessionId: sid };\n }\n\n private async sendNotification(\n url: string,\n method: string,\n options?: {\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n },\n ): Promise<void> {\n if (this.closed) return;\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ jsonrpc: \"2.0\", method }),\n signal: AbortSignal.timeout(30_000),\n });\n }\n\n /**\n * Return the auth headers to send on an outbound request. Workspace auth\n * (SP or OBO) is only resolved when `forwardWorkspaceAuth` is true; for\n * non-workspace hosts no bearer token is attached.\n */\n private async resolveAuthHeaders(options?: {\n authOverride?: Record<string, string>;\n forwardWorkspaceAuth?: boolean;\n }): Promise<Record<string, string>> {\n if (!options?.forwardWorkspaceAuth) return {};\n if (options.authOverride) return options.authOverride;\n return this.authenticate();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"sourcesContent":["/**\n * Custom MCP over HTTP (Streamable) — not `@modelcontextprotocol/sdk`\n *\n * This module implements a tiny JSON-RPC 2.0 client on `fetch` for the subset\n * of MCP we need: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call` over a single JSON request/response. We do not use the official\n * SDK because:\n *\n * - **Policy and auth are the product** — every outbound URL is checked with\n * {@link McpHostPolicy} (allowlist, DNS, private/blocked IP ranges) before\n * the first byte is sent, and workspace tokens are only forwarded when\n * `forwardWorkspaceAuth` is true for that destination. A generic transport\n * from the SDK would still need the same hooks; re-wrapping it would be\n * about as much code, with a larger third-party surface to audit.\n * - **Narrow scope** — we only target Databricks-hosted MCP over Streamable\n * HTTP, not stdio, full SSE sessions, or the rest of the protocol. A\n * hand-rolled path keeps the call graph obvious in code review.\n * - **Zero extra runtime dependency** for this path, consistent with other\n * small, security-sensitive AppKit pieces.\n *\n * Revisit if we add more transports, or if the SDK ships a first-class way to\n * inject our host policy and per-URL auth without fighting the default\n * transport.\n */\nimport type { AgentToolDefinition } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n assertResolvedHostSafe,\n checkMcpUrl,\n type DnsLookup,\n type McpHostPolicy,\n} from \"./host-policy\";\nimport type { McpEndpointConfig } from \"./types\";\n\nconst logger = createLogger(\"connector:mcp\");\n\n/**\n * Hard cap on the size of a single MCP response body, including SSE\n * frames bundled into one HTTP response. MCP `initialize` / `tools/list`\n * / `tools/call` responses are JSON-RPC payloads — single-digit kilobytes\n * in normal use. A response anywhere near this size signals either a\n * misbehaving server or an attempt to exhaust client memory; we'd rather\n * fail loudly than allocate unbounded buffers from a remote.\n */\nconst MCP_RESPONSE_BODY_LIMIT_BYTES = 1024 * 1024;\n\n/**\n * Read a fetch Response body into a string with a hard size cap. Aborts\n * and throws if the cumulative bytes read cross {@link\n * MCP_RESPONSE_BODY_LIMIT_BYTES}, so a remote server cannot keep\n * streaming data past the limit. Returns the empty string when the\n * response has no readable body.\n */\n/**\n * Empty-object fallback used when an MCP server ships a missing or\n * malformed `inputSchema`. Matches the shape downstream adapters expect\n * for a function tool that takes no arguments.\n */\nconst EMPTY_TOOL_PARAMETERS: AgentToolDefinition[\"parameters\"] = {\n type: \"object\",\n properties: {},\n};\n\n/**\n * Coerce a remote MCP server's reported `inputSchema` into the\n * JSONSchema7 shape AppKit's adapters expect for a function tool's\n * `parameters`. The MCP wire type is `Record<string, unknown>`, so a\n * misbehaving (or malicious) server could ship arbitrary JSON. We accept\n * only the standard `{ type: \"object\", properties: {...} }` shape and\n * fall back to an empty-parameters schema otherwise — the tool still\n * registers, it just can't accept arguments.\n */\nfunction coerceToolParameters(\n inputSchema: Record<string, unknown> | undefined,\n): AgentToolDefinition[\"parameters\"] {\n if (!inputSchema || typeof inputSchema !== \"object\") {\n return EMPTY_TOOL_PARAMETERS;\n }\n const { type, properties } = inputSchema;\n if (type !== \"object\") return EMPTY_TOOL_PARAMETERS;\n if (\n properties !== undefined &&\n (typeof properties !== \"object\" ||\n properties === null ||\n Array.isArray(properties))\n ) {\n return EMPTY_TOOL_PARAMETERS;\n }\n return inputSchema as AgentToolDefinition[\"parameters\"];\n}\n\nasync function readResponseTextCapped(\n response: Response,\n maxBytes: number,\n contextLabel: string,\n): Promise<string> {\n if (!response.body) return \"\";\n const reader = response.body.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let total = 0;\n let out = \"\";\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n throw new Error(\n `MCP ${contextLabel}: response body exceeded ${maxBytes} bytes — refusing to allocate unbounded buffer from a remote server.`,\n );\n }\n out += decoder.decode(value, { stream: true });\n }\n out += decoder.decode();\n } finally {\n reader.releaseLock();\n }\n return out;\n}\n\ninterface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: number;\n method: string;\n params?: Record<string, unknown>;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: number;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\ninterface McpToolSchema {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n}\n\ninterface McpToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\n/**\n * Per-endpoint outcome of {@link AppKitMcpClient.connectAll}. Callers (the\n * agents plugin in particular) use the split to warn at startup when some\n * MCP servers are unreachable without aborting boot for the rest.\n */\nexport interface McpConnectAllResult {\n connected: string[];\n failed: Array<{ name: string; error: Error }>;\n}\n\ninterface McpServerConnection {\n config: McpEndpointConfig;\n resolvedUrl: string;\n /**\n * Whether workspace auth (SP / OBO) may be forwarded to this endpoint's URL.\n * Decided at `connect()` time via {@link McpHostPolicy} and cached for the\n * lifetime of the connection.\n */\n forwardWorkspaceAuth: boolean;\n tools: Map<string, McpToolSchema>;\n}\n\n/**\n * Lightweight MCP client for Databricks-hosted MCP servers.\n *\n * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk\n * or LangChain dependency. Supports the Streamable HTTP transport only\n * (POST with JSON-RPC request, single JSON-RPC response). Implements exactly\n * four methods: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call`. No prompts/resources/completion/sampling.\n *\n * All outbound URLs are gated by an {@link McpHostPolicy}: unallowlisted hosts\n * are rejected before the first byte is sent, and workspace credentials are\n * only forwarded to the same-origin workspace. See `mcp-host-policy.ts`.\n *\n * Rationale for hand-rolling JSON-RPC instead of `@modelcontextprotocol/sdk`:\n * see the file-level comment at the top of this module.\n */\nexport class AppKitMcpClient {\n private connections = new Map<string, McpServerConnection>();\n private sessionIds = new Map<string, string>();\n private requestId = 0;\n private closed = false;\n\n constructor(\n private workspaceHost: string,\n private authenticate: () => Promise<Record<string, string>>,\n private policy: McpHostPolicy,\n private options: { dnsLookup?: DnsLookup; fetchImpl?: typeof fetch } = {},\n ) {}\n\n /**\n * Connects every endpoint in parallel and returns a structured summary so\n * callers can distinguish \"all connected\" from \"some failed\".\n *\n * Returning the result instead of throwing is deliberate: one\n * misconfigured MCP server should not take down the entire agents plugin\n * at boot, and the agents plugin uses the summary to warn at startup with\n * the failed-endpoint names. Errors are also logged here so a caller\n * that ignores the return still gets per-endpoint diagnostics.\n *\n * @returns `connected` lists the endpoint names that initialised\n * successfully; `failed` carries `{ name, error }` for the rest.\n */\n async connectAll(\n endpoints: McpEndpointConfig[],\n ): Promise<McpConnectAllResult> {\n const results = await Promise.allSettled(\n endpoints.map((ep) => this.connect(ep)),\n );\n const out: McpConnectAllResult = { connected: [], failed: [] };\n for (let i = 0; i < results.length; i++) {\n const r = results[i];\n const name = endpoints[i].name;\n if (r.status === \"fulfilled\") {\n out.connected.push(name);\n } else {\n const error =\n r.reason instanceof Error ? r.reason : new Error(String(r.reason));\n logger.error(\"Failed to connect MCP server %s: %O\", name, error);\n out.failed.push({ name, error });\n }\n }\n return out;\n }\n\n private resolveUrl(endpoint: McpEndpointConfig): string {\n if (\n endpoint.url.startsWith(\"http://\") ||\n endpoint.url.startsWith(\"https://\")\n ) {\n return endpoint.url;\n }\n return `${this.workspaceHost}${endpoint.url}`;\n }\n\n async connect(endpoint: McpEndpointConfig): Promise<void> {\n const resolvedUrl = this.resolveUrl(endpoint);\n const check = checkMcpUrl(resolvedUrl, this.policy);\n if (!check.ok) {\n throw new Error(\n `MCP endpoint '${endpoint.name}' refused at connect: ${check.reason}`,\n );\n }\n await assertResolvedHostSafe(\n check.url.hostname,\n this.policy,\n this.options.dnsLookup,\n );\n\n logger.info(\n \"Connecting to MCP server: %s at %s (forwardWorkspaceAuth=%s)\",\n endpoint.name,\n resolvedUrl,\n check.forwardWorkspaceAuth,\n );\n\n const initResponse = await this.sendRpc(\n resolvedUrl,\n \"initialize\",\n {\n protocolVersion: \"2025-03-26\",\n capabilities: {},\n clientInfo: { name: \"appkit-agent\", version: \"0.1.0\" },\n },\n { forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n\n if (initResponse.sessionId) {\n this.sessionIds.set(endpoint.name, initResponse.sessionId);\n }\n const sessionId = this.sessionIds.get(endpoint.name);\n\n await this.sendNotification(resolvedUrl, \"notifications/initialized\", {\n sessionId,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n });\n\n const listResponse = await this.sendRpc(\n resolvedUrl,\n \"tools/list\",\n {},\n { sessionId, forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n const toolList =\n (listResponse.result as { tools?: McpToolSchema[] })?.tools ?? [];\n\n const tools = new Map<string, McpToolSchema>();\n for (const tool of toolList) {\n tools.set(tool.name, tool);\n }\n\n this.connections.set(endpoint.name, {\n config: endpoint,\n resolvedUrl,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n tools,\n });\n logger.info(\n \"Connected to MCP server %s: %d tools available\",\n endpoint.name,\n tools.size,\n );\n }\n\n getAllToolDefinitions(): AgentToolDefinition[] {\n const defs: AgentToolDefinition[] = [];\n for (const [serverName, conn] of this.connections) {\n for (const [toolName, schema] of conn.tools) {\n defs.push({\n name: `mcp.${serverName}.${toolName}`,\n description: schema.description ?? toolName,\n parameters: coerceToolParameters(schema.inputSchema),\n });\n }\n }\n return defs;\n }\n\n /**\n * Whether the named MCP server may receive workspace-scoped auth headers\n * (e.g., an OBO bearer token from an end-user request). Callers should gate\n * auth-forwarding decisions on this to prevent credential exfiltration to\n * non-workspace hosts.\n */\n canForwardWorkspaceAuth(serverName: string): boolean {\n return this.connections.get(serverName)?.forwardWorkspaceAuth ?? false;\n }\n\n async callTool(\n qualifiedName: string,\n args: unknown,\n authHeaders?: Record<string, string>,\n callerSignal?: AbortSignal,\n ): Promise<string> {\n const parts = qualifiedName.split(\".\");\n if (parts.length < 3 || parts[0] !== \"mcp\") {\n throw new Error(`Invalid MCP tool name: ${qualifiedName}`);\n }\n const serverName = parts[1];\n const toolName = parts.slice(2).join(\".\");\n\n const conn = this.connections.get(serverName);\n if (!conn) {\n throw new Error(`MCP server not connected: ${serverName}`);\n }\n\n const sessionId = this.sessionIds.get(serverName);\n // authHeaders are caller-supplied credentials (typically the OBO token).\n // Only honor them if the destination URL was admitted with\n // forwardWorkspaceAuth=true at connect time.\n const scopedAuthOverride = conn.forwardWorkspaceAuth\n ? authHeaders\n : undefined;\n\n const rpcResult = await this.sendRpc(\n conn.resolvedUrl,\n \"tools/call\",\n { name: toolName, arguments: args },\n {\n authOverride: scopedAuthOverride,\n sessionId,\n forwardWorkspaceAuth: conn.forwardWorkspaceAuth,\n callerSignal,\n },\n );\n const result = rpcResult.result as McpToolCallResult;\n\n // `text` is optional on `McpToolCallResult.content[]` per the MCP\n // spec; filtering only on `type === \"text\"` lets `c.text` be\n // `undefined`, which `Array.join` would render as the literal\n // string `\"undefined\"` and ship to the agent. Narrow on both\n // fields so the joined string only contains real text.\n const textContent = (result.content ?? []).filter(\n (c): c is { type: \"text\"; text: string } =>\n c.type === \"text\" && typeof c.text === \"string\",\n );\n\n if (result.isError) {\n const errText = textContent.map((c) => c.text).join(\"\\n\");\n throw new Error(errText || \"MCP tool call failed\");\n }\n\n return textContent.map((c) => c.text).join(\"\\n\");\n }\n\n async close(): Promise<void> {\n this.closed = true;\n this.connections.clear();\n this.sessionIds.clear();\n }\n\n private async sendRpc(\n url: string,\n method: string,\n params?: Record<string, unknown>,\n options?: {\n authOverride?: Record<string, string>;\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n /**\n * Optional external abort signal (typically the agent's stream signal).\n * Composed with the built-in 30 s timeout so `/cancel` or agent-run\n * shutdown immediately propagates to the MCP fetch rather than waiting\n * for the remote server to respond.\n */\n callerSignal?: AbortSignal;\n },\n ): Promise<{ result: unknown; sessionId?: string }> {\n if (this.closed) throw new Error(\"MCP client is closed\");\n\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: ++this.requestId,\n method,\n ...(params && { params }),\n };\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n const signals: AbortSignal[] = [AbortSignal.timeout(30_000)];\n if (options?.callerSignal) signals.push(options.callerSignal);\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n\n if (!response.ok) {\n throw new Error(\n `MCP request to ${method} failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n // Always read the body via the capped helper so a misconfigured or\n // malicious server can't exhaust client memory by streaming an\n // unbounded payload. Applies to both SSE (`response.text()` would\n // have buffered the whole stream) and plain JSON (`response.json()`\n // does the same internally).\n const bodyText = await readResponseTextCapped(\n response,\n MCP_RESPONSE_BODY_LIMIT_BYTES,\n method,\n );\n let json: JsonRpcResponse;\n\n if (contentType.includes(\"text/event-stream\")) {\n const lastData = bodyText\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data: \"))\n .map((line) => line.slice(6))\n .pop();\n if (!lastData) {\n throw new Error(`MCP SSE response for ${method} contained no data`);\n }\n json = JSON.parse(lastData) as JsonRpcResponse;\n } else {\n if (bodyText.length === 0) {\n throw new Error(`MCP response for ${method} had an empty body`);\n }\n json = JSON.parse(bodyText) as JsonRpcResponse;\n }\n\n if (json.error) {\n throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);\n }\n\n const sid = response.headers.get(\"mcp-session-id\") ?? undefined;\n return { result: json.result, sessionId: sid };\n }\n\n private async sendNotification(\n url: string,\n method: string,\n options?: {\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n },\n ): Promise<void> {\n if (this.closed) return;\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n // MCP notifications are fire-and-forget per spec — we don't throw on\n // failure. But silently swallowing 4xx/5xx hides server-side\n // rejections that would otherwise look like a successful connect()\n // followed by mysterious tool-call failures. Surface the bad status\n // via the logger so the dev sees it without breaking the protocol\n // contract.\n try {\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ jsonrpc: \"2.0\", method }),\n signal: AbortSignal.timeout(30_000),\n });\n if (!response.ok) {\n logger.warn(\n \"MCP notification %s to %s returned %d %s — the server may have rejected the request, but per MCP spec notifications are fire-and-forget and the connection is considered established.\",\n method,\n url,\n response.status,\n response.statusText,\n );\n }\n } catch (err) {\n logger.warn(\n \"MCP notification %s to %s failed before a response was received: %O\",\n method,\n url,\n err,\n );\n }\n }\n\n /**\n * Return the auth headers to send on an outbound request. Workspace auth\n * (SP or OBO) is only resolved when `forwardWorkspaceAuth` is true; for\n * non-workspace hosts no bearer token is attached.\n */\n private async resolveAuthHeaders(options?: {\n authOverride?: Record<string, string>;\n forwardWorkspaceAuth?: boolean;\n }): Promise<Record<string, string>> {\n if (!options?.forwardWorkspaceAuth) return {};\n if (options.authOverride) return options.authOverride;\n return this.authenticate();\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { McpHostPolicyConfig } from "./host-policy.js";
2
2
  import { McpEndpointConfig } from "./types.js";
3
- import { AppKitMcpClient } from "./client.js";
3
+ import { AppKitMcpClient, McpConnectAllResult } from "./client.js";
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Input shape consumed by {@link AppKitMcpClient.connect}. Produced by the
4
4
  * agents plugin from user-facing `HostedTool` declarations (see
5
- * `plugins/agents/tools/hosted-tools.ts`) and accepted directly by the
5
+ * `core/agent/tools/hosted-tools.ts`) and accepted directly by the
6
6
  * connector to keep its surface free of agent-layer concepts.
7
7
  */
8
8
  interface McpEndpointConfig {
@@ -1,3 +1,4 @@
1
+ import { applyToolkitOptions } from "./toolkit-options.js";
1
2
  import { toToolJSONSchema } from "./tools/json-schema.js";
2
3
 
3
4
  //#region src/core/agent/build-toolkit.ts
@@ -16,16 +17,10 @@ import { toToolJSONSchema } from "./tools/json-schema.js";
16
17
  * dispatch back through `PluginContext.executeTool` for OBO + telemetry.
17
18
  */
18
19
  function buildToolkitEntries(pluginName, registry, opts = {}) {
19
- const prefix = opts.prefix ?? `${pluginName}.`;
20
- const only = opts.only ? new Set(opts.only) : null;
21
- const except = opts.except ? new Set(opts.except) : null;
22
- const rename = opts.rename ?? {};
23
20
  const out = {};
24
21
  for (const [localName, entry] of Object.entries(registry)) {
25
- if (only && !only.has(localName)) continue;
26
- if (except?.has(localName)) continue;
27
- const keyAfterPrefix = `${prefix}${localName}`;
28
- const key = rename[localName] ?? keyAfterPrefix;
22
+ const key = applyToolkitOptions(localName, pluginName, opts);
23
+ if (key === null) continue;
29
24
  const parameters = toToolJSONSchema(entry.schema);
30
25
  const def = {
31
26
  name: key,
@@ -1 +1 @@
1
- {"version":3,"file":"build-toolkit.js","names":[],"sources":["../../../src/core/agent/build-toolkit.ts"],"sourcesContent":["import type { AgentToolDefinition } from \"shared\";\nimport type { ToolRegistry } from \"./tools/define-tool\";\nimport { toToolJSONSchema } from \"./tools/json-schema\";\nimport type { ToolkitEntry, ToolkitOptions } from \"./types\";\n\n/**\n * Converts a plugin's internal `ToolRegistry` into a keyed record of\n * `ToolkitEntry` markers suitable for spreading into an `AgentDefinition.tools`\n * record.\n *\n * The `opts` record controls shape and filtering:\n * - `prefix` — overrides the default `${pluginName}.` prefix; `\"\"` drops it.\n * - `only` — allowlist of local tool names to include (post-prefix).\n * - `except` — denylist of local names.\n * - `rename` — per-tool key remapping (applied after prefix/filter).\n *\n * Each entry carries `pluginName` + `localName` so the agents plugin can\n * dispatch back through `PluginContext.executeTool` for OBO + telemetry.\n */\nexport function buildToolkitEntries(\n pluginName: string,\n registry: ToolRegistry,\n opts: ToolkitOptions = {},\n): Record<string, ToolkitEntry> {\n const prefix = opts.prefix ?? `${pluginName}.`;\n const only = opts.only ? new Set(opts.only) : null;\n const except = opts.except ? new Set(opts.except) : null;\n const rename = opts.rename ?? {};\n\n const out: Record<string, ToolkitEntry> = {};\n\n for (const [localName, entry] of Object.entries(registry)) {\n if (only && !only.has(localName)) continue;\n if (except?.has(localName)) continue;\n\n const keyAfterPrefix = `${prefix}${localName}`;\n const key = rename[localName] ?? keyAfterPrefix;\n\n const parameters = toToolJSONSchema(\n entry.schema,\n ) as unknown as AgentToolDefinition[\"parameters\"];\n\n const def: AgentToolDefinition = {\n name: key,\n description: entry.description,\n parameters,\n };\n if (entry.annotations) {\n def.annotations = entry.annotations;\n }\n\n out[key] = {\n __toolkitRef: true,\n pluginName,\n localName,\n def,\n annotations: entry.annotations,\n autoInheritable: entry.autoInheritable,\n };\n }\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,SAAgB,oBACd,YACA,UACA,OAAuB,EAAE,EACK;CAC9B,MAAM,SAAS,KAAK,UAAU,GAAG,WAAW;CAC5C,MAAM,OAAO,KAAK,OAAO,IAAI,IAAI,KAAK,KAAK,GAAG;CAC9C,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,GAAG;CACpD,MAAM,SAAS,KAAK,UAAU,EAAE;CAEhC,MAAM,MAAoC,EAAE;AAE5C,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,EAAE;AACzD,MAAI,QAAQ,CAAC,KAAK,IAAI,UAAU,CAAE;AAClC,MAAI,QAAQ,IAAI,UAAU,CAAE;EAE5B,MAAM,iBAAiB,GAAG,SAAS;EACnC,MAAM,MAAM,OAAO,cAAc;EAEjC,MAAM,aAAa,iBACjB,MAAM,OACP;EAED,MAAM,MAA2B;GAC/B,MAAM;GACN,aAAa,MAAM;GACnB;GACD;AACD,MAAI,MAAM,YACR,KAAI,cAAc,MAAM;AAG1B,MAAI,OAAO;GACT,cAAc;GACd;GACA;GACA;GACA,aAAa,MAAM;GACnB,iBAAiB,MAAM;GACxB;;AAGH,QAAO"}
1
+ {"version":3,"file":"build-toolkit.js","names":[],"sources":["../../../src/core/agent/build-toolkit.ts"],"sourcesContent":["import type { AgentToolDefinition } from \"shared\";\nimport { applyToolkitOptions } from \"./toolkit-options\";\nimport type { ToolRegistry } from \"./tools/define-tool\";\nimport { toToolJSONSchema } from \"./tools/json-schema\";\nimport type { ToolkitEntry, ToolkitOptions } from \"./types\";\n\n/**\n * Converts a plugin's internal `ToolRegistry` into a keyed record of\n * `ToolkitEntry` markers suitable for spreading into an `AgentDefinition.tools`\n * record.\n *\n * The `opts` record controls shape and filtering:\n * - `prefix` — overrides the default `${pluginName}.` prefix; `\"\"` drops it.\n * - `only` — allowlist of local tool names to include (post-prefix).\n * - `except` — denylist of local names.\n * - `rename` — per-tool key remapping (applied after prefix/filter).\n *\n * Each entry carries `pluginName` + `localName` so the agents plugin can\n * dispatch back through `PluginContext.executeTool` for OBO + telemetry.\n */\nexport function buildToolkitEntries(\n pluginName: string,\n registry: ToolRegistry,\n opts: ToolkitOptions = {},\n): Record<string, ToolkitEntry> {\n const out: Record<string, ToolkitEntry> = {};\n\n for (const [localName, entry] of Object.entries(registry)) {\n const key = applyToolkitOptions(localName, pluginName, opts);\n if (key === null) continue;\n\n const parameters = toToolJSONSchema(\n entry.schema,\n ) as unknown as AgentToolDefinition[\"parameters\"];\n\n const def: AgentToolDefinition = {\n name: key,\n description: entry.description,\n parameters,\n };\n if (entry.annotations) {\n def.annotations = entry.annotations;\n }\n\n out[key] = {\n __toolkitRef: true,\n pluginName,\n localName,\n def,\n annotations: entry.annotations,\n autoInheritable: entry.autoInheritable,\n };\n }\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,YACA,UACA,OAAuB,EAAE,EACK;CAC9B,MAAM,MAAoC,EAAE;AAE5C,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,EAAE;EACzD,MAAM,MAAM,oBAAoB,WAAW,YAAY,KAAK;AAC5D,MAAI,QAAQ,KAAM;EAElB,MAAM,aAAa,iBACjB,MAAM,OACP;EAED,MAAM,MAA2B;GAC/B,MAAM;GACN,aAAa,MAAM;GACnB;GACD;AACD,MAAI,MAAM,YACR,KAAI,cAAc,MAAM;AAG1B,MAAI,OAAO;GACT,cAAc;GACd;GACA;GACA;GACA,aAAa,MAAM;GACnB,iBAAiB,MAAM;GACxB;;AAGH,QAAO"}
@@ -11,7 +11,12 @@ interface LoadContext {
11
11
  defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;
12
12
  /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */
13
13
  availableTools?: Record<string, AgentTool>;
14
- /** Registered plugin toolkits referenced by frontmatter `toolkits: [...]`. */
14
+ /**
15
+ * Registered plugin toolkits referenced by `plugin:NAME` entries in the
16
+ * unified `tools:` frontmatter list. Keyed by plugin name; each value
17
+ * exposes the same `toolkit(opts?)` surface as the `plugins` argument to
18
+ * `tools(plugins) => Record<...>` in the code form.
19
+ */
15
20
  plugins?: Map<string, ToolkitProvider>;
16
21
  /**
17
22
  * Code-defined agents contributed by `agents({ agents: { ... } })`. The
@@ -1 +1 @@
1
- {"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;UAiBU,eAAA;EACR,OAAA,GAAU,IAAA,GAAO,cAAA,KAAmB,MAAA;AAAA;AAAA,UAGrB,WAAA;EAJQ;EAMvB,YAAA,GAAe,YAAA,GAAe,OAAA,CAAQ,YAAA;EALI;EAO1C,cAAA,GAAiB,MAAA,SAAe,SAAA;EAPf;EASjB,OAAA,GAAU,GAAA,SAAY,eAAA;EATc;;;AAGtC;;;;EAcE,UAAA,GAAa,MAAA,SAAe,eAAA;AAAA;AAAA,UAGb,UAAA;EAbE;EAejB,IAAA,EAAM,MAAA,SAAe,eAAA;EAbX;EAeV,YAAA;AAAA;;;;;;iBA6Bc,uBAAA,CAAwB,QAAA;AAAxC;;;;;AA+BA;;;AA/BA,iBA+BsB,iBAAA,CACpB,QAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,eAAA;;;;;;;;;;;;AAgCX;;;;;;;iBAAsB,iBAAA,CACpB,GAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,UAAA"}
1
+ {"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;UAiBU,eAAA;EACR,OAAA,GAAU,IAAA,GAAO,cAAA,KAAmB,MAAA;AAAA;AAAA,UAGrB,WAAA;EAJQ;EAMvB,YAAA,GAAe,YAAA,GAAe,OAAA,CAAQ,YAAA;EALI;EAO1C,cAAA,GAAiB,MAAA,SAAe,SAAA;EAPf;;;;;AAGnB;EAWE,OAAA,GAAU,GAAA,SAAY,eAAA;;;;;;;;EAQtB,UAAA,GAAa,MAAA,SAAe,eAAA;AAAA;AAAA,UAGb,UAAA;EAHI;EAKnB,IAAA,EAAM,MAAA,SAAe,eAAA;EAtBrB;EAwBA,YAAA;AAAA;;;;;;iBA0Dc,uBAAA,CAAwB,QAAA;;;;;;;;;iBA8BlB,iBAAA,CACpB,QAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,eAAA;;;;;;;;;;;;;;;;;;;iBAgCW,iBAAA,CACpB,GAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,UAAA"}
@@ -6,6 +6,7 @@ import yaml from "js-yaml";
6
6
 
7
7
  //#region src/core/agent/load-agents.ts
8
8
  const logger = createLogger("agents:loader");
9
+ const PLUGIN_PREFIX = "plugin:";
9
10
  /**
10
11
  * Derives the logical agent id from a markdown path. When the file is named
11
12
  * `agent.md`, the id is the parent directory name (folder-based layout);
@@ -21,7 +22,6 @@ function agentIdFromMarkdownPath(filePath) {
21
22
  const ALLOWED_KEYS = new Set([
22
23
  "endpoint",
23
24
  "model",
24
- "toolkits",
25
25
  "tools",
26
26
  "agents",
27
27
  "maxSteps",
@@ -191,36 +191,76 @@ function buildDefinition(name, raw, filePath, ctx) {
191
191
  function resolveFrontmatterTools(agentName, fm, filePath, ctx) {
192
192
  const out = {};
193
193
  const pluginIdx = ctx.plugins ?? /* @__PURE__ */ new Map();
194
- for (const spec of fm.toolkits ?? []) {
195
- const [pluginName, opts] = parseToolkitSpec(spec, filePath, agentName);
196
- const provider = pluginIdx.get(pluginName);
197
- if (!provider) throw new Error(`Agent '${agentName}' (${filePath}) references toolkit '${pluginName}', but plugin '${pluginName}' is not registered. Available: ${pluginIdx.size > 0 ? Array.from(pluginIdx.keys()).join(", ") : "<none>"}`);
198
- const entries = provider.toolkit(opts);
199
- for (const [key, entry] of Object.entries(entries)) {
200
- if (!isToolkitEntry(entry)) throw new Error(`Plugin '${pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`);
201
- out[key] = entry;
202
- }
203
- }
204
- for (const key of fm.tools ?? []) {
205
- const tool = ctx.availableTools?.[key];
206
- if (!tool) {
207
- const available = ctx.availableTools ? Object.keys(ctx.availableTools).join(", ") : "<none>";
208
- throw new Error(`Agent '${agentName}' (${filePath}) references tool '${key}', which is not in the agents() plugin's tools field. Available: ${available}`);
194
+ for (const entry of fm.tools ?? []) {
195
+ const parsed = parseToolEntry(entry, filePath, agentName);
196
+ if (parsed.kind === "plugin") {
197
+ const provider = pluginIdx.get(parsed.pluginName);
198
+ if (!provider) {
199
+ const available = pluginIdx.size > 0 ? Array.from(pluginIdx.keys()).join(", ") : "<none>";
200
+ throw new Error(`Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`);
201
+ }
202
+ const entries = provider.toolkit(parsed.opts);
203
+ for (const [key, value] of Object.entries(entries)) {
204
+ if (!isToolkitEntry(value)) throw new Error(`Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`);
205
+ out[key] = value;
206
+ }
207
+ } else {
208
+ const tool = ctx.availableTools?.[parsed.toolName];
209
+ if (!tool) {
210
+ const available = ctx.availableTools ? Object.keys(ctx.availableTools).join(", ") : "<none>";
211
+ throw new Error(`Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. If you meant to reference a plugin, use the 'plugin:NAME' prefix.`);
212
+ }
213
+ out[parsed.toolName] = tool;
209
214
  }
210
- out[key] = tool;
211
215
  }
212
216
  return out;
213
217
  }
214
- function parseToolkitSpec(spec, filePath, agentName) {
215
- if (typeof spec === "string") return [spec, void 0];
216
- if (typeof spec !== "object" || spec === null) throw new Error(`Agent '${agentName}' (${filePath}) has invalid toolkit entry: ${JSON.stringify(spec)}`);
217
- const keys = Object.keys(spec);
218
- if (keys.length !== 1) throw new Error(`Agent '${agentName}' (${filePath}) toolkit entry must have exactly one key, got: ${keys.join(", ")}`);
219
- const pluginName = keys[0];
220
- const value = spec[pluginName];
221
- if (Array.isArray(value)) return [pluginName, { only: value }];
222
- if (typeof value === "object" && value !== null) return [pluginName, value];
223
- throw new Error(`Agent '${agentName}' (${filePath}) toolkit '${pluginName}' options must be an array of tool names or an options object`);
218
+ /**
219
+ * Classify one item in the `tools:` frontmatter list into either a plugin
220
+ * reference (with optional ToolkitOptions) or an ambient tool lookup.
221
+ *
222
+ * Strings starting with `plugin:` are bare plugin references. Strings
223
+ * without the prefix are ambient tool names. Object entries are
224
+ * single-key mappings keyed by `plugin:NAME`; the value is either an
225
+ * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.
226
+ */
227
+ function parseToolEntry(entry, filePath, agentName) {
228
+ if (typeof entry === "string") {
229
+ if (entry.startsWith(PLUGIN_PREFIX)) {
230
+ const pluginName = entry.slice(7);
231
+ if (pluginName.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
232
+ return {
233
+ kind: "plugin",
234
+ pluginName,
235
+ opts: void 0
236
+ };
237
+ }
238
+ if (entry.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`);
239
+ return {
240
+ kind: "ambient",
241
+ toolName: entry
242
+ };
243
+ }
244
+ if (typeof entry !== "object" || entry === null) throw new Error(`Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`);
245
+ const keys = Object.keys(entry);
246
+ if (keys.length !== 1) throw new Error(`Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(", ")}`);
247
+ const key = keys[0];
248
+ if (key === "plugin") throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
249
+ if (!key.startsWith(PLUGIN_PREFIX)) throw new Error(`Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. Use a bare string for ambient tools (e.g. \`- get_weather\`).`);
250
+ const pluginName = key.slice(7);
251
+ if (pluginName.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
252
+ const value = entry[key];
253
+ if (Array.isArray(value)) return {
254
+ kind: "plugin",
255
+ pluginName,
256
+ opts: { only: value }
257
+ };
258
+ if (typeof value === "object" && value !== null) return {
259
+ kind: "plugin",
260
+ pluginName,
261
+ opts: value
262
+ };
263
+ throw new Error(`Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`);
224
264
  }
225
265
 
226
266
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /** Registered plugin toolkits referenced by frontmatter `toolkits: [...]`. */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n toolkits?: ToolkitSpec[];\n tools?: string[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n maxSteps?: number;\n maxTokens?: number;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\ntype ToolkitSpec = string | { [pluginName: string]: ToolkitOptions | string[] };\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"toolkits\",\n \"tools\",\n \"agents\",\n \"maxSteps\",\n \"maxTokens\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n /** Reserved folder name until per-agent skills land; not an agent package. */\n const RESERVED_DIRS = new Set([\"skills\"]);\n\n const agentIds = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .filter((name) => !RESERVED_DIRS.has(name))\n .sort();\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new Error(\n `Agents subdirectory '${path.join(dir, id)}' must contain agent.md.`,\n );\n }\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!match) {\n return { data: null, content: raw.trim() };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(match[1]);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: match[2].trim() };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: match[2].trim() };\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const spec of fm.toolkits ?? []) {\n const [pluginName, opts] = parseToolkitSpec(spec, filePath, agentName);\n const provider = pluginIdx.get(pluginName);\n if (!provider) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) references toolkit '${pluginName}', but plugin '${pluginName}' is not registered. Available: ${\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\"\n }`,\n );\n }\n const entries = provider.toolkit(opts) as Record<string, unknown>;\n for (const [key, entry] of Object.entries(entries)) {\n if (!isToolkitEntry(entry)) {\n throw new Error(\n `Plugin '${pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = entry as ToolkitEntry;\n }\n }\n\n for (const key of fm.tools ?? []) {\n const tool = ctx.availableTools?.[key];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references tool '${key}', which is not in the agents() plugin's tools field. Available: ${available}`,\n );\n }\n out[key] = tool;\n }\n\n return out;\n}\n\nfunction parseToolkitSpec(\n spec: ToolkitSpec,\n filePath: string,\n agentName: string,\n): [string, ToolkitOptions | undefined] {\n if (typeof spec === \"string\") {\n return [spec, undefined];\n }\n if (typeof spec !== \"object\" || spec === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid toolkit entry: ${JSON.stringify(spec)}`,\n );\n }\n const keys = Object.keys(spec);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) toolkit entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const pluginName = keys[0];\n const value = spec[pluginName];\n if (Array.isArray(value)) {\n return [pluginName, { only: value }];\n }\n if (typeof value === \"object\" && value !== null) {\n return [pluginName, value as ToolkitOptions];\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) toolkit '${pluginName}' options must be an array of tool names or an options object`,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n maxSteps?: number;\n maxTokens?: number;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"maxSteps\",\n \"maxTokens\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n /** Reserved folder name until per-agent skills land; not an agent package. */\n const RESERVED_DIRS = new Set([\"skills\"]);\n\n const agentIds = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .filter((name) => !RESERVED_DIRS.has(name))\n .sort();\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new Error(\n `Agents subdirectory '${path.join(dir, id)}' must contain agent.md.`,\n );\n }\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!match) {\n return { data: null, content: raw.trim() };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(match[1]);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: match[2].trim() };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: match[2].trim() };\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":"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"}