@tangle-network/hub-sdk 1.0.3 → 1.0.5

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.
@@ -0,0 +1,119 @@
1
+ import { t as HubClient } from "../client-BH3gTLjN.js";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { Hono } from "hono";
4
+ import { AgentProfile, AgentProfileConnection, AgentProfileMcpServer } from "@tangle-network/agent-interface";
5
+
6
+ //#region src/mcp/tools.d.ts
7
+ type HubMcpToolDescriptor = {
8
+ name: string;
9
+ path: string;
10
+ description: string;
11
+ inputSchema: Record<string, unknown>;
12
+ };
13
+ type HubMcpToolResult = {
14
+ content: Array<{
15
+ type: "text";
16
+ text: string;
17
+ }>;
18
+ isError?: boolean;
19
+ /**
20
+ * Machine-readable error detail for a failed call (MCP `structuredContent`).
21
+ * Carries the hub error `code` and `details` (e.g. the `approval` object with
22
+ * `id`/`connectionId` for a `HUB_APPROVAL_REQUIRED` gate) so consumers can act
23
+ * programmatically — the flattened `content` text alone drops that structure.
24
+ */
25
+ structuredContent?: Record<string, unknown>;
26
+ };
27
+ //#endregion
28
+ //#region src/mcp/bridge.d.ts
29
+ interface HubMcpBridgeClient {
30
+ listTools(): Promise<HubMcpToolDescriptor[]>;
31
+ callTool(name: string, args: unknown): Promise<HubMcpToolResult>;
32
+ }
33
+ declare class HttpHubMcpBridgeClient implements HubMcpBridgeClient {
34
+ private readonly endpoint;
35
+ constructor(endpoint: string);
36
+ listTools(): Promise<HubMcpToolDescriptor[]>;
37
+ callTool(name: string, args: unknown): Promise<HubMcpToolResult>;
38
+ }
39
+ declare function createHubMcpBridgeServer(client: HubMcpBridgeClient): Server;
40
+ //#endregion
41
+ //#region src/mcp/profile.d.ts
42
+ /** Keep authored identity separate from runtime-only grant materialization. */
43
+ declare function lowerProfileConnections(profile: AgentProfile | undefined, runtimeMcp: Record<string, AgentProfileMcpServer>): {
44
+ profile?: AgentProfile;
45
+ workspaceProfile?: AgentProfile;
46
+ };
47
+ //#endregion
48
+ //#region src/mcp/scope.d.ts
49
+ type HubMcpAuthRetry = <T>(operation: () => Promise<T>) => Promise<T>;
50
+ interface HubMcpScopeOptions {
51
+ hash(value: object | string): string;
52
+ withAuthRetry?: HubMcpAuthRetry;
53
+ }
54
+ /** Opaque grants owned by one embedded host; close revokes only this scope. */
55
+ declare class HubMcpScope {
56
+ private readonly options;
57
+ private readonly grants;
58
+ private closed;
59
+ private readonly withAuthRetry;
60
+ constructor(options: HubMcpScopeOptions);
61
+ close(): void;
62
+ resolveConnectionsToMcp<T>(client: HubClient, connections: readonly AgentProfileConnection[], serverForGrant: (token: string) => T): Promise<ResolvedHubConnections<T>>;
63
+ register(input: {
64
+ client: HubClient;
65
+ connectionId: string;
66
+ capabilities: readonly string[];
67
+ /**
68
+ * Pre-resolved tool descriptors. When set, `listTools` returns
69
+ * these verbatim instead of calling `tools.describe` per capability path.
70
+ * The resolver pre-populates this for wildcard (`*`) grants, where it has
71
+ * already enumerated the connection's tools from a single `tools.search`,
72
+ * so the bridge does zero describe round-trips. `tools[].path` is still the
73
+ * authoritative allowlist `callTool` dispatches against.
74
+ */
75
+ tools?: readonly HubMcpToolDescriptor[];
76
+ }): string;
77
+ /**
78
+ * Revoke a single grant token. Returns true when a grant was removed. Safe
79
+ * to call on an already-revoked or never-issued token (returns false) so
80
+ * shutdown paths can revoke unconditionally without double-free risk.
81
+ */
82
+ revoke(token: string): boolean;
83
+ /** Trusted sidecar metadata for the native exact-effect permission hook. */
84
+ connectionId(token: string): string;
85
+ listTools(token: string): Promise<HubMcpToolDescriptor[]>;
86
+ callTool(token: string, name: string, args: unknown): Promise<HubMcpToolResult>;
87
+ private readGrant;
88
+ }
89
+ //#endregion
90
+ //#region src/mcp/resolver.d.ts
91
+ /**
92
+ * Resolution result. `mcp` is the runtime MCP server map the backend
93
+ * installs; `grantTokens` are the opaque sidecar-local grant handles that
94
+ * MUST be revoked when the owning backend shuts down (see
95
+ * `SdkBackend.shutdown`).
96
+ */
97
+ type ResolvedHubConnections<T> = {
98
+ mcp: Record<string, T>;
99
+ grantTokens: string[]; /** Stable identity of the concrete connections and tools behind this result. */
100
+ resolutionKey: string;
101
+ /**
102
+ * Set when the result is an EMPTY toolbelt standing in for a failure the
103
+ * request tolerated (see the attach-all rule on `isAttachAllRequest`). The
104
+ * session runs, but with none of the tenant's integrations attached, so this
105
+ * must be surfaced — a silently empty toolbelt is exactly the trade the
106
+ * resolver's fail-loud posture exists to avoid.
107
+ */
108
+ degraded?: {
109
+ reason: "hub-unauthenticated";
110
+ message: string;
111
+ requestShape: "attach-all";
112
+ };
113
+ };
114
+ //#endregion
115
+ //#region src/mcp/routes.d.ts
116
+ declare function createHubMcpRoutes(scope: HubMcpScope): Hono;
117
+ //#endregion
118
+ export { HttpHubMcpBridgeClient, type HubMcpAuthRetry, HubMcpScope, type HubMcpScopeOptions, type HubMcpToolDescriptor, type HubMcpToolResult, type ResolvedHubConnections, createHubMcpBridgeServer, createHubMcpRoutes, lowerProfileConnections };
119
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/mcp/tools.ts","../../src/mcp/bridge.ts","../../src/mcp/profile.ts","../../src/mcp/scope.ts","../../src/mcp/resolver.ts","../../src/mcp/routes.ts"],"mappings":";;;;;;KAIY,oBAAA;EACV,IAAA;EACA,IAAA;EACA,WAAA;EACA,WAAA,EAAa,MAAA;AAAA;AAAA,KAGH,gBAAA;EACV,OAAA,EAAS,KAAA;IAAQ,IAAA;IAAc,IAAA;EAAA;EAC/B,OAAA;EALa;;;AAGf;;;EASE,iBAAA,GAAoB,MAAA;AAAA;;;UCTZ,kBAAA;EACR,SAAA,IAAa,OAAA,CAAQ,oBAAA;EACrB,QAAA,CAAS,IAAA,UAAc,IAAA,YAAgB,OAAA,CAAQ,gBAAA;AAAA;AAAA,cAGpC,sBAAA,YAAkC,kBAAA;EAAA,iBAC5B,QAAA;cAEL,QAAA;EAON,SAAA,CAAA,GAAa,OAAA,CAAQ,oBAAA;EAQrB,QAAA,CAAS,IAAA,UAAc,IAAA,YAAgB,OAAA,CAAQ,gBAAA;AAAA;AAAA,iBAQvC,wBAAA,CAAyB,MAAA,EAAQ,kBAAA,GAAqB,MAAA;;;;iBCpCtD,uBAAA,CACd,OAAA,EAAS,YAAA,cACT,UAAA,EAAY,MAAA,SAAe,qBAAA;EACxB,OAAA,GAAU,YAAA;EAAc,gBAAA,GAAmB,YAAA;AAAA;;;KCMpC,eAAA,OAAsB,SAAA,QAAiB,OAAA,CAAQ,CAAA,MAAO,OAAA,CAAQ,CAAA;AAAA,UACzD,kBAAA;EACf,IAAA,CAAK,KAAA;EACL,aAAA,GAAgB,eAAA;AAAA;;cAUL,WAAA;EAAA,iBAIkB,OAAA;EAAA,iBAHZ,MAAA;EAAA,QACT,MAAA;EAAA,iBACS,aAAA;cACY,OAAA,EAAS,kBAAA;EAGtC,KAAA,CAAA;EAIA,uBAAA,GAAA,CACE,MAAA,EAAQ,SAAA,EACR,WAAA,WAAsB,sBAAA,IACtB,cAAA,GAAiB,KAAA,aAAkB,CAAA,GAClC,OAAA,CAAQ,sBAAA,CAAuB,CAAA;EAQlC,QAAA,CAAS,KAAA;IACP,MAAA,EAAQ,SAAA;IACR,YAAA;IACA,YAAA;IH1CO;;;;;;;;IGmDP,KAAA,YAAiB,oBAAA;EAAA;;AFxDoD;;;;EE8EvE,MAAA,CAAO,KAAA;EFxEwC;EE6E/C,YAAA,CAAa,KAAA;EAIP,SAAA,CAAU,KAAA,WAAgB,OAAA,CAAQ,oBAAA;EAqClC,QAAA,CACJ,KAAA,UACA,IAAA,UAEA,IAAA,YACC,OAAA,CAAQ,gBAAA;EAAA,QAiFH,SAAA;AAAA;;;;;;AH9MV;;;KI+DY,sBAAA;EACV,GAAA,EAAK,MAAA,SAAe,CAAA;EACpB,WAAA,YJhEiB;EIkEjB,aAAA;EJjEA;;;;;;;EIyEA,QAAA;IACE,MAAA;IACA,OAAA;IACA,YAAA;EAAA;AAAA;;;iBCrFY,kBAAA,CAAmB,KAAA,EAAO,WAAA,GAAc,IAAA"}
@@ -0,0 +1,482 @@
1
+ import { b as isHubUnauthenticated, v as HubSdkError } from "../client-CsdLLECZ.js";
2
+ import { n as createHubMcpBridgeServer, t as HttpHubMcpBridgeClient } from "../bridge-D8_lAKaW.js";
3
+ import { Hono } from "hono";
4
+ import { z } from "zod";
5
+ import { randomBytes } from "node:crypto";
6
+ //#region src/mcp/profile.ts
7
+ /** Keep authored identity separate from runtime-only grant materialization. */
8
+ function lowerProfileConnections(profile, runtimeMcp) {
9
+ if (!profile) return {};
10
+ const { connections: _connections, ...withoutConnections } = profile;
11
+ const collisions = Object.keys(runtimeMcp).filter((name) => name in (profile.mcp ?? {}));
12
+ if (collisions.length) throw new Error(`MCP server ${collisions.join(", ")} is declared by both the profile and the effective runtime; rename the profile server so it cannot be shadowed`);
13
+ return {
14
+ profile: withoutConnections,
15
+ workspaceProfile: {
16
+ ...withoutConnections,
17
+ ...Object.keys(runtimeMcp).length ? { mcp: {
18
+ ...profile.mcp,
19
+ ...runtimeMcp
20
+ } } : {}
21
+ }
22
+ };
23
+ }
24
+ //#endregion
25
+ //#region src/mcp/routes.ts
26
+ function createHubMcpRoutes(scope) {
27
+ const routes = new Hono();
28
+ const callToolSchema = z.object({
29
+ name: z.string().min(1),
30
+ arguments: z.unknown().optional()
31
+ });
32
+ routes.get("/grants/:token/tools", async (c) => {
33
+ const token = c.req.param("token");
34
+ try {
35
+ const tools = await scope.listTools(token);
36
+ return c.json({
37
+ tools,
38
+ connectionId: scope.connectionId(token)
39
+ });
40
+ } catch (error) {
41
+ return hubMcpError(c, error instanceof Error ? error : new Error(String(error)));
42
+ }
43
+ });
44
+ routes.post("/grants/:token/call", async (c) => {
45
+ const token = c.req.param("token");
46
+ const parsed = callToolSchema.safeParse(await c.req.json().catch(() => null));
47
+ if (!parsed.success) return c.json({
48
+ content: [{
49
+ type: "text",
50
+ text: "Invalid Hub MCP tool call request"
51
+ }],
52
+ isError: true
53
+ }, 400);
54
+ try {
55
+ const result = await scope.callTool(token, parsed.data.name, parsed.data.arguments ?? {});
56
+ return c.json(result);
57
+ } catch (error) {
58
+ return hubMcpError(c, error instanceof Error ? error : new Error(String(error)));
59
+ }
60
+ });
61
+ return routes;
62
+ }
63
+ function hubMcpError(c, error) {
64
+ const message = error.message;
65
+ return c.json({
66
+ content: [{
67
+ type: "text",
68
+ text: message
69
+ }],
70
+ isError: true
71
+ }, 403);
72
+ }
73
+ //#endregion
74
+ //#region src/mcp/tools.ts
75
+ function assertConnectionIsActive(connection, connectionId) {
76
+ if (!connection) throw new HubSdkError({
77
+ code: "HUB_CONNECTION_MISSING",
78
+ message: `Hub connection "${connectionId}" not found for this principal`,
79
+ details: { connectionId }
80
+ });
81
+ if (connection.status !== "active") throw new HubSdkError({
82
+ code: connection.status === "revoked" ? "HUB_CONNECTION_REVOKED" : "HUB_CONNECTION_MISSING",
83
+ message: `Hub connection "${connectionId}" is "${connection.status}", expected "active"`,
84
+ details: {
85
+ connectionId,
86
+ providerId: connection.providerId,
87
+ status: connection.status
88
+ }
89
+ });
90
+ }
91
+ function toHubMcpToolName(path) {
92
+ return path.replace(/[^a-zA-Z0-9_-]/g, "_");
93
+ }
94
+ function readInputSchema(tool) {
95
+ if (tool.inputSchema && typeof tool.inputSchema === "object" && !Array.isArray(tool.inputSchema)) return tool.inputSchema;
96
+ return {
97
+ type: "object",
98
+ properties: {}
99
+ };
100
+ }
101
+ function errorResult(message) {
102
+ return {
103
+ content: [{
104
+ type: "text",
105
+ text: message
106
+ }],
107
+ isError: true
108
+ };
109
+ }
110
+ //#endregion
111
+ //#region src/mcp/resolver.ts
112
+ /**
113
+ * Wildcard token. As a `connectionId` it means "every active connection of the
114
+ * principal"; as the sole `capabilities` entry it means "every tool the
115
+ * connection exposes". Both are expanded at session-spawn from cached
116
+ * per-provider catalog searches (see `expandConnectionRefs`). The web dashboard emits
117
+ * `{ connectionId: "*", capabilities: ["*"] }` to attach all connected hub
118
+ * integrations; explicit CLI grants name concrete connections + paths.
119
+ */
120
+ const WILDCARD = "*";
121
+ /**
122
+ * Whether every ref is the full attach-all sentinel — `{ connectionId: "*",
123
+ * capabilities: ["*"] }`, naming neither a connection nor a capability.
124
+ *
125
+ * Such a request asserts nothing: "give me whatever integrations I happen to
126
+ * have" is satisfied by an empty toolbelt, so resolving it to no servers is a
127
+ * legitimate answer rather than a swallowed failure. Every other shape names
128
+ * something that must exist — a concrete connection, or a concrete capability
129
+ * fanned across connections — and stays fail-loud, mixed sets included.
130
+ */
131
+ function isAttachAllRequest(refs) {
132
+ return refs.length > 0 && refs.every((ref) => ref.connectionId === WILDCARD && ref.capabilities.length === 1 && ref.capabilities[0] === WILDCARD);
133
+ }
134
+ /** Resolve required grants once, with rollback before the host's auth retry. */
135
+ async function resolveConnectionsToMcp(client, connections, host) {
136
+ const empty = () => ({
137
+ mcp: {},
138
+ grantTokens: [],
139
+ resolutionKey: host.hash([])
140
+ });
141
+ if (connections.length === 0) return empty();
142
+ try {
143
+ return await host.withAuthRetry(() => resolveConnectionsOnce(client, connections, host));
144
+ } catch (error) {
145
+ if (!isHubUnauthenticated(error) || !isAttachAllRequest(connections)) throw error;
146
+ return {
147
+ ...empty(),
148
+ degraded: {
149
+ reason: "hub-unauthenticated",
150
+ message: "Hub rejected the current credential and it could not be refreshed; no Hub integrations were attached",
151
+ requestShape: "attach-all"
152
+ }
153
+ };
154
+ }
155
+ }
156
+ async function resolveConnectionsOnce(client, connections, host) {
157
+ const listed = await client.connections.list();
158
+ const connectionsById = /* @__PURE__ */ new Map();
159
+ for (const conn of listed.connections) connectionsById.set(conn.id, conn);
160
+ const concreteRefs = await expandConnectionRefs(client, connections, listed.connections);
161
+ const resolutionKey = host.hash(concreteRefs.map((ref) => ({
162
+ connectionId: ref.connectionId,
163
+ providerId: connectionsById.get(ref.connectionId)?.providerId,
164
+ alias: ref.alias ?? defaultAliasFor(connectionsById.get(ref.connectionId)?.providerId ?? "", ref.connectionId, ref.capabilities),
165
+ capabilities: [...ref.capabilities].sort(),
166
+ descriptors: ref.descriptors ? [...ref.descriptors].sort((a, b) => a.path.localeCompare(b.path)) : void 0
167
+ })).sort((a, b) => `${a.connectionId}:${a.alias ?? ""}`.localeCompare(`${b.connectionId}:${b.alias ?? ""}`)));
168
+ const seenAliases = /* @__PURE__ */ new Map();
169
+ const mcp = {};
170
+ const grantTokens = [];
171
+ try {
172
+ for (const ref of concreteRefs) {
173
+ const conn = connectionsById.get(ref.connectionId);
174
+ assertConnectionIsActive(conn, ref.connectionId);
175
+ if (!ref.descriptors) for (const capabilityPath of ref.capabilities) {
176
+ const tool = (await client.tools.describe(capabilityPath)).tool;
177
+ if (tool.requiredConnectionProviderId !== void 0 && tool.requiredConnectionProviderId !== conn.providerId) throw new HubSdkError({
178
+ code: "HUB_INVALID_INPUT",
179
+ message: `Capability "${capabilityPath}" requires provider "${tool.requiredConnectionProviderId}" but connection "${ref.connectionId}" is "${conn.providerId}"`,
180
+ details: {
181
+ connectionId: ref.connectionId,
182
+ providerId: conn.providerId,
183
+ capabilityPath,
184
+ requiredProviderId: tool.requiredConnectionProviderId
185
+ }
186
+ });
187
+ }
188
+ const alias = ref.alias ?? defaultAliasFor(conn.providerId, ref.connectionId, ref.capabilities);
189
+ const previousConnectionId = seenAliases.get(alias);
190
+ if (previousConnectionId !== void 0) throw new HubSdkError({
191
+ code: "HUB_INVALID_INPUT",
192
+ message: `Duplicate MCP alias "${alias}" — each hub connection grant must have a unique alias (connections "${previousConnectionId}" and "${ref.connectionId}")`,
193
+ details: {
194
+ alias,
195
+ connectionId: ref.connectionId,
196
+ previousConnectionId
197
+ }
198
+ });
199
+ seenAliases.set(alias, ref.connectionId);
200
+ const grantToken = host.grants.register({
201
+ client,
202
+ connectionId: ref.connectionId,
203
+ capabilities: ref.capabilities,
204
+ tools: ref.descriptors
205
+ });
206
+ grantTokens.push(grantToken);
207
+ mcp[alias] = host.serverForGrant(grantToken);
208
+ }
209
+ } catch (error) {
210
+ for (const token of grantTokens) host.grants.revoke(token);
211
+ throw error;
212
+ }
213
+ return {
214
+ mcp,
215
+ grantTokens,
216
+ resolutionKey
217
+ };
218
+ }
219
+ /**
220
+ * Expand `*` wildcards into concrete connection refs.
221
+ *
222
+ * - `connectionId: "*"` fans out to one ref per ACTIVE connection, carrying
223
+ * the original capability set. This is the web dashboard's "attach all my
224
+ * connected integrations" sentinel — best-effort, so a connection that
225
+ * resolves to zero tools is dropped rather than producing an empty server.
226
+ * - `capabilities: ["*"]` enumerates the connection's tools from a cached
227
+ * provider-scoped `tools.search("", { provider })` and emits the concrete
228
+ * paths plus pre-built descriptors. An EXPLICIT
229
+ * connection that exposes no tools fails loud (the caller named it); a
230
+ * wildcard-connection one is skipped.
231
+ *
232
+ * Explicit (non-wildcard) refs pass through untouched for the existing
233
+ * fail-loud validation path.
234
+ */
235
+ async function expandConnectionRefs(client, refs, allConnections) {
236
+ const active = allConnections.filter((c) => c.status === "active");
237
+ const catalogByProvider = /* @__PURE__ */ new Map();
238
+ const loadCatalog = (providerId) => {
239
+ let cached = catalogByProvider.get(providerId);
240
+ if (cached === void 0) {
241
+ cached = client.tools.search("", {
242
+ provider: providerId,
243
+ limit: 200
244
+ }).then((res) => res.tools ?? []);
245
+ catalogByProvider.set(providerId, cached);
246
+ }
247
+ return cached;
248
+ };
249
+ const connExpanded = refs.flatMap((ref) => {
250
+ if (ref.connectionId === WILDCARD) return active.map((conn) => ({
251
+ connectionId: conn.id,
252
+ capabilities: [...ref.capabilities],
253
+ fromWildcardConnection: true
254
+ }));
255
+ return [{
256
+ connectionId: ref.connectionId,
257
+ capabilities: [...ref.capabilities],
258
+ alias: ref.alias,
259
+ fromWildcardConnection: false
260
+ }];
261
+ });
262
+ for (const ref of connExpanded) if (ref.capabilities.length === 1 && ref.capabilities[0] === WILDCARD) {
263
+ const conn = connectionsByIdLookup(allConnections, ref.connectionId);
264
+ if (conn?.status === "active") loadCatalog(conn.providerId).catch(() => {});
265
+ }
266
+ const out = [];
267
+ for (const ref of connExpanded) {
268
+ if (!(ref.capabilities.length === 1 && ref.capabilities[0] === WILDCARD)) {
269
+ out.push({
270
+ connectionId: ref.connectionId,
271
+ capabilities: ref.capabilities,
272
+ alias: ref.alias,
273
+ fromWildcardConnection: ref.fromWildcardConnection
274
+ });
275
+ continue;
276
+ }
277
+ const conn = connectionsByIdLookup(allConnections, ref.connectionId);
278
+ if (!conn || conn.status !== "active") {
279
+ out.push({
280
+ connectionId: ref.connectionId,
281
+ capabilities: [WILDCARD],
282
+ alias: ref.alias,
283
+ fromWildcardConnection: ref.fromWildcardConnection
284
+ });
285
+ continue;
286
+ }
287
+ const descriptors = buildToolDescriptors((await loadCatalog(conn.providerId)).filter((t) => t.connectionStatus === "connected" && (t.requiredConnectionProviderId ?? t.providerId) === conn.providerId));
288
+ if (descriptors.length === 0) {
289
+ if (ref.fromWildcardConnection) continue;
290
+ throw new HubSdkError({
291
+ code: "HUB_INVALID_INPUT",
292
+ message: `Connection "${ref.connectionId}" (${conn.providerId}) exposes no tools — cannot expand capability \`*\``,
293
+ details: {
294
+ connectionId: ref.connectionId,
295
+ providerId: conn.providerId
296
+ }
297
+ });
298
+ }
299
+ out.push({
300
+ connectionId: ref.connectionId,
301
+ capabilities: descriptors.map((d) => d.path),
302
+ alias: ref.alias,
303
+ descriptors,
304
+ fromWildcardConnection: ref.fromWildcardConnection
305
+ });
306
+ }
307
+ return out;
308
+ }
309
+ function connectionsByIdLookup(connections, id) {
310
+ return connections.find((c) => c.id === id);
311
+ }
312
+ /**
313
+ * Build MCP tool descriptors from catalog search results, deduping by the
314
+ * sanitized MCP tool name (hub paths with the same leaf collapse, matching the
315
+ * grant bridge's own naming).
316
+ */
317
+ function buildToolDescriptors(tools) {
318
+ const seen = /* @__PURE__ */ new Set();
319
+ const out = [];
320
+ for (const tool of tools) {
321
+ const name = toHubMcpToolName(tool.path);
322
+ if (seen.has(name)) continue;
323
+ seen.add(name);
324
+ out.push({
325
+ name,
326
+ path: tool.path,
327
+ description: tool.description ?? tool.title ?? tool.path,
328
+ inputSchema: tool.inputSchema && typeof tool.inputSchema === "object" && !Array.isArray(tool.inputSchema) ? tool.inputSchema : {
329
+ type: "object",
330
+ properties: {}
331
+ }
332
+ });
333
+ }
334
+ return out;
335
+ }
336
+ function defaultAliasFor(providerId, connectionId, capabilityPaths) {
337
+ return `${providerId}-${connectionId.replace(/^(hubconn_|conn_)/, "").replace(new RegExp(`^${escapeRegExp(providerId)}_`), "").replace(/[^a-zA-Z0-9]/g, "").slice(-8)}-${capabilityPaths.length === 1 ? capabilityPaths[0]?.split(".").pop() ?? capabilityPaths[0] : "tools"}`.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
338
+ }
339
+ function escapeRegExp(value) {
340
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
341
+ }
342
+ //#endregion
343
+ //#region src/mcp/scope.ts
344
+ /** Opaque grants owned by one embedded host; close revokes only this scope. */
345
+ var HubMcpScope = class {
346
+ grants = /* @__PURE__ */ new Map();
347
+ closed = false;
348
+ withAuthRetry;
349
+ constructor(options) {
350
+ this.options = options;
351
+ this.withAuthRetry = options.withAuthRetry ?? ((operation) => operation());
352
+ }
353
+ close() {
354
+ this.closed = true;
355
+ this.grants.clear();
356
+ }
357
+ resolveConnectionsToMcp(client, connections, serverForGrant) {
358
+ return resolveConnectionsToMcp(client, connections, {
359
+ grants: this,
360
+ hash: this.options.hash,
361
+ withAuthRetry: this.withAuthRetry,
362
+ serverForGrant
363
+ });
364
+ }
365
+ register(input) {
366
+ if (this.closed) throw new HubSdkError({
367
+ code: "HUB_POLICY_DENIED",
368
+ message: "Hub MCP scope is closed"
369
+ });
370
+ const token = `hmcp_${randomBytes(32).toString("base64url")}`;
371
+ this.grants.set(token, {
372
+ client: input.client,
373
+ connectionId: input.connectionId,
374
+ capabilities: [...input.capabilities],
375
+ tools: input.tools ? [...input.tools] : void 0
376
+ });
377
+ return token;
378
+ }
379
+ /**
380
+ * Revoke a single grant token. Returns true when a grant was removed. Safe
381
+ * to call on an already-revoked or never-issued token (returns false) so
382
+ * shutdown paths can revoke unconditionally without double-free risk.
383
+ */
384
+ revoke(token) {
385
+ return this.grants.delete(token);
386
+ }
387
+ /** Trusted sidecar metadata for the native exact-effect permission hook. */
388
+ connectionId(token) {
389
+ return this.readGrant(token).connectionId;
390
+ }
391
+ async listTools(token) {
392
+ const grant = this.readGrant(token);
393
+ if (grant.tools) return grant.tools;
394
+ const resolved = [];
395
+ const seenNames = /* @__PURE__ */ new Set();
396
+ for (const path of grant.capabilities) {
397
+ const tool = (await this.withAuthRetry(() => {
398
+ this.readGrant(token);
399
+ return grant.client.tools.describe(path);
400
+ })).tool;
401
+ const name = toHubMcpToolName(path);
402
+ if (seenNames.has(name)) throw new HubSdkError({
403
+ code: "HUB_INVALID_INPUT",
404
+ message: `Duplicate MCP tool name "${name}" for Hub grant`,
405
+ details: {
406
+ connectionId: grant.connectionId,
407
+ path
408
+ }
409
+ });
410
+ seenNames.add(name);
411
+ resolved.push({
412
+ name,
413
+ path,
414
+ description: tool.description ?? tool.title ?? path,
415
+ inputSchema: readInputSchema(tool)
416
+ });
417
+ }
418
+ this.readGrant(token);
419
+ grant.tools = resolved;
420
+ return resolved;
421
+ }
422
+ async callTool(token, name, args) {
423
+ const grant = this.readGrant(token);
424
+ const tool = (await this.listTools(token)).find((candidate) => candidate.name === name);
425
+ if (!tool) return errorResult(`Unknown tool: ${name}`);
426
+ let effectInvocation;
427
+ let effectArgs = args ?? {};
428
+ if (effectArgs && typeof effectArgs === "object" && !Array.isArray(effectArgs) && Object.hasOwn(effectArgs, "__tangle_effect_invocation")) {
429
+ const { __tangle_effect_invocation: invocation, ...original } = effectArgs;
430
+ if (typeof invocation !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(invocation)) return errorResult("Invalid native effect invocation");
431
+ effectInvocation = invocation;
432
+ effectArgs = original;
433
+ }
434
+ this.readGrant(token);
435
+ try {
436
+ const result = await this.withAuthRetry(() => {
437
+ this.readGrant(token);
438
+ return grant.client.tools.invoke(tool.path, effectArgs, {
439
+ connectionId: grant.connectionId,
440
+ ...effectInvocation ? { idempotencyKey: `line-effect:${effectInvocation}` } : {}
441
+ });
442
+ });
443
+ return { content: [{
444
+ type: "text",
445
+ text: JSON.stringify(result.result ?? result)
446
+ }] };
447
+ } catch (error) {
448
+ if (error instanceof HubSdkError) {
449
+ const approval = error.code === "HUB_APPROVAL_REQUIRED" ? error.details?.approval : void 0;
450
+ return {
451
+ content: [{
452
+ type: "text",
453
+ text: approval ? JSON.stringify({
454
+ code: error.code,
455
+ message: error.message,
456
+ approval
457
+ }) : `${error.code}: ${error.message}`
458
+ }],
459
+ isError: true,
460
+ structuredContent: {
461
+ code: error.code,
462
+ message: error.message,
463
+ ...error.details ? { details: error.details } : {}
464
+ }
465
+ };
466
+ }
467
+ return errorResult(error instanceof Error ? error.message : String(error));
468
+ }
469
+ }
470
+ readGrant(token) {
471
+ const grant = this.grants.get(token);
472
+ if (!grant) throw new HubSdkError({
473
+ code: "HUB_POLICY_DENIED",
474
+ message: "Unknown Hub MCP grant token"
475
+ });
476
+ return grant;
477
+ }
478
+ };
479
+ //#endregion
480
+ export { HttpHubMcpBridgeClient, HubMcpScope, createHubMcpBridgeServer, createHubMcpRoutes, lowerProfileConnections };
481
+
482
+ //# sourceMappingURL=index.js.map