@aroha-sdk/mcp-bridge 1.0.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.
- package/package.json +21 -0
- package/src/bridge.test.ts +186 -0
- package/src/index.ts +165 -0
- package/tsconfig.json +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aroha-sdk/mcp-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Aroha ↔ MCP (Model Context Protocol) adapter",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@aroha-sdk/core": "^1.0.0",
|
|
14
|
+
"@aroha-sdk/registry": "^1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.4.5",
|
|
18
|
+
"vitest": "^1.6.0",
|
|
19
|
+
"@types/node": "^20.14.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { mcpToolsToArohaCapabilities, arohaAgentToMcpTools, type McpToolDefinition } from "./index.js";
|
|
3
|
+
import type { DiscoveredAgent } from "@aroha-sdk/registry";
|
|
4
|
+
import type { ArohaEnvelope, ArohaClient } from "@aroha-sdk/core";
|
|
5
|
+
import * as ed from "@noble/ed25519";
|
|
6
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
7
|
+
|
|
8
|
+
ed.etc.sha512Sync = (...m: Parameters<typeof sha512>) => sha512(...m);
|
|
9
|
+
|
|
10
|
+
function makeMcpTool(name: string, description = "A test tool"): McpToolDefinition {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
description,
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: { query: { type: "string" } },
|
|
17
|
+
required: ["query"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeDiscoveredAgent(): DiscoveredAgent {
|
|
23
|
+
return {
|
|
24
|
+
record: {
|
|
25
|
+
didHash: "0xtest-provider",
|
|
26
|
+
ownerAddress: "0xOwner",
|
|
27
|
+
manifestCID: "Qm123",
|
|
28
|
+
publicKeyB64: Buffer.alloc(32).toString("base64url"),
|
|
29
|
+
stakedWei: 1_000_000_000_000_000_000n,
|
|
30
|
+
reputationScore: 8000,
|
|
31
|
+
active: true,
|
|
32
|
+
registeredAt: Date.now() - 86400_000,
|
|
33
|
+
lastUpdatedAt: Date.now(),
|
|
34
|
+
},
|
|
35
|
+
manifest: {
|
|
36
|
+
aroha: "1.0",
|
|
37
|
+
did: "did:aroha:test-provider",
|
|
38
|
+
name: "Test Provider",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
endpoint: "https://provider.example.com/aroha",
|
|
41
|
+
capabilities: [
|
|
42
|
+
{
|
|
43
|
+
id: "search-flights",
|
|
44
|
+
version: "1.0.0",
|
|
45
|
+
description: "Search for flights",
|
|
46
|
+
inputSchema: { type: "object", properties: { origin: { type: "string" } } },
|
|
47
|
+
outputSchema: { type: "object", properties: { flights: { type: "array" } } },
|
|
48
|
+
async: false,
|
|
49
|
+
maxResponseMs: 5000,
|
|
50
|
+
requiredTrustLevel: 1,
|
|
51
|
+
pricing: { model: "free" },
|
|
52
|
+
sagaRole: "none",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "book-flight",
|
|
56
|
+
version: "1.0.0",
|
|
57
|
+
description: "Book a flight",
|
|
58
|
+
inputSchema: { type: "object", properties: { flightId: { type: "string" } } },
|
|
59
|
+
outputSchema: { type: "object", properties: { booking: { type: "string" } } },
|
|
60
|
+
async: false,
|
|
61
|
+
maxResponseMs: 10000,
|
|
62
|
+
requiredTrustLevel: 2,
|
|
63
|
+
pricing: { model: "per-transaction", fee: "10.00" },
|
|
64
|
+
sagaRole: "participant",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
protocols: ["aroha/1.0"],
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
signature: "dummy-sig",
|
|
70
|
+
},
|
|
71
|
+
endpoint: "https://provider.example.com/aroha",
|
|
72
|
+
publicKey: new Uint8Array(32),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("mcpToolsToArohaCapabilities", () => {
|
|
77
|
+
it("converts MCP tools to Aroha capability definitions", () => {
|
|
78
|
+
const tools = [makeMcpTool("aroha_search_flights"), makeMcpTool("aroha_book_hotel")];
|
|
79
|
+
const caps = mcpToolsToArohaCapabilities(tools);
|
|
80
|
+
|
|
81
|
+
expect(caps).toHaveLength(2);
|
|
82
|
+
expect(caps[0].id).toBe("search-flights");
|
|
83
|
+
expect(caps[1].id).toBe("book-hotel");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("sets correct default fields", () => {
|
|
87
|
+
const caps = mcpToolsToArohaCapabilities([makeMcpTool("aroha_do_thing", "Does the thing")]);
|
|
88
|
+
const cap = caps[0];
|
|
89
|
+
expect(cap.version).toBe("1.0.0");
|
|
90
|
+
expect(cap.async).toBe(false);
|
|
91
|
+
expect(cap.requiredTrustLevel).toBe(1);
|
|
92
|
+
expect(cap.pricing.model).toBe("free");
|
|
93
|
+
expect(cap.sagaRole).toBe("none");
|
|
94
|
+
expect(cap.description).toBe("Does the thing");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("preserves the inputSchema", () => {
|
|
98
|
+
const tool = makeMcpTool("aroha_test");
|
|
99
|
+
const caps = mcpToolsToArohaCapabilities([tool]);
|
|
100
|
+
expect(caps[0].inputSchema).toEqual(tool.inputSchema);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("arohaAgentToMcpTools", () => {
|
|
105
|
+
it("produces one MCP tool per capability", async () => {
|
|
106
|
+
const priv = ed.utils.randomPrivateKey();
|
|
107
|
+
const agent = makeDiscoveredAgent();
|
|
108
|
+
const mockClient = { send: vi.fn(), stream: vi.fn(), fetchDIDDocument: vi.fn() } as unknown as ArohaClient;
|
|
109
|
+
|
|
110
|
+
const { tools } = arohaAgentToMcpTools(
|
|
111
|
+
agent,
|
|
112
|
+
"did:aroha:orchestrator",
|
|
113
|
+
priv,
|
|
114
|
+
mockClient,
|
|
115
|
+
"saga-test"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(tools).toHaveLength(2);
|
|
119
|
+
expect(tools[0].name).toBe("aroha_search_flights");
|
|
120
|
+
expect(tools[1].name).toBe("aroha_book_flight");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("tool definitions include _aroha metadata", async () => {
|
|
124
|
+
const priv = ed.utils.randomPrivateKey();
|
|
125
|
+
const agent = makeDiscoveredAgent();
|
|
126
|
+
const mockClient = { send: vi.fn(), stream: vi.fn(), fetchDIDDocument: vi.fn() } as unknown as ArohaClient;
|
|
127
|
+
|
|
128
|
+
const { tools } = arohaAgentToMcpTools(
|
|
129
|
+
agent,
|
|
130
|
+
"did:aroha:orchestrator",
|
|
131
|
+
priv,
|
|
132
|
+
mockClient,
|
|
133
|
+
"saga-test"
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(tools[0]._aroha?.agentDID).toBe("did:aroha:test-provider");
|
|
137
|
+
expect(tools[0]._aroha?.capabilityId).toBe("search-flights");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("handleToolCall returns error for unknown tool", async () => {
|
|
141
|
+
const priv = ed.utils.randomPrivateKey();
|
|
142
|
+
const agent = makeDiscoveredAgent();
|
|
143
|
+
const mockClient = { send: vi.fn(), stream: vi.fn(), fetchDIDDocument: vi.fn() } as unknown as ArohaClient;
|
|
144
|
+
|
|
145
|
+
const { handleToolCall } = arohaAgentToMcpTools(
|
|
146
|
+
agent,
|
|
147
|
+
"did:aroha:orchestrator",
|
|
148
|
+
priv,
|
|
149
|
+
mockClient,
|
|
150
|
+
"saga-test"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const result = await handleToolCall("aroha_nonexistent", {});
|
|
154
|
+
expect(result.isError).toBe(true);
|
|
155
|
+
expect(result.content[0].text).toMatch(/Unknown tool/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("handleToolCall calls client.send and returns result", async () => {
|
|
159
|
+
const priv = ed.utils.randomPrivateKey();
|
|
160
|
+
const agent = makeDiscoveredAgent();
|
|
161
|
+
|
|
162
|
+
const mockResponse: Partial<ArohaEnvelope> = {
|
|
163
|
+
type: "ArohaResponse",
|
|
164
|
+
body: { capability: "search-flights", result: { flights: ["AA100"] } } as ArohaEnvelope["body"],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mockClient = {
|
|
168
|
+
send: vi.fn().mockResolvedValue(mockResponse),
|
|
169
|
+
stream: vi.fn(),
|
|
170
|
+
fetchDIDDocument: vi.fn(),
|
|
171
|
+
} as unknown as ArohaClient;
|
|
172
|
+
|
|
173
|
+
const { handleToolCall } = arohaAgentToMcpTools(
|
|
174
|
+
agent,
|
|
175
|
+
"did:aroha:orchestrator",
|
|
176
|
+
priv,
|
|
177
|
+
mockClient,
|
|
178
|
+
"saga-test"
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const result = await handleToolCall("aroha_search_flights", { origin: "JFK" });
|
|
182
|
+
expect(result.isError).toBeUndefined();
|
|
183
|
+
expect(result.content[0].text).toContain("AA100");
|
|
184
|
+
expect(mockClient.send).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aroha ↔ MCP Bridge
|
|
3
|
+
*
|
|
4
|
+
* Two directions:
|
|
5
|
+
*
|
|
6
|
+
* toMcp() — Wrap a discovered Aroha agent's capabilities as MCP tool definitions.
|
|
7
|
+
* Claude Desktop / any MCP client can call them as normal tools.
|
|
8
|
+
* The bridge transparently sends ArohaRequest messages under the hood.
|
|
9
|
+
*
|
|
10
|
+
* fromMcp() — Expose an MCP server's tools as a Aroha capability manifest.
|
|
11
|
+
* Any Aroha orchestrator can discover and call the MCP server
|
|
12
|
+
* without knowing it's MCP-backed.
|
|
13
|
+
*
|
|
14
|
+
* The bridge is purely an adapter — it translates formats.
|
|
15
|
+
* It adds no business logic and changes no message semantics.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
buildEnvelope,
|
|
20
|
+
ArohaClient,
|
|
21
|
+
type ArohaEnvelope,
|
|
22
|
+
} from "@aroha-sdk/core";
|
|
23
|
+
import { type ArohaRequestBody, type ArohaReserveBody } from "@aroha-sdk/core";
|
|
24
|
+
import { type DiscoveredAgent, type CapabilityDefinition } from "@aroha-sdk/registry";
|
|
25
|
+
|
|
26
|
+
// ─── MCP Types (subset of the MCP spec we need) ───────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface McpToolDefinition {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object";
|
|
33
|
+
properties: Record<string, unknown>;
|
|
34
|
+
required?: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Aroha metadata — carried as an extension field */
|
|
37
|
+
_aroha?: {
|
|
38
|
+
agentDID: string;
|
|
39
|
+
capabilityId: string;
|
|
40
|
+
sagaRole: "none" | "participant";
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type McpToolCallResult = {
|
|
45
|
+
content: Array<{ type: "text"; text: string }>;
|
|
46
|
+
isError?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ─── Aroha → MCP ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert a discovered Aroha agent's capabilities into MCP tool definitions.
|
|
53
|
+
* Returns both the definitions (for registering with MCP) and a handler
|
|
54
|
+
* function that executes the tool call via Aroha.
|
|
55
|
+
*/
|
|
56
|
+
export function arohaAgentToMcpTools(
|
|
57
|
+
agent: DiscoveredAgent,
|
|
58
|
+
orchestratorDID: string,
|
|
59
|
+
orchestratorPrivateKey: Uint8Array,
|
|
60
|
+
client: ArohaClient,
|
|
61
|
+
correlationId: string
|
|
62
|
+
): {
|
|
63
|
+
tools: McpToolDefinition[];
|
|
64
|
+
handleToolCall: (toolName: string, args: Record<string, unknown>) => Promise<McpToolCallResult>;
|
|
65
|
+
} {
|
|
66
|
+
const tools: McpToolDefinition[] = agent.manifest.capabilities.map((cap) =>
|
|
67
|
+
capabilityToMcpTool(cap, agent.manifest.did)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handleToolCall = async (
|
|
71
|
+
toolName: string,
|
|
72
|
+
args: Record<string, unknown>
|
|
73
|
+
): Promise<McpToolCallResult> => {
|
|
74
|
+
const cap = agent.manifest.capabilities.find(
|
|
75
|
+
(c) => mcpToolName(c.id) === toolName
|
|
76
|
+
);
|
|
77
|
+
if (!cap) {
|
|
78
|
+
return { content: [{ type: "text", text: `Unknown tool: ${toolName}` }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const body: ArohaRequestBody = { capability: cap.id, params: args };
|
|
83
|
+
const envelope = await buildEnvelope(
|
|
84
|
+
"ArohaRequest",
|
|
85
|
+
orchestratorDID,
|
|
86
|
+
agent.manifest.did,
|
|
87
|
+
body,
|
|
88
|
+
correlationId,
|
|
89
|
+
orchestratorPrivateKey
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const response = await client.send(agent.endpoint, envelope);
|
|
93
|
+
if (!response) {
|
|
94
|
+
return { content: [{ type: "text", text: "Agent returned no response (async)" }] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.type === "ArohaError") {
|
|
98
|
+
const err = response.body as { message: string };
|
|
99
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = (response.body as { result: unknown }).result;
|
|
103
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { content: [{ type: "text", text: String(err) }], isError: true };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return { tools, handleToolCall };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── MCP → Aroha ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert MCP tool definitions into a Aroha capability manifest fragment.
|
|
116
|
+
* Use this to register an MCP server as a Aroha agent.
|
|
117
|
+
*/
|
|
118
|
+
export function mcpToolsToArohaCapabilities(
|
|
119
|
+
tools: McpToolDefinition[]
|
|
120
|
+
): CapabilityDefinition[] {
|
|
121
|
+
return tools.map((tool) => ({
|
|
122
|
+
id: arohaCapabilityId(tool.name),
|
|
123
|
+
version: "1.0.0",
|
|
124
|
+
description: tool.description,
|
|
125
|
+
inputSchema: tool.inputSchema as Record<string, unknown>,
|
|
126
|
+
outputSchema: { type: "object", properties: { result: { type: "object" } } },
|
|
127
|
+
async: false,
|
|
128
|
+
maxResponseMs: 30_000,
|
|
129
|
+
requiredTrustLevel: 1,
|
|
130
|
+
pricing: { model: "free" },
|
|
131
|
+
sagaRole: "none",
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function capabilityToMcpTool(
|
|
138
|
+
cap: CapabilityDefinition,
|
|
139
|
+
agentDID: string
|
|
140
|
+
): McpToolDefinition {
|
|
141
|
+
return {
|
|
142
|
+
name: mcpToolName(cap.id),
|
|
143
|
+
description: cap.description ?? cap.id,
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: (cap.inputSchema as { properties?: Record<string,unknown> })?.properties ?? {},
|
|
147
|
+
required: (cap.inputSchema as { required?: string[] })?.required,
|
|
148
|
+
},
|
|
149
|
+
_aroha: {
|
|
150
|
+
agentDID,
|
|
151
|
+
capabilityId: cap.id,
|
|
152
|
+
sagaRole: cap.sagaRole,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Convert a Aroha capability ID to a valid MCP tool name (snake_case). */
|
|
158
|
+
function mcpToolName(capabilityId: string): string {
|
|
159
|
+
return `aroha_${capabilityId.replace(/-/g, "_")}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Convert an MCP tool name back to a Aroha capability ID. */
|
|
163
|
+
function arohaCapabilityId(mcpName: string): string {
|
|
164
|
+
return mcpName.replace(/^aroha_/, "").replace(/_/g, "-");
|
|
165
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": { "composite": true, "outDir": "./dist", "rootDir": "./src" },
|
|
4
|
+
"references": [{ "path": "../aroha-core" }, { "path": "../aroha-registry" }],
|
|
5
|
+
"include": ["src/**/*"],
|
|
6
|
+
"exclude": ["dist", "node_modules"]
|
|
7
|
+
}
|