@databricks/appkit 0.31.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 (218) hide show
  1. package/CLAUDE.md +54 -1
  2. package/NOTICE.md +2 -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 +16 -1
  8. package/dist/beta.js +14 -1
  9. package/dist/connectors/index.js +3 -0
  10. package/dist/connectors/mcp/client.d.ts +85 -0
  11. package/dist/connectors/mcp/client.d.ts.map +1 -0
  12. package/dist/connectors/mcp/client.js +296 -0
  13. package/dist/connectors/mcp/client.js.map +1 -0
  14. package/dist/connectors/mcp/host-policy.d.ts +51 -0
  15. package/dist/connectors/mcp/host-policy.d.ts.map +1 -0
  16. package/dist/connectors/mcp/host-policy.js +168 -0
  17. package/dist/connectors/mcp/host-policy.js.map +1 -0
  18. package/dist/connectors/mcp/index.d.ts +3 -0
  19. package/dist/connectors/mcp/index.js +4 -0
  20. package/dist/connectors/mcp/types.d.ts +16 -0
  21. package/dist/connectors/mcp/types.d.ts.map +1 -0
  22. package/dist/context/index.js +1 -1
  23. package/dist/core/agent/build-toolkit.d.ts +2 -0
  24. package/dist/core/agent/build-toolkit.js +45 -0
  25. package/dist/core/agent/build-toolkit.js.map +1 -0
  26. package/dist/core/agent/consume-adapter-stream.js +33 -0
  27. package/dist/core/agent/consume-adapter-stream.js.map +1 -0
  28. package/dist/core/agent/create-agent.d.ts +27 -0
  29. package/dist/core/agent/create-agent.d.ts.map +1 -0
  30. package/dist/core/agent/create-agent.js +50 -0
  31. package/dist/core/agent/create-agent.js.map +1 -0
  32. package/dist/core/agent/load-agents.d.ts +72 -0
  33. package/dist/core/agent/load-agents.d.ts.map +1 -0
  34. package/dist/core/agent/load-agents.js +268 -0
  35. package/dist/core/agent/load-agents.js.map +1 -0
  36. package/dist/core/agent/normalize-result.js +39 -0
  37. package/dist/core/agent/normalize-result.js.map +1 -0
  38. package/dist/core/agent/plugins-map.js +44 -0
  39. package/dist/core/agent/plugins-map.js.map +1 -0
  40. package/dist/core/agent/run-agent.d.ts +58 -0
  41. package/dist/core/agent/run-agent.d.ts.map +1 -0
  42. package/dist/core/agent/run-agent.js +257 -0
  43. package/dist/core/agent/run-agent.js.map +1 -0
  44. package/dist/core/agent/system-prompt.js +38 -0
  45. package/dist/core/agent/system-prompt.js.map +1 -0
  46. package/dist/core/agent/toolkit-options.js +28 -0
  47. package/dist/core/agent/toolkit-options.js.map +1 -0
  48. package/dist/core/agent/toolkit-resolver.js +44 -0
  49. package/dist/core/agent/toolkit-resolver.js.map +1 -0
  50. package/dist/core/agent/tools/define-tool.d.ts +66 -0
  51. package/dist/core/agent/tools/define-tool.d.ts.map +1 -0
  52. package/dist/core/agent/tools/define-tool.js +50 -0
  53. package/dist/core/agent/tools/define-tool.js.map +1 -0
  54. package/dist/core/agent/tools/function-tool.d.ts +38 -0
  55. package/dist/core/agent/tools/function-tool.d.ts.map +1 -0
  56. package/dist/core/agent/tools/function-tool.js +22 -0
  57. package/dist/core/agent/tools/function-tool.js.map +1 -0
  58. package/dist/core/agent/tools/hosted-tools.d.ts +47 -0
  59. package/dist/core/agent/tools/hosted-tools.d.ts.map +1 -0
  60. package/dist/core/agent/tools/hosted-tools.js +67 -0
  61. package/dist/core/agent/tools/hosted-tools.js.map +1 -0
  62. package/dist/core/agent/tools/index.d.ts +5 -0
  63. package/dist/core/agent/tools/index.js +7 -0
  64. package/dist/core/agent/tools/json-schema.js +24 -0
  65. package/dist/core/agent/tools/json-schema.js.map +1 -0
  66. package/dist/core/agent/tools/sql-policy.js +256 -0
  67. package/dist/core/agent/tools/sql-policy.js.map +1 -0
  68. package/dist/core/agent/tools/tool.d.ts +63 -0
  69. package/dist/core/agent/tools/tool.d.ts.map +1 -0
  70. package/dist/core/agent/tools/tool.js +42 -0
  71. package/dist/core/agent/tools/tool.js.map +1 -0
  72. package/dist/core/agent/types.d.ts +299 -0
  73. package/dist/core/agent/types.d.ts.map +1 -0
  74. package/dist/core/agent/types.js +12 -0
  75. package/dist/core/agent/types.js.map +1 -0
  76. package/dist/core/appkit.d.ts +1 -0
  77. package/dist/core/appkit.d.ts.map +1 -1
  78. package/dist/core/appkit.js +31 -4
  79. package/dist/core/appkit.js.map +1 -1
  80. package/dist/core/plugin-context.d.ts +133 -0
  81. package/dist/core/plugin-context.d.ts.map +1 -0
  82. package/dist/core/plugin-context.js +220 -0
  83. package/dist/core/plugin-context.js.map +1 -0
  84. package/dist/index.d.ts +11 -11
  85. package/dist/internal-telemetry/appkit-log.js +19 -0
  86. package/dist/internal-telemetry/appkit-log.js.map +1 -0
  87. package/dist/internal-telemetry/config.js +15 -0
  88. package/dist/internal-telemetry/config.js.map +1 -0
  89. package/dist/internal-telemetry/index.js +4 -0
  90. package/dist/internal-telemetry/reporter.js +132 -0
  91. package/dist/internal-telemetry/reporter.js.map +1 -0
  92. package/dist/plugin/plugin.d.ts +18 -3
  93. package/dist/plugin/plugin.d.ts.map +1 -1
  94. package/dist/plugin/plugin.js +26 -2
  95. package/dist/plugin/plugin.js.map +1 -1
  96. package/dist/plugin/to-plugin.d.ts +3 -2
  97. package/dist/plugin/to-plugin.d.ts.map +1 -1
  98. package/dist/plugin/to-plugin.js +7 -4
  99. package/dist/plugin/to-plugin.js.map +1 -1
  100. package/dist/plugins/agents/agents.d.ts +186 -0
  101. package/dist/plugins/agents/agents.d.ts.map +1 -0
  102. package/dist/plugins/agents/agents.js +979 -0
  103. package/dist/plugins/agents/agents.js.map +1 -0
  104. package/dist/plugins/agents/defaults.js +13 -0
  105. package/dist/plugins/agents/defaults.js.map +1 -0
  106. package/dist/plugins/agents/event-channel.js +64 -0
  107. package/dist/plugins/agents/event-channel.js.map +1 -0
  108. package/dist/plugins/agents/event-translator.js +224 -0
  109. package/dist/plugins/agents/event-translator.js.map +1 -0
  110. package/dist/plugins/agents/index.d.ts +4 -0
  111. package/dist/plugins/agents/index.js +6 -0
  112. package/dist/plugins/agents/manifest.js +26 -0
  113. package/dist/plugins/agents/manifest.js.map +1 -0
  114. package/dist/plugins/agents/schemas.js +51 -0
  115. package/dist/plugins/agents/schemas.js.map +1 -0
  116. package/dist/plugins/agents/thread-store.js +58 -0
  117. package/dist/plugins/agents/thread-store.js.map +1 -0
  118. package/dist/plugins/agents/tool-approval-gate.js +75 -0
  119. package/dist/plugins/agents/tool-approval-gate.js.map +1 -0
  120. package/dist/plugins/analytics/analytics.d.ts +15 -1
  121. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  122. package/dist/plugins/analytics/analytics.js +37 -2
  123. package/dist/plugins/analytics/analytics.js.map +1 -1
  124. package/dist/plugins/analytics/index.js +1 -0
  125. package/dist/plugins/analytics/types.js +15 -0
  126. package/dist/plugins/analytics/types.js.map +1 -0
  127. package/dist/plugins/beta-exports.generated.d.ts +2 -0
  128. package/dist/plugins/beta-exports.generated.js +4 -0
  129. package/dist/plugins/files/plugin.d.ts +20 -2
  130. package/dist/plugins/files/plugin.d.ts.map +1 -1
  131. package/dist/plugins/files/plugin.js +120 -2
  132. package/dist/plugins/files/plugin.js.map +1 -1
  133. package/dist/plugins/genie/genie.d.ts +17 -3
  134. package/dist/plugins/genie/genie.d.ts.map +1 -1
  135. package/dist/plugins/genie/genie.js +61 -2
  136. package/dist/plugins/genie/genie.js.map +1 -1
  137. package/dist/plugins/genie/types.d.ts +10 -2
  138. package/dist/plugins/genie/types.d.ts.map +1 -1
  139. package/dist/plugins/jobs/plugin.js +1 -1
  140. package/dist/plugins/lakebase/index.d.ts +2 -2
  141. package/dist/plugins/lakebase/index.js +1 -1
  142. package/dist/plugins/lakebase/lakebase.d.ts +31 -3
  143. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  144. package/dist/plugins/lakebase/lakebase.js +77 -5
  145. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  146. package/dist/plugins/lakebase/types.d.ts +39 -1
  147. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  148. package/dist/plugins/server/index.d.ts +12 -0
  149. package/dist/plugins/server/index.d.ts.map +1 -1
  150. package/dist/plugins/server/index.js +47 -10
  151. package/dist/plugins/server/index.js.map +1 -1
  152. package/dist/plugins/server/types.d.ts +11 -3
  153. package/dist/plugins/server/types.d.ts.map +1 -1
  154. package/dist/shared/src/agent.d.ts +75 -1
  155. package/dist/shared/src/agent.d.ts.map +1 -1
  156. package/dist/shared/src/index.d.ts +1 -1
  157. package/dist/shared/src/plugin.d.ts +8 -0
  158. package/dist/shared/src/plugin.d.ts.map +1 -1
  159. package/docs/api/appkit/Class.AppKitMcpClient.md +157 -0
  160. package/docs/api/appkit/Class.DatabricksAdapter.md +151 -0
  161. package/docs/api/appkit/Class.Plugin.md +65 -23
  162. package/docs/api/appkit/Function.agentIdFromMarkdownPath.md +18 -0
  163. package/docs/api/appkit/Function.createAgent.md +33 -0
  164. package/docs/api/appkit/Function.createApp.md +10 -8
  165. package/docs/api/appkit/Function.defineTool.md +26 -0
  166. package/docs/api/appkit/Function.executeFromRegistry.md +25 -0
  167. package/docs/api/appkit/Function.functionToolToDefinition.md +16 -0
  168. package/docs/api/appkit/Function.isFunctionTool.md +16 -0
  169. package/docs/api/appkit/Function.isHostedTool.md +16 -0
  170. package/docs/api/appkit/Function.isToolkitEntry.md +18 -0
  171. package/docs/api/appkit/Function.loadAgentFromFile.md +21 -0
  172. package/docs/api/appkit/Function.loadAgentsFromDir.md +26 -0
  173. package/docs/api/appkit/Function.mcpServer.md +28 -0
  174. package/docs/api/appkit/Function.parseTextToolCalls.md +26 -0
  175. package/docs/api/appkit/Function.resolveHostedTools.md +16 -0
  176. package/docs/api/appkit/Function.runAgent.md +26 -0
  177. package/docs/api/appkit/Function.tool.md +28 -0
  178. package/docs/api/appkit/Function.toolsFromRegistry.md +20 -0
  179. package/docs/api/appkit/Interface.AgentAdapter.md +21 -0
  180. package/docs/api/appkit/Interface.AgentDefinition.md +112 -0
  181. package/docs/api/appkit/Interface.AgentInput.md +37 -0
  182. package/docs/api/appkit/Interface.AgentRunContext.md +32 -0
  183. package/docs/api/appkit/Interface.AgentToolDefinition.md +37 -0
  184. package/docs/api/appkit/Interface.AgentsPluginConfig.md +241 -0
  185. package/docs/api/appkit/Interface.AutoInheritToolsConfig.md +27 -0
  186. package/docs/api/appkit/Interface.BasePluginConfig.md +1 -0
  187. package/docs/api/appkit/Interface.FunctionTool.md +80 -0
  188. package/docs/api/appkit/Interface.McpConnectAllResult.md +38 -0
  189. package/docs/api/appkit/Interface.Message.md +55 -0
  190. package/docs/api/appkit/Interface.PluginToolkitProvider.md +22 -0
  191. package/docs/api/appkit/Interface.PromptContext.md +30 -0
  192. package/docs/api/appkit/Interface.RegisteredAgent.md +75 -0
  193. package/docs/api/appkit/Interface.RunAgentInput.md +34 -0
  194. package/docs/api/appkit/Interface.RunAgentResult.md +23 -0
  195. package/docs/api/appkit/Interface.Thread.md +46 -0
  196. package/docs/api/appkit/Interface.ThreadStore.md +103 -0
  197. package/docs/api/appkit/Interface.ToolAnnotations.md +56 -0
  198. package/docs/api/appkit/Interface.ToolConfig.md +72 -0
  199. package/docs/api/appkit/Interface.ToolEntry.md +73 -0
  200. package/docs/api/appkit/Interface.ToolProvider.md +38 -0
  201. package/docs/api/appkit/Interface.ToolkitEntry.md +59 -0
  202. package/docs/api/appkit/Interface.ToolkitOptions.md +45 -0
  203. package/docs/api/appkit/TypeAlias.AgentEvent.md +299 -0
  204. package/docs/api/appkit/TypeAlias.AgentTool.md +11 -0
  205. package/docs/api/appkit/TypeAlias.AgentTools.md +8 -0
  206. package/docs/api/appkit/TypeAlias.AgentToolsFn.md +20 -0
  207. package/docs/api/appkit/TypeAlias.BaseSystemPromptOption.md +9 -0
  208. package/docs/api/appkit/TypeAlias.HostedTool.md +10 -0
  209. package/docs/api/appkit/TypeAlias.Plugins.md +26 -0
  210. package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +29 -0
  211. package/docs/api/appkit/TypeAlias.ToolRegistry.md +6 -0
  212. package/docs/api/appkit/Variable.agents.md +19 -0
  213. package/docs/api/appkit.md +113 -62
  214. package/docs/plugins/agents.md +441 -0
  215. package/docs/privacy.md +41 -0
  216. package/llms.txt +54 -1
  217. package/package.json +4 -2
  218. package/sbom.cdx.json +1 -1
