@tangle-network/hub-sdk 1.0.4 → 1.0.6

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 CHANGED
@@ -132,21 +132,32 @@ Present that approval through the product's decision flow.
132
132
  Set `approve: true` only after the caller has authorized that action.
133
133
  Automatic capability-token minting remains available when the server returns no concrete approval.
134
134
 
135
- ## GitHub OAuth scopes
135
+ ## OAuth scopes
136
136
 
137
- Pass the exact scope set when the caller needs GitHub repository selection.
138
- Platform accepts `read:user`, `repo`, and `read:org` together for this request.
139
- It rejects a callback if GitHub grants a different set.
140
- Omitting `requestedScopes` retains the existing default GitHub flow.
141
- The SDK refuses a start response with different scopes, including responses from an older Platform deployment.
137
+ `requestedScopes` is a provider-generic, non-empty unique subset of the connector
138
+ manifest and the host's optional narrowing. Platform rejects empty, duplicate,
139
+ unknown, or widening requests. The exact set is bound to OAuth state; narrowed
140
+ flows persist a connection only when the provider grants that same set, with no
141
+ missing, narrower, or additional grants. Hosts that do not narrow retain their
142
+ existing defaults and provider-grant fallback.
143
+
144
+ The SDK refuses a start response with different scopes, including responses from
145
+ an older Platform deployment. GitHub retains its special contract: the exact
146
+ `read:user`, `repo`, and `read:org` set. Gmail can request manifest-declared
147
+ read-only access when the host permits it:
142
148
 
143
149
  ```ts
144
150
  const hub = HubClient.fromEnv();
151
+ await hub.connections.start("gmail", {
152
+ requestedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
153
+ });
145
154
  await hub.connections.start("github", {
146
155
  requestedScopes: ["read:user", "repo", "read:org"],
147
156
  });
148
157
  ```
149
158
 
159
+ Omitting `requestedScopes` retains the host-selected or manifest default.
160
+
150
161
  ## Workflows
151
162
 
152
163
  `hub.workflows` covers the `/v1/workflows` surface — the same resource the
