@tangle-network/hub-sdk 1.0.4 → 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.
- package/README.md +25 -0
- package/dist/bridge-D8_lAKaW.js +81 -0
- package/dist/bridge-D8_lAKaW.js.map +1 -0
- package/dist/client-BH3gTLjN.d.ts +2571 -0
- package/dist/client-BH3gTLjN.d.ts.map +1 -0
- package/dist/client-CsdLLECZ.js +1574 -0
- package/dist/client-CsdLLECZ.js.map +1 -0
- package/dist/index.d.ts +3 -2568
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1568
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +119 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +482 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +16 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/package.json +14 -6
|
@@ -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
|