@@ -0,0 +1,296 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import { assertResolvedHostSafe, checkMcpUrl } from "./host-policy.js";
3
+
4
+ //#region src/connectors/mcp/client.ts
5
+ const logger = createLogger("connector:mcp");
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
+ /**
68
+ * Lightweight MCP client for Databricks-hosted MCP servers.
69
+ *
70
+ * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk
71
+ * or LangChain dependency. Supports the Streamable HTTP transport only
72
+ * (POST with JSON-RPC request, single JSON-RPC response). Implements exactly
73
+ * four methods: `initialize`, `notifications/initialized`, `tools/list`,
74
+ * `tools/call`. No prompts/resources/completion/sampling.
75
+ *
76
+ * All outbound URLs are gated by an {@link McpHostPolicy}: unallowlisted hosts
77
+ * are rejected before the first byte is sent, and workspace credentials are
78
+ * only forwarded to the same-origin workspace. See `mcp-host-policy.ts`.
79
+ *
80
+ * Rationale for hand-rolling JSON-RPC instead of `@modelcontextprotocol/sdk`:
81
+ * see the file-level comment at the top of this module.
82
+ */
83
+ var AppKitMcpClient = class {
84
+ connections = /* @__PURE__ */ new Map();
85
+ sessionIds = /* @__PURE__ */ new Map();
86
+ requestId = 0;
87
+ closed = false;
88
+ constructor(workspaceHost, authenticate, policy, options = {}) {
89
+ this.workspaceHost = workspaceHost;
90
+ this.authenticate = authenticate;
91
+ this.policy = policy;
92
+ this.options = options;
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
+ */
107
+ async connectAll(endpoints) {
108
+ const results = await Promise.allSettled(endpoints.map((ep) => this.connect(ep)));
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;
127
+ }
128
+ resolveUrl(endpoint) {
129
+ if (endpoint.url.startsWith("http://") || endpoint.url.startsWith("https://")) return endpoint.url;
130
+ return `${this.workspaceHost}${endpoint.url}`;
131
+ }
132
+ async connect(endpoint) {
133
+ const resolvedUrl = this.resolveUrl(endpoint);
134
+ const check = checkMcpUrl(resolvedUrl, this.policy);
135
+ if (!check.ok) throw new Error(`MCP endpoint '${endpoint.name}' refused at connect: ${check.reason}`);
136
+ await assertResolvedHostSafe(check.url.hostname, this.policy, this.options.dnsLookup);
137
+ logger.info("Connecting to MCP server: %s at %s (forwardWorkspaceAuth=%s)", endpoint.name, resolvedUrl, check.forwardWorkspaceAuth);
138
+ const initResponse = await this.sendRpc(resolvedUrl, "initialize", {
139
+ protocolVersion: "2025-03-26",
140
+ capabilities: {},
141
+ clientInfo: {
142
+ name: "appkit-agent",
143
+ version: "0.1.0"
144
+ }
145
+ }, { forwardWorkspaceAuth: check.forwardWorkspaceAuth });
146
+ if (initResponse.sessionId) this.sessionIds.set(endpoint.name, initResponse.sessionId);
147
+ const sessionId = this.sessionIds.get(endpoint.name);
148
+ await this.sendNotification(resolvedUrl, "notifications/initialized", {
149
+ sessionId,
150
+ forwardWorkspaceAuth: check.forwardWorkspaceAuth
151
+ });
152
+ const toolList = (await this.sendRpc(resolvedUrl, "tools/list", {}, {
153
+ sessionId,
154
+ forwardWorkspaceAuth: check.forwardWorkspaceAuth
155
+ })).result?.tools ?? [];
156
+ const tools = /* @__PURE__ */ new Map();
157
+ for (const tool of toolList) tools.set(tool.name, tool);
158
+ this.connections.set(endpoint.name, {
159
+ config: endpoint,
160
+ resolvedUrl,
161
+ forwardWorkspaceAuth: check.forwardWorkspaceAuth,
162
+ tools
163
+ });
164
+ logger.info("Connected to MCP server %s: %d tools available", endpoint.name, tools.size);
165
+ }
166
+ getAllToolDefinitions() {
167
+ const defs = [];
168
+ for (const [serverName, conn] of this.connections) for (const [toolName, schema] of conn.tools) defs.push({
169
+ name: `mcp.${serverName}.${toolName}`,
170
+ description: schema.description ?? toolName,
171
+ parameters: coerceToolParameters(schema.inputSchema)
172
+ });
173
+ return defs;
174
+ }
175
+ /**
176
+ * Whether the named MCP server may receive workspace-scoped auth headers
177
+ * (e.g., an OBO bearer token from an end-user request). Callers should gate
178
+ * auth-forwarding decisions on this to prevent credential exfiltration to
179
+ * non-workspace hosts.
180
+ */
181
+ canForwardWorkspaceAuth(serverName) {
182
+ return this.connections.get(serverName)?.forwardWorkspaceAuth ?? false;
183
+ }
184
+ async callTool(qualifiedName, args, authHeaders, callerSignal) {
185
+ const parts = qualifiedName.split(".");
186
+ if (parts.length < 3 || parts[0] !== "mcp") throw new Error(`Invalid MCP tool name: ${qualifiedName}`);
187
+ const serverName = parts[1];
188
+ const toolName = parts.slice(2).join(".");
189
+ const conn = this.connections.get(serverName);
190
+ if (!conn) throw new Error(`MCP server not connected: ${serverName}`);
191
+ const sessionId = this.sessionIds.get(serverName);
192
+ const scopedAuthOverride = conn.forwardWorkspaceAuth ? authHeaders : void 0;
193
+ const result = (await this.sendRpc(conn.resolvedUrl, "tools/call", {
194
+ name: toolName,
195
+ arguments: args
196
+ }, {
197
+ authOverride: scopedAuthOverride,
198
+ sessionId,
199
+ forwardWorkspaceAuth: conn.forwardWorkspaceAuth,
200
+ callerSignal
201
+ })).result;
202
+ const textContent = (result.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string");
203
+ if (result.isError) {
204
+ const errText = textContent.map((c) => c.text).join("\n");
205
+ throw new Error(errText || "MCP tool call failed");
206
+ }
207
+ return textContent.map((c) => c.text).join("\n");
208
+ }
209
+ async close() {
210
+ this.closed = true;
211
+ this.connections.clear();
212
+ this.sessionIds.clear();
213
+ }
214
+ async sendRpc(url, method, params, options) {
215
+ if (this.closed) throw new Error("MCP client is closed");
216
+ const request = {
217
+ jsonrpc: "2.0",
218
+ id: ++this.requestId,
219
+ method,
220
+ ...params && { params }
221
+ };
222
+ const authHeaders = await this.resolveAuthHeaders(options);
223
+ const headers = {
224
+ "Content-Type": "application/json",
225
+ Accept: "application/json, text/event-stream",
226
+ ...authHeaders
227
+ };
228
+ if (options?.sessionId) headers["Mcp-Session-Id"] = options.sessionId;
229
+ const fetchImpl = this.options.fetchImpl ?? fetch;
230
+ const signals = [AbortSignal.timeout(3e4)];
231
+ if (options?.callerSignal) signals.push(options.callerSignal);
232
+ const response = await fetchImpl(url, {
233
+ method: "POST",
234
+ headers,
235
+ body: JSON.stringify(request),
236
+ signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0]
237
+ });
238
+ if (!response.ok) throw new Error(`MCP request to ${method} failed: ${response.status} ${response.statusText}`);
239
+ const contentType = response.headers.get("content-type") ?? "";
240
+ const bodyText = await readResponseTextCapped(response, MCP_RESPONSE_BODY_LIMIT_BYTES, method);
241
+ let json;
242
+ if (contentType.includes("text/event-stream")) {
243
+ const lastData = bodyText.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).pop();
244
+ if (!lastData) throw new Error(`MCP SSE response for ${method} contained no data`);
245
+ json = JSON.parse(lastData);
246
+ } else {
247
+ if (bodyText.length === 0) throw new Error(`MCP response for ${method} had an empty body`);
248
+ json = JSON.parse(bodyText);
249
+ }
250
+ if (json.error) throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);
251
+ const sid = response.headers.get("mcp-session-id") ?? void 0;
252
+ return {
253
+ result: json.result,
254
+ sessionId: sid
255
+ };
256
+ }
257
+ async sendNotification(url, method, options) {
258
+ if (this.closed) return;
259
+ const authHeaders = await this.resolveAuthHeaders(options);
260
+ const headers = {
261
+ "Content-Type": "application/json",
262
+ Accept: "application/json, text/event-stream",
263
+ ...authHeaders
264
+ };
265
+ if (options?.sessionId) headers["Mcp-Session-Id"] = options.sessionId;
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
+ }
281
+ }
282
+ /**
283
+ * Return the auth headers to send on an outbound request. Workspace auth
284
+ * (SP or OBO) is only resolved when `forwardWorkspaceAuth` is true; for
285
+ * non-workspace hosts no bearer token is attached.
286
+ */
287
+ async resolveAuthHeaders(options) {
288
+ if (!options?.forwardWorkspaceAuth) return {};
289
+ if (options.authOverride) return options.authOverride;
290
+ return this.authenticate();
291
+ }
292
+ };
293
+
294
+ //#endregion
295
+ export { AppKitMcpClient };
296
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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\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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"}
@@ -0,0 +1,51 @@
1
+ //#region src/connectors/mcp/host-policy.d.ts
2
+ /**
3
+ * DNS lookup function compatible with `dns/promises.lookup(host, { all: true })`.
4
+ * Exposed as an injection point so callers (tests, custom DNS resolvers) can
5
+ * override the default resolver.
6
+ */
7
+ type DnsLookup = (hostname: string, options: {
8
+ all: true;
9
+ }) => Promise<Array<{
10
+ address: string;
11
+ family: number;
12
+ }>>;
13
+ /**
14
+ * Policy that decides whether a given MCP endpoint URL is allowed and whether
15
+ * Databricks workspace credentials (SP or OBO) may be forwarded to it.
16
+ *
17
+ * The default posture is zero-trust: only same-origin workspace URLs receive
18
+ * workspace credentials, and all other destinations must be explicitly
19
+ * allowlisted by the application developer. Private / link-local IP ranges
20
+ * are blocked outright to prevent SSRF into cloud metadata services.
21
+ */
22
+ interface McpHostPolicy {
23
+ /** Lowercased hostname of the Databricks workspace (same-origin target). */
24
+ readonly workspaceHostname: string;
25
+ /** Additional allowlisted hostnames (lowercased). Workspace auth is NEVER forwarded to these. */
26
+ readonly trustedHosts: ReadonlySet<string>;
27
+ /** Permit `http://localhost`, `127.0.0.1`, `::1` URLs. Typically true only in development. */
28
+ readonly allowLocalhost: boolean;
29
+ }
30
+ /**
31
+ * Config shape accepted by {@link buildMcpHostPolicy}, matching the
32
+ * `mcp` field on `AgentsPluginConfig`.
33
+ */
34
+ interface McpHostPolicyConfig {
35
+ /**
36
+ * Additional hostnames that may host custom MCP servers beyond the same-origin
37
+ * workspace. Compared case-insensitively; bare hostnames only (no scheme or
38
+ * path). Workspace credentials (SP / OBO) are never forwarded to these hosts —
39
+ * they must handle authentication themselves.
40
+ */
41
+ trustedHosts?: string[];
42
+ /**
43
+ * Allow `http://localhost`, `127.0.0.1`, and `::1` MCP URLs for local
44
+ * development. Defaults to `true` when `NODE_ENV !== "production"`,
45
+ * otherwise `false`. Workspace credentials are never forwarded to localhost.
46
+ */
47
+ allowLocalhost?: boolean;
48
+ }
49
+ //#endregion
50
+ export { DnsLookup, McpHostPolicy, McpHostPolicyConfig };
51
+ //# sourceMappingURL=host-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-policy.d.ts","names":[],"sources":["../../../src/connectors/mcp/host-policy.ts"],"mappings":";;AAQA;;;;KAAY,SAAA,IACV,QAAA,UACA,OAAA;EAAW,GAAA;AAAA,MACR,OAAA,CAAQ,KAAA;EAAQ,OAAA;EAAiB,MAAA;AAAA;;;;AAWtC;;;;;;UAAiB,aAAA;EAMN;EAAA,SAJA,iBAAA;EAIc;EAAA,SAFd,YAAA,EAAc,WAAA;EASW;EAAA,SAPzB,cAAA;AAAA;;;;;UAOM,mBAAA;;;;;;;EAOf,YAAA;;;;;;EAMA,cAAA;AAAA"}
@@ -0,0 +1,168 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP, isIPv4 } from "node:net";
3
+
4
+ //#region src/connectors/mcp/host-policy.ts
5
+ /** Build an {@link McpHostPolicy} from user config + the resolved workspace URL. */
6
+ function buildMcpHostPolicy(config, workspaceHost) {
7
+ const workspaceHostname = safeHostname(workspaceHost);
8
+ if (!workspaceHostname) throw new Error(`Invalid workspace host for MCP policy: ${JSON.stringify(workspaceHost)}`);
9
+ return {
10
+ workspaceHostname,
11
+ trustedHosts: new Set((config?.trustedHosts ?? []).map((h) => h.trim().toLowerCase())),
12
+ allowLocalhost: config?.allowLocalhost ?? process.env.NODE_ENV !== "production"
13
+ };
14
+ }
15
+ /**
16
+ * Synchronously decide whether an MCP URL is allowed under the given policy
17
+ * and whether workspace credentials may be forwarded to it.
18
+ *
19
+ * Hard rejections:
20
+ * - Non-`http(s)` schemes.
21
+ * - `http://` unless the host is localhost AND `allowLocalhost` is true.
22
+ * - Hosts that are neither same-origin workspace, localhost (if allowed),
23
+ * nor in `trustedHosts`.
24
+ */
25
+ function checkMcpUrl(rawUrl, policy) {
26
+ let url;
27
+ try {
28
+ url = new URL(rawUrl);
29
+ } catch {
30
+ return {
31
+ ok: false,
32
+ reason: `MCP URL is not a valid absolute URL: ${rawUrl}`
33
+ };
34
+ }
35
+ if (url.protocol !== "http:" && url.protocol !== "https:") return {
36
+ ok: false,
37
+ reason: `MCP URL scheme '${url.protocol}' is not allowed (http(s) only): ${rawUrl}`
38
+ };
39
+ const host = url.hostname.toLowerCase();
40
+ const isLoopback = isLoopbackHost(host);
41
+ if (url.protocol === "http:" && !(isLoopback && policy.allowLocalhost)) return {
42
+ ok: false,
43
+ reason: `MCP URL uses plaintext http:// which forwards bearer tokens in cleartext: ${rawUrl}. Use https:// or enable allowLocalhost for a localhost dev server.`
44
+ };
45
+ if (host === policy.workspaceHostname) return {
46
+ ok: true,
47
+ forwardWorkspaceAuth: true,
48
+ url
49
+ };
50
+ if (isLoopback) {
51
+ if (!policy.allowLocalhost) return {
52
+ ok: false,
53
+ reason: `MCP URL points to localhost but allowLocalhost is disabled: ${rawUrl}`
54
+ };
55
+ return {
56
+ ok: true,
57
+ forwardWorkspaceAuth: false,
58
+ url
59
+ };
60
+ }
61
+ if (policy.trustedHosts.has(host)) return {
62
+ ok: true,
63
+ forwardWorkspaceAuth: false,
64
+ url
65
+ };
66
+ return {
67
+ ok: false,
68
+ reason: `MCP host '${host}' is not allowed. Either use a same-origin workspace URL (${policy.workspaceHostname}) or add it to agents({ mcp: { trustedHosts: ['${host}'] } }).`
69
+ };
70
+ }
71
+ /**
72
+ * Resolve `hostname` via DNS and assert that none of its addresses fall in a
73
+ * blocked IP range (loopback, RFC1918, link-local, CGNAT, cloud metadata).
74
+ *
75
+ * Throws with a descriptive error if any resolved address is blocked. Pass
76
+ * `allowLocalhost: true` to permit `127.0.0.1` / `::1` specifically.
77
+ *
78
+ * Note: this only guards against hosts that statically resolve to private
79
+ * ranges. Full SSRF protection requires socket-level IP pinning after
80
+ * resolution (DNS rebinding defense), which is out of scope here.
81
+ */
82
+ async function assertResolvedHostSafe(hostname, policy, lookup$1 = lookup) {
83
+ const lowered = hostname.toLowerCase();
84
+ if (isIP(lowered)) {
85
+ if (isBlockedIp(lowered, policy.allowLocalhost)) throw new Error(`MCP host ${lowered} is in a blocked IP range`);
86
+ return;
87
+ }
88
+ if (lowered === "localhost") {
89
+ if (!policy.allowLocalhost) throw new Error(`MCP host localhost is not allowed under the current policy`);
90
+ return;
91
+ }
92
+ let resolved;
93
+ try {
94
+ resolved = await lookup$1(hostname, { all: true });
95
+ } catch (cause) {
96
+ throw new Error(`MCP host ${hostname} could not be resolved via DNS: ${cause instanceof Error ? cause.message : String(cause)}`);
97
+ }
98
+ if (resolved.length === 0) throw new Error(`MCP host ${hostname} returned no DNS addresses`);
99
+ for (const { address } of resolved) if (isBlockedIp(address, policy.allowLocalhost)) throw new Error(`MCP host ${hostname} resolved to blocked address ${address} (private / link-local ranges are not allowed)`);
100
+ }
101
+ /** Whether a raw hostname literal is one of the recognised loopback aliases. */
102
+ function isLoopbackHost(host) {
103
+ const lowered = host.toLowerCase();
104
+ return lowered === "localhost" || lowered === "127.0.0.1" || lowered === "::1" || lowered === "[::1]" || lowered === "0:0:0:0:0:0:0:1";
105
+ }
106
+ /**
107
+ * Check whether a resolved IP address is in a range that should never receive
108
+ * workspace credentials. `allowLocalhost` carves out 127.0.0.0/8 and ::1.
109
+ */
110
+ function isBlockedIp(address, allowLocalhost) {
111
+ if (isIPv4(address)) return isBlockedIpv4(address, allowLocalhost);
112
+ if (isIP(address) === 6) return isBlockedIpv6(address, allowLocalhost);
113
+ return true;
114
+ }
115
+ function isBlockedIpv4(addr, allowLocalhost) {
116
+ const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
117
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return true;
118
+ const [a, b] = parts;
119
+ if (a === 0) return true;
120
+ if (a === 127) return !allowLocalhost;
121
+ if (a === 10) return true;
122
+ if (a === 172 && b >= 16 && b <= 31) return true;
123
+ if (a === 192 && b === 168) return true;
124
+ if (a === 169 && b === 254) return true;
125
+ if (a === 100 && b >= 64 && b <= 127) return true;
126
+ if (a >= 224) return true;
127
+ return false;
128
+ }
129
+ function isBlockedIpv6(addr, allowLocalhost) {
130
+ const lowered = addr.toLowerCase().replace(/^\[|\]$/g, "");
131
+ if (lowered === "::") return true;
132
+ if (lowered === "::1" || lowered === "0:0:0:0:0:0:0:1") return !allowLocalhost;
133
+ if (lowered.startsWith("::ffff:")) {
134
+ const tail = lowered.slice(7);
135
+ if (isIPv4(tail)) return isBlockedIpv4(tail, allowLocalhost);
136
+ const hexV4 = hexPairToDottedIpv4(tail);
137
+ if (hexV4) return isBlockedIpv4(hexV4, allowLocalhost);
138
+ }
139
+ if (/^f[cd][0-9a-f]{2}:/.test(lowered)) return true;
140
+ if (/^fe[89ab][0-9a-f]:/.test(lowered)) return true;
141
+ if (lowered.startsWith("ff")) return true;
142
+ return false;
143
+ }
144
+ /**
145
+ * Parse the trailing two hex groups of an IPv4-mapped IPv6 address written
146
+ * in colon-hex form (e.g. `a9fe:a9fe`) into the equivalent dotted-quad IPv4
147
+ * representation (`169.254.169.254`). Returns null for anything else.
148
+ */
149
+ function hexPairToDottedIpv4(tail) {
150
+ const match = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
151
+ if (!match) return null;
152
+ const hi = Number.parseInt(match[1], 16);
153
+ const lo = Number.parseInt(match[2], 16);
154
+ if (!Number.isFinite(hi) || !Number.isFinite(lo)) return null;
155
+ if (hi < 0 || hi > 65535 || lo < 0 || lo > 65535) return null;
156
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
157
+ }
158
+ function safeHostname(rawUrl) {
159
+ try {
160
+ return new URL(rawUrl).hostname.toLowerCase();
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ //#endregion
167
+ export { assertResolvedHostSafe, buildMcpHostPolicy, checkMcpUrl };
168
+ //# sourceMappingURL=host-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-policy.js","names":["defaultLookup","lookup"],"sources":["../../../src/connectors/mcp/host-policy.ts"],"sourcesContent":["import { lookup as defaultLookup } from \"node:dns/promises\";\nimport { isIP, isIPv4 } from \"node:net\";\n\n/**\n * DNS lookup function compatible with `dns/promises.lookup(host, { all: true })`.\n * Exposed as an injection point so callers (tests, custom DNS resolvers) can\n * override the default resolver.\n */\nexport type DnsLookup = (\n hostname: string,\n options: { all: true },\n) => Promise<Array<{ address: string; family: number }>>;\n\n/**\n * Policy that decides whether a given MCP endpoint URL is allowed and whether\n * Databricks workspace credentials (SP or OBO) may be forwarded to it.\n *\n * The default posture is zero-trust: only same-origin workspace URLs receive\n * workspace credentials, and all other destinations must be explicitly\n * allowlisted by the application developer. Private / link-local IP ranges\n * are blocked outright to prevent SSRF into cloud metadata services.\n */\nexport interface McpHostPolicy {\n /** Lowercased hostname of the Databricks workspace (same-origin target). */\n readonly workspaceHostname: string;\n /** Additional allowlisted hostnames (lowercased). Workspace auth is NEVER forwarded to these. */\n readonly trustedHosts: ReadonlySet<string>;\n /** Permit `http://localhost`, `127.0.0.1`, `::1` URLs. Typically true only in development. */\n readonly allowLocalhost: boolean;\n}\n\n/**\n * Config shape accepted by {@link buildMcpHostPolicy}, matching the\n * `mcp` field on `AgentsPluginConfig`.\n */\nexport interface McpHostPolicyConfig {\n /**\n * Additional hostnames that may host custom MCP servers beyond the same-origin\n * workspace. Compared case-insensitively; bare hostnames only (no scheme or\n * path). Workspace credentials (SP / OBO) are never forwarded to these hosts —\n * they must handle authentication themselves.\n */\n trustedHosts?: string[];\n /**\n * Allow `http://localhost`, `127.0.0.1`, and `::1` MCP URLs for local\n * development. Defaults to `true` when `NODE_ENV !== \"production\"`,\n * otherwise `false`. Workspace credentials are never forwarded to localhost.\n */\n allowLocalhost?: boolean;\n}\n\n/** Build an {@link McpHostPolicy} from user config + the resolved workspace URL. */\nexport function buildMcpHostPolicy(\n config: McpHostPolicyConfig | undefined,\n workspaceHost: string,\n): McpHostPolicy {\n const workspaceHostname = safeHostname(workspaceHost);\n if (!workspaceHostname) {\n throw new Error(\n `Invalid workspace host for MCP policy: ${JSON.stringify(workspaceHost)}`,\n );\n }\n const trustedHosts = new Set(\n (config?.trustedHosts ?? []).map((h) => h.trim().toLowerCase()),\n );\n const allowLocalhost =\n config?.allowLocalhost ?? process.env.NODE_ENV !== \"production\";\n return { workspaceHostname, trustedHosts, allowLocalhost };\n}\n\ntype McpUrlCheck =\n | {\n readonly ok: true;\n /** Whether it is safe to forward workspace-scoped credentials (SP/OBO) to this URL. */\n readonly forwardWorkspaceAuth: boolean;\n /** Parsed URL for reuse by the caller. */\n readonly url: URL;\n }\n | { readonly ok: false; readonly reason: string };\n\n/**\n * Synchronously decide whether an MCP URL is allowed under the given policy\n * and whether workspace credentials may be forwarded to it.\n *\n * Hard rejections:\n * - Non-`http(s)` schemes.\n * - `http://` unless the host is localhost AND `allowLocalhost` is true.\n * - Hosts that are neither same-origin workspace, localhost (if allowed),\n * nor in `trustedHosts`.\n */\nexport function checkMcpUrl(\n rawUrl: string,\n policy: McpHostPolicy,\n): McpUrlCheck {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n return {\n ok: false,\n reason: `MCP URL is not a valid absolute URL: ${rawUrl}`,\n };\n }\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return {\n ok: false,\n reason: `MCP URL scheme '${url.protocol}' is not allowed (http(s) only): ${rawUrl}`,\n };\n }\n\n const host = url.hostname.toLowerCase();\n const isLoopback = isLoopbackHost(host);\n\n if (url.protocol === \"http:\" && !(isLoopback && policy.allowLocalhost)) {\n return {\n ok: false,\n reason: `MCP URL uses plaintext http:// which forwards bearer tokens in cleartext: ${rawUrl}. Use https:// or enable allowLocalhost for a localhost dev server.`,\n };\n }\n\n if (host === policy.workspaceHostname) {\n return { ok: true, forwardWorkspaceAuth: true, url };\n }\n\n if (isLoopback) {\n if (!policy.allowLocalhost) {\n return {\n ok: false,\n reason: `MCP URL points to localhost but allowLocalhost is disabled: ${rawUrl}`,\n };\n }\n return { ok: true, forwardWorkspaceAuth: false, url };\n }\n\n if (policy.trustedHosts.has(host)) {\n return { ok: true, forwardWorkspaceAuth: false, url };\n }\n\n return {\n ok: false,\n reason: `MCP host '${host}' is not allowed. Either use a same-origin workspace URL (${policy.workspaceHostname}) or add it to agents({ mcp: { trustedHosts: ['${host}'] } }).`,\n };\n}\n\n/**\n * Resolve `hostname` via DNS and assert that none of its addresses fall in a\n * blocked IP range (loopback, RFC1918, link-local, CGNAT, cloud metadata).\n *\n * Throws with a descriptive error if any resolved address is blocked. Pass\n * `allowLocalhost: true` to permit `127.0.0.1` / `::1` specifically.\n *\n * Note: this only guards against hosts that statically resolve to private\n * ranges. Full SSRF protection requires socket-level IP pinning after\n * resolution (DNS rebinding defense), which is out of scope here.\n */\nexport async function assertResolvedHostSafe(\n hostname: string,\n policy: McpHostPolicy,\n lookup: DnsLookup = defaultLookup,\n): Promise<void> {\n const lowered = hostname.toLowerCase();\n\n if (isIP(lowered)) {\n if (isBlockedIp(lowered, policy.allowLocalhost)) {\n throw new Error(`MCP host ${lowered} is in a blocked IP range`);\n }\n return;\n }\n\n if (lowered === \"localhost\") {\n if (!policy.allowLocalhost) {\n throw new Error(\n `MCP host localhost is not allowed under the current policy`,\n );\n }\n return;\n }\n\n let resolved: Array<{ address: string }>;\n try {\n resolved = await lookup(hostname, { all: true });\n } catch (cause) {\n throw new Error(\n `MCP host ${hostname} could not be resolved via DNS: ${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (resolved.length === 0) {\n throw new Error(`MCP host ${hostname} returned no DNS addresses`);\n }\n\n for (const { address } of resolved) {\n if (isBlockedIp(address, policy.allowLocalhost)) {\n throw new Error(\n `MCP host ${hostname} resolved to blocked address ${address} (private / link-local ranges are not allowed)`,\n );\n }\n }\n}\n\n/** Whether a raw hostname literal is one of the recognised loopback aliases. */\nexport function isLoopbackHost(host: string): boolean {\n const lowered = host.toLowerCase();\n return (\n lowered === \"localhost\" ||\n lowered === \"127.0.0.1\" ||\n lowered === \"::1\" ||\n lowered === \"[::1]\" ||\n lowered === \"0:0:0:0:0:0:0:1\"\n );\n}\n\n/**\n * Check whether a resolved IP address is in a range that should never receive\n * workspace credentials. `allowLocalhost` carves out 127.0.0.0/8 and ::1.\n */\nexport function isBlockedIp(address: string, allowLocalhost: boolean): boolean {\n if (isIPv4(address)) {\n return isBlockedIpv4(address, allowLocalhost);\n }\n if (isIP(address) === 6) {\n return isBlockedIpv6(address, allowLocalhost);\n }\n // Not a recognisable IP literal — fail-closed.\n return true;\n}\n\nfunction isBlockedIpv4(addr: string, allowLocalhost: boolean): boolean {\n const parts = addr.split(\".\").map((p) => Number.parseInt(p, 10));\n if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {\n return true;\n }\n const [a, b] = parts;\n if (a === 0) return true;\n if (a === 127) return !allowLocalhost;\n if (a === 10) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 169 && b === 254) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n if (a >= 224) return true;\n return false;\n}\n\nfunction isBlockedIpv6(addr: string, allowLocalhost: boolean): boolean {\n const lowered = addr.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n\n if (lowered === \"::\") return true;\n if (lowered === \"::1\" || lowered === \"0:0:0:0:0:0:0:1\")\n return !allowLocalhost;\n\n // IPv4-mapped IPv6: `::ffff:<v4>` may be written in dotted form\n // (`::ffff:169.254.169.254`) or colon-hex form (`::ffff:a9fe:a9fe`). Both\n // route to the same destination, so we must normalise before delegating\n // to the IPv4 blocklist.\n if (lowered.startsWith(\"::ffff:\")) {\n const tail = lowered.slice(\"::ffff:\".length);\n if (isIPv4(tail)) return isBlockedIpv4(tail, allowLocalhost);\n const hexV4 = hexPairToDottedIpv4(tail);\n if (hexV4) return isBlockedIpv4(hexV4, allowLocalhost);\n }\n\n // Unique Local Addresses (fc00::/7) — `fc` and `fd` only.\n if (/^f[cd][0-9a-f]{2}:/.test(lowered)) return true;\n // Link-local fe80::/10 — the first 10 bits are 1111111010, i.e. the\n // second hex nibble must be 8-b. Matches fe80:..–febf:..\n if (/^fe[89ab][0-9a-f]:/.test(lowered)) return true;\n // Multicast ff00::/8.\n if (lowered.startsWith(\"ff\")) return true;\n return false;\n}\n\n/**\n * Parse the trailing two hex groups of an IPv4-mapped IPv6 address written\n * in colon-hex form (e.g. `a9fe:a9fe`) into the equivalent dotted-quad IPv4\n * representation (`169.254.169.254`). Returns null for anything else.\n */\nfunction hexPairToDottedIpv4(tail: string): string | null {\n const match = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n if (!match) return null;\n const hi = Number.parseInt(match[1], 16);\n const lo = Number.parseInt(match[2], 16);\n if (!Number.isFinite(hi) || !Number.isFinite(lo)) return null;\n if (hi < 0 || hi > 0xffff || lo < 0 || lo > 0xffff) return null;\n const a = (hi >> 8) & 0xff;\n const b = hi & 0xff;\n const c = (lo >> 8) & 0xff;\n const d = lo & 0xff;\n return `${a}.${b}.${c}.${d}`;\n}\n\nfunction safeHostname(rawUrl: string): string | null {\n try {\n return new URL(rawUrl).hostname.toLowerCase();\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { McpHostPolicyConfig } from "./host-policy.js";
2
+ import { McpEndpointConfig } from "./types.js";
3
+ import { AppKitMcpClient, McpConnectAllResult } from "./client.js";
@@ -0,0 +1,4 @@
1
+ import { buildMcpHostPolicy } from "./host-policy.js";
2
+ import { AppKitMcpClient } from "./client.js";
3
+
4
+ export { };
@@ -0,0 +1,16 @@
1
+ //#region src/connectors/mcp/types.d.ts
2
+ /**
3
+ * Input shape consumed by {@link AppKitMcpClient.connect}. Produced by the
4
+ * agents plugin from user-facing `HostedTool` declarations (see
5
+ * `core/agent/tools/hosted-tools.ts`) and accepted directly by the
6
+ * connector to keep its surface free of agent-layer concepts.
7
+ */
8
+ interface McpEndpointConfig {
9
+ /** Stable logical name used as the `mcp.<name>.*` tool prefix and in logs. */
10
+ name: string;
11
+ /** Absolute URL (`https://…`) or workspace-relative path (`/api/2.0/mcp/…`). */
12
+ url: string;
13
+ }
14
+ //#endregion
15
+ export { McpEndpointConfig };
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/connectors/mcp/types.ts"],"mappings":";;AAMA;;;;;UAAiB,iBAAA;;EAEf,IAAA;;EAEA,GAAA;AAAA"}
@@ -10,5 +10,5 @@ var init_context = __esmMin((() => {
10
10
 
11
11
  //#endregion
12
12
  init_context();
13
- export { init_context };
13
+ export { getWorkspaceClient, init_context };
14
14
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ import "./types.js";
2
+ import "./tools/define-tool.js";