@@ -420,3 +431,28 @@ const result = await broker.invoke(grant.grantId, connectionId, "github.issues.l
420
431
  ```
421
432
 
422
433
  `invoke` mints a fresh one-use, 60-second broker token and checks its selected scope before execution. The host application must recheck its own current customer enrollment on every call and revoke the app grant when access ends. Do not return broker tokens or application secrets to a browser or persist them in agent profiles.
434
+
435
+ ## Embedded Node MCP hosts
436
+
437
+ `@tangle-network/hub-sdk/mcp` owns `HubMcpScope`, connection resolution,
438
+ `createHubMcpRoutes`, the HTTP bridge and `lowerProfileConnections`. The root
439
+ client export remains browser/Worker-safe.
440
+
441
+ Create one scope for the host's lifetime with its existing canonical hash and
442
+ credential-renewal function. Mount `createHubMcpRoutes(scope)` on the host's
443
+ existing listener. `scope.resolveConnectionsToMcp(client, refs, serverForGrant)`
444
+ validates the connections, provider and tool allowlists, rolls back partial
445
+ allocation, and gives the host its native MCP representation. Only a pure
446
+ attach-all request can return an explicitly degraded empty result; required
447
+ connections remain errors. The host surfaces degradation and recovery.
448
+
449
+ Revoke each resolution's `grantTokens` when its run settles. `scope.close()`
450
+ revokes the remaining grants of that instance and prevents new allocation.
451
+ Other scopes and their borrowed clients remain independent.
452
+
453
+ The published `@tangle-network/hub-sdk/mcp/stdio` entry reads only
454
+ `TANGLE_HUB_MCP_ENDPOINT`. Launch it through Node with the opaque callback
455
+ endpoint; do not forward the Hub credential. `lowerProfileConnections` keeps
456
+ the authored adapter profile separate from the ephemeral workspace profile.
457
+ Grant endpoints belong to runtime materialization, never persisted authored
458
+ profiles or trace metadata.
@@ -0,0 +1,81 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ //#region src/mcp/bridge.ts
6
+ var HttpHubMcpBridgeClient = class {
7
+ endpoint;
8
+ constructor(endpoint) {
9
+ if (!endpoint || endpoint.trim().length === 0) throw new Error("TANGLE_HUB_MCP_ENDPOINT is required");
10
+ this.endpoint = endpoint.replace(/\/+$/, "");
11
+ }
12
+ async listTools() {
13
+ return (await requestJson("GET", `${this.endpoint}/tools`)).tools;
14
+ }
15
+ async callTool(name, args) {
16
+ return requestJson("POST", `${this.endpoint}/call`, {
17
+ name,
18
+ arguments: args ?? {}
19
+ });
20
+ }
21
+ };
22
+ function createHubMcpBridgeServer(client) {
23
+ const server = new Server({
24
+ name: "tangle-hub",
25
+ version: "0.1.0"
26
+ }, { capabilities: { tools: {} } });
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
28
+ return { tools: (await client.listTools()).map((tool) => ({
29
+ name: tool.name,
30
+ description: tool.description,
31
+ inputSchema: tool.inputSchema
32
+ })) };
33
+ });
34
+ server.setRequestHandler(CallToolRequestSchema, async (request) => client.callTool(request.params.name, request.params.arguments ?? {}));
35
+ return server;
36
+ }
37
+ function requestJson(method, url, body) {
38
+ return new Promise((resolve, reject) => {
39
+ const parsed = new URL(url);
40
+ const payload = body ? JSON.stringify(body) : void 0;
41
+ const request = (parsed.protocol === "https:" ? https : http).request({
42
+ protocol: parsed.protocol,
43
+ hostname: parsed.hostname,
44
+ port: parsed.port,
45
+ path: `${parsed.pathname}${parsed.search}`,
46
+ method,
47
+ rejectUnauthorized: parsed.hostname !== "127.0.0.1",
48
+ headers: {
49
+ Accept: "application/json",
50
+ ...payload ? {
51
+ "Content-Type": "application/json",
52
+ "Content-Length": Buffer.byteLength(payload).toString()
53
+ } : {}
54
+ }
55
+ }, (response) => {
56
+ const chunks = [];
57
+ response.on("data", (chunk) => {
58
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
59
+ });
60
+ response.on("end", () => {
61
+ const text = Buffer.concat(chunks).toString("utf8");
62
+ if ((response.statusCode ?? 500) >= 400) {
63
+ reject(new Error(text || `Hub MCP request failed: ${response.statusCode}`));
64
+ return;
65
+ }
66
+ try {
67
+ resolve(JSON.parse(text));
68
+ } catch (error) {
69
+ reject(error);
70
+ }
71
+ });
72
+ });
73
+ request.on("error", reject);
74
+ if (payload) request.write(payload);
75
+ request.end();
76
+ });
77
+ }
78
+ //#endregion
79
+ export { createHubMcpBridgeServer as n, HttpHubMcpBridgeClient as t };
80
+
81
+ //# sourceMappingURL=bridge-D8_lAKaW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge-D8_lAKaW.js","names":[],"sources":["../src/mcp/bridge.ts"],"sourcesContent":["import http from \"node:http\";\nimport https from \"node:https\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { HubMcpToolDescriptor, HubMcpToolResult } from \"./tools.js\";\n\ntype JsonObject = Record<string, unknown>;\n\ninterface HubMcpBridgeClient {\n listTools(): Promise<HubMcpToolDescriptor[]>;\n callTool(name: string, args: unknown): Promise<HubMcpToolResult>;\n}\n\nexport class HttpHubMcpBridgeClient implements HubMcpBridgeClient {\n private readonly endpoint: string;\n\n constructor(endpoint: string) {\n if (!endpoint || endpoint.trim().length === 0) {\n throw new Error(\"TANGLE_HUB_MCP_ENDPOINT is required\");\n }\n this.endpoint = endpoint.replace(/\\/+$/, \"\");\n }\n\n async listTools(): Promise<HubMcpToolDescriptor[]> {\n const response = await requestJson<{ tools: HubMcpToolDescriptor[] }>(\n \"GET\",\n `${this.endpoint}/tools`,\n );\n return response.tools;\n }\n\n async callTool(name: string, args: unknown): Promise<HubMcpToolResult> {\n return requestJson<HubMcpToolResult>(\"POST\", `${this.endpoint}/call`, {\n name,\n arguments: args ?? {},\n });\n }\n}\n\nexport function createHubMcpBridgeServer(client: HubMcpBridgeClient): Server {\n const server = new Server(\n { name: \"tangle-hub\", version: \"0.1.0\" },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const tools = await client.listTools();\n return {\n tools: tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n })),\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) =>\n client.callTool(request.params.name, request.params.arguments ?? {}),\n );\n\n return server;\n}\n\nfunction requestJson<T>(\n method: \"GET\" | \"POST\",\n url: string,\n body?: JsonObject,\n) {\n return new Promise<T>((resolve, reject) => {\n const parsed = new URL(url);\n const payload = body ? JSON.stringify(body) : undefined;\n const transport = parsed.protocol === \"https:\" ? https : http;\n const request = transport.request(\n {\n protocol: parsed.protocol,\n hostname: parsed.hostname,\n port: parsed.port,\n path: `${parsed.pathname}${parsed.search}`,\n method,\n rejectUnauthorized: parsed.hostname !== \"127.0.0.1\",\n headers: {\n Accept: \"application/json\",\n ...(payload\n ? {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(payload).toString(),\n }\n : {}),\n },\n },\n (response) => {\n const chunks: Buffer[] = [];\n response.on(\"data\", (chunk) => {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n });\n response.on(\"end\", () => {\n const text = Buffer.concat(chunks).toString(\"utf8\");\n if ((response.statusCode ?? 500) >= 400) {\n reject(\n new Error(\n text || `Hub MCP request failed: ${response.statusCode}`,\n ),\n );\n return;\n }\n try {\n resolve(JSON.parse(text) as T);\n } catch (error) {\n reject(error);\n }\n });\n },\n );\n request.on(\"error\", reject);\n if (payload) request.write(payload);\n request.end();\n });\n}\n"],"mappings":";;;;;AAgBA,IAAa,yBAAb,MAAkE;CAChE;CAEA,YAAY,UAAkB;AAC5B,MAAI,CAAC,YAAY,SAAS,MAAM,CAAC,WAAW,EAC1C,OAAM,IAAI,MAAM,sCAAsC;AAExD,OAAK,WAAW,SAAS,QAAQ,QAAQ,GAAG;;CAG9C,MAAM,YAA6C;AAKjD,UAAO,MAJgB,YACrB,OACA,GAAG,KAAK,SAAS,QAClB,EACe;;CAGlB,MAAM,SAAS,MAAc,MAA0C;AACrE,SAAO,YAA8B,QAAQ,GAAG,KAAK,SAAS,QAAQ;GACpE;GACA,WAAW,QAAQ,EAAE;GACtB,CAAC;;;AAIN,SAAgB,yBAAyB,QAAoC;CAC3E,MAAM,SAAS,IAAI,OACjB;EAAE,MAAM;EAAc,SAAS;EAAS,EACxC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,EAAE,CAChC;AAED,QAAO,kBAAkB,wBAAwB,YAAY;AAE3D,SAAO,EACL,QAAO,MAFW,OAAO,WAAW,EAEvB,KAAK,UAAU;GAC1B,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,aAAa,KAAK;GACnB,EAAE,EACJ;GACD;AAEF,QAAO,kBAAkB,uBAAuB,OAAO,YACrD,OAAO,SAAS,QAAQ,OAAO,MAAM,QAAQ,OAAO,aAAa,EAAE,CAAC,CACrE;AAED,QAAO;;AAGT,SAAS,YACP,QACA,KACA,MACA;AACA,QAAO,IAAI,SAAY,SAAS,WAAW;EACzC,MAAM,SAAS,IAAI,IAAI,IAAI;EAC3B,MAAM,UAAU,OAAO,KAAK,UAAU,KAAK,GAAG,KAAA;EAE9C,MAAM,WADY,OAAO,aAAa,WAAW,QAAQ,MAC/B,QACxB;GACE,UAAU,OAAO;GACjB,UAAU,OAAO;GACjB,MAAM,OAAO;GACb,MAAM,GAAG,OAAO,WAAW,OAAO;GAClC;GACA,oBAAoB,OAAO,aAAa;GACxC,SAAS;IACP,QAAQ;IACR,GAAI,UACA;KACE,gBAAgB;KAChB,kBAAkB,OAAO,WAAW,QAAQ,CAAC,UAAU;KACxD,GACD,EAAE;IACP;GACF,GACA,aAAa;GACZ,MAAM,SAAmB,EAAE;AAC3B,YAAS,GAAG,SAAS,UAAU;AAC7B,WAAO,KAAK,OAAO,SAAS,MAAM,GAAG,QAAQ,OAAO,KAAK,MAAM,CAAC;KAChE;AACF,YAAS,GAAG,aAAa;IACvB,MAAM,OAAO,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO;AACnD,SAAK,SAAS,cAAc,QAAQ,KAAK;AACvC,YACE,IAAI,MACF,QAAQ,2BAA2B,SAAS,aAC7C,CACF;AACD;;AAEF,QAAI;AACF,aAAQ,KAAK,MAAM,KAAK,CAAM;aACvB,OAAO;AACd,YAAO,MAAM;;KAEf;IAEL;AACD,UAAQ,GAAG,SAAS,OAAO;AAC3B,MAAI,QAAS,SAAQ,MAAM,QAAQ;AACnC,UAAQ,KAAK;GACb"}