@slashfi/agents-sdk 0.34.1 → 0.36.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/README.md +2 -1
- package/dist/agent-definitions/auth.d.ts +3 -3
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +10 -4
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +12 -3
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +35 -16
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +17 -22
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/auth-governance.js.map +1 -1
- package/dist/call-agent-schema.d.ts.map +1 -1
- package/dist/call-agent-schema.js +1 -2
- package/dist/call-agent-schema.js.map +1 -1
- package/dist/cjs/agent-definitions/auth.js +10 -4
- package/dist/cjs/agent-definitions/auth.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/agent-definitions/integrations.js +35 -16
- package/dist/cjs/agent-definitions/integrations.js.map +1 -1
- package/dist/cjs/agent-definitions/remote-registry.js +17 -22
- package/dist/cjs/agent-definitions/remote-registry.js.map +1 -1
- package/dist/cjs/agent-definitions/users.js.map +1 -1
- package/dist/cjs/auth-governance.js.map +1 -1
- package/dist/cjs/call-agent-schema.js +1 -2
- package/dist/cjs/call-agent-schema.js.map +1 -1
- package/dist/cjs/define.js.map +1 -1
- package/dist/cjs/events.js.map +1 -1
- package/dist/cjs/index.js +6 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-manager.js.map +1 -1
- package/dist/cjs/registry-consumer.js +60 -15
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/registry.js +61 -11
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/server.js +143 -192
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/types.js +13 -0
- package/dist/cjs/types.js.map +1 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js.map +1 -1
- package/dist/events.d.ts +85 -9
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +1 -1
- package/dist/key-manager.js.map +1 -1
- package/dist/registry-consumer.d.ts +8 -8
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +60 -15
- package/dist/registry-consumer.js.map +1 -1
- package/dist/registry.d.ts +16 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -11
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +136 -185
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/auth.ts +31 -14
- package/src/agent-definitions/config.ts +4 -4
- package/src/agent-definitions/integrations.ts +119 -63
- package/src/agent-definitions/remote-registry.ts +65 -38
- package/src/agent-definitions/users.ts +36 -3
- package/src/auth-governance.ts +2 -2
- package/src/call-agent-schema.test.ts +4 -1
- package/src/call-agent-schema.ts +4 -3
- package/src/consumer.test.ts +4 -1
- package/src/define.ts +18 -12
- package/src/events.ts +83 -9
- package/src/hooks.test.ts +439 -0
- package/src/index.ts +14 -1
- package/src/key-manager.ts +9 -2
- package/src/registry-consumer.ts +85 -24
- package/src/registry.ts +88 -13
- package/src/server.ts +215 -239
- package/src/types.ts +62 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tools/call/call_agent and tools/call/list_agents hooks.
|
|
3
|
+
*
|
|
4
|
+
* These go through the full production path:
|
|
5
|
+
* HTTP POST → JSON-RPC → MCP tools/call → handleToolCall → registry hooks
|
|
6
|
+
*
|
|
7
|
+
* Mirrors how atlas-os uses these hooks to intercept and route requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import {
|
|
12
|
+
createAgentRegistry,
|
|
13
|
+
createAgentServer,
|
|
14
|
+
defineAgent,
|
|
15
|
+
defineTool,
|
|
16
|
+
} from "./index";
|
|
17
|
+
import type {
|
|
18
|
+
AgentDefinition,
|
|
19
|
+
AgentServer,
|
|
20
|
+
CallAgentToolCallEvent,
|
|
21
|
+
ListAgentsToolCallEvent,
|
|
22
|
+
} from "./index";
|
|
23
|
+
|
|
24
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
// Test agents
|
|
26
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
27
|
+
|
|
28
|
+
const echoAgent = defineAgent({
|
|
29
|
+
path: "@echo",
|
|
30
|
+
entrypoint: "Echo agent",
|
|
31
|
+
config: { name: "Echo", description: "Echoes input back" },
|
|
32
|
+
visibility: "public" as const,
|
|
33
|
+
tools: [
|
|
34
|
+
defineTool({
|
|
35
|
+
name: "echo",
|
|
36
|
+
description: "Echo the input",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { message: { type: "string" } },
|
|
40
|
+
required: ["message"],
|
|
41
|
+
},
|
|
42
|
+
execute: async (input: { message: string }) => ({
|
|
43
|
+
echoed: input.message,
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const mathAgent = defineAgent({
|
|
50
|
+
path: "@math",
|
|
51
|
+
entrypoint: "Math agent",
|
|
52
|
+
config: { name: "Math", description: "Does math" },
|
|
53
|
+
visibility: "public" as const,
|
|
54
|
+
tools: [
|
|
55
|
+
defineTool({
|
|
56
|
+
name: "add",
|
|
57
|
+
description: "Add two numbers",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
a: { type: "number" },
|
|
62
|
+
b: { type: "number" },
|
|
63
|
+
},
|
|
64
|
+
required: ["a", "b"],
|
|
65
|
+
},
|
|
66
|
+
execute: async (input: { a: number; b: number }) => ({
|
|
67
|
+
result: input.a + input.b,
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
74
|
+
// Helper: MCP JSON-RPC call through HTTP
|
|
75
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
76
|
+
|
|
77
|
+
let nextId = 1;
|
|
78
|
+
async function mcpCall(
|
|
79
|
+
baseUrl: string,
|
|
80
|
+
toolName: string,
|
|
81
|
+
args: Record<string, unknown>,
|
|
82
|
+
) {
|
|
83
|
+
const res = await fetch(baseUrl, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
jsonrpc: "2.0",
|
|
88
|
+
id: nextId++,
|
|
89
|
+
method: "tools/call",
|
|
90
|
+
params: { name: toolName, arguments: args },
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const json = (await res.json()) as {
|
|
95
|
+
result?: { content?: Array<{ text: string }> };
|
|
96
|
+
};
|
|
97
|
+
const text = json.result?.content?.[0]?.text;
|
|
98
|
+
return text ? JSON.parse(text) : json;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
102
|
+
// tools/call/call_agent tests
|
|
103
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
|
|
105
|
+
describe("tools/call/call_agent hook", () => {
|
|
106
|
+
const PORT = 19870;
|
|
107
|
+
|
|
108
|
+
test("resolve() short-circuits the default handler", async () => {
|
|
109
|
+
// This mirrors atlas-os: intercept call_agent, route externally, resolve()
|
|
110
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
111
|
+
registry.register(echoAgent);
|
|
112
|
+
|
|
113
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
114
|
+
event.resolve({
|
|
115
|
+
success: true,
|
|
116
|
+
result: { intercepted: true, originalPath: event.request.path },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const server = createAgentServer(registry, { port: PORT });
|
|
121
|
+
await server.start();
|
|
122
|
+
|
|
123
|
+
const result = await mcpCall(`http://localhost:${PORT}`, "call_agent", {
|
|
124
|
+
action: "execute_tool",
|
|
125
|
+
path: "@echo",
|
|
126
|
+
tool: "echo",
|
|
127
|
+
params: { message: "hello" },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
expect(result.result.intercepted).toBe(true);
|
|
132
|
+
expect(result.result.originalPath).toBe("@echo");
|
|
133
|
+
await server.stop();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("next() runs default handler and hook observes", async () => {
|
|
137
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
138
|
+
registry.register(echoAgent);
|
|
139
|
+
|
|
140
|
+
let hookSawRequest = false;
|
|
141
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
142
|
+
hookSawRequest = true;
|
|
143
|
+
await event.next();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const server = createAgentServer(registry, { port: PORT + 1 });
|
|
147
|
+
await server.start();
|
|
148
|
+
|
|
149
|
+
const result = await mcpCall(`http://localhost:${PORT + 1}`, "call_agent", {
|
|
150
|
+
action: "execute_tool",
|
|
151
|
+
path: "@echo",
|
|
152
|
+
tool: "echo",
|
|
153
|
+
params: { message: "passthrough" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(hookSawRequest).toBe(true);
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
expect(result.result.echoed).toBe("passthrough");
|
|
159
|
+
await server.stop();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("next() with modified request reroutes the call", async () => {
|
|
163
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
164
|
+
registry.register(echoAgent);
|
|
165
|
+
registry.register(mathAgent);
|
|
166
|
+
|
|
167
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
168
|
+
if (event.request.action === "execute_tool" && event.request.path === "@echo") {
|
|
169
|
+
await event.next({
|
|
170
|
+
...event.request,
|
|
171
|
+
path: "@math",
|
|
172
|
+
tool: "add",
|
|
173
|
+
params: { a: 10, b: 20 },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const server = createAgentServer(registry, { port: PORT + 2 });
|
|
179
|
+
await server.start();
|
|
180
|
+
|
|
181
|
+
const result = await mcpCall(`http://localhost:${PORT + 2}`, "call_agent", {
|
|
182
|
+
action: "execute_tool",
|
|
183
|
+
path: "@echo",
|
|
184
|
+
tool: "echo",
|
|
185
|
+
params: { message: "this gets rerouted" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect(result.result.result).toBe(30);
|
|
190
|
+
await server.stop();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("no listener = default behavior unchanged", async () => {
|
|
194
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
195
|
+
registry.register(echoAgent);
|
|
196
|
+
|
|
197
|
+
const server = createAgentServer(registry, { port: PORT + 3 });
|
|
198
|
+
await server.start();
|
|
199
|
+
|
|
200
|
+
const result = await mcpCall(`http://localhost:${PORT + 3}`, "call_agent", {
|
|
201
|
+
action: "execute_tool",
|
|
202
|
+
path: "@echo",
|
|
203
|
+
tool: "echo",
|
|
204
|
+
params: { message: "no hook" },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result.success).toBe(true);
|
|
208
|
+
expect(result.result.echoed).toBe("no hook");
|
|
209
|
+
await server.stop();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("resolve() with error response", async () => {
|
|
213
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
214
|
+
registry.register(echoAgent);
|
|
215
|
+
|
|
216
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
217
|
+
event.resolve({
|
|
218
|
+
success: false,
|
|
219
|
+
error: "Agent not available in remote registry",
|
|
220
|
+
code: "AGENT_NOT_FOUND",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const server = createAgentServer(registry, { port: PORT + 4 });
|
|
225
|
+
await server.start();
|
|
226
|
+
|
|
227
|
+
const result = await mcpCall(`http://localhost:${PORT + 4}`, "call_agent", {
|
|
228
|
+
action: "execute_tool",
|
|
229
|
+
path: "@nonexistent",
|
|
230
|
+
tool: "foo",
|
|
231
|
+
params: {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(false);
|
|
235
|
+
expect(result.error).toBe("Agent not available in remote registry");
|
|
236
|
+
await server.stop();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
241
|
+
// tools/call/list_agents tests
|
|
242
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
+
|
|
244
|
+
describe("tools/call/list_agents hook", () => {
|
|
245
|
+
const PORT = 19880;
|
|
246
|
+
|
|
247
|
+
test("next() with additional agents merges into listing", async () => {
|
|
248
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
249
|
+
registry.register(echoAgent);
|
|
250
|
+
|
|
251
|
+
const remoteAgent = defineAgent({
|
|
252
|
+
path: "@remote-db",
|
|
253
|
+
entrypoint: "Remote database agent",
|
|
254
|
+
config: { name: "RemoteDB", description: "Query remote databases" },
|
|
255
|
+
visibility: "public" as const,
|
|
256
|
+
tools: [
|
|
257
|
+
defineTool({
|
|
258
|
+
name: "query",
|
|
259
|
+
description: "Run a SQL query",
|
|
260
|
+
inputSchema: { type: "object", properties: { sql: { type: "string" } } },
|
|
261
|
+
execute: async () => ({ rows: [] }),
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
267
|
+
await event.next([remoteAgent]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const server = createAgentServer(registry, { port: PORT });
|
|
271
|
+
await server.start();
|
|
272
|
+
|
|
273
|
+
const result = await mcpCall(`http://localhost:${PORT}`, "list_agents", {});
|
|
274
|
+
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
277
|
+
expect(paths).toContain("@echo");
|
|
278
|
+
expect(paths).toContain("@remote-db");
|
|
279
|
+
expect(result.total).toBe(2);
|
|
280
|
+
await server.stop();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("injected agents appear in BM25 search results", async () => {
|
|
284
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
285
|
+
registry.register(echoAgent);
|
|
286
|
+
|
|
287
|
+
const remoteAgent = defineAgent({
|
|
288
|
+
path: "@snowflake",
|
|
289
|
+
entrypoint: "Snowflake analytics",
|
|
290
|
+
config: { name: "Snowflake", description: "Analytics data warehouse queries" },
|
|
291
|
+
visibility: "public" as const,
|
|
292
|
+
tools: [
|
|
293
|
+
defineTool({
|
|
294
|
+
name: "run_query",
|
|
295
|
+
description: "Run an analytics query on Snowflake",
|
|
296
|
+
inputSchema: { type: "object", properties: { sql: { type: "string" } } },
|
|
297
|
+
execute: async () => ({ rows: [] }),
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
303
|
+
await event.next([remoteAgent]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const server = createAgentServer(registry, { port: PORT + 1 });
|
|
307
|
+
await server.start();
|
|
308
|
+
|
|
309
|
+
const result = await mcpCall(`http://localhost:${PORT + 1}`, "list_agents", {
|
|
310
|
+
query: "analytics warehouse",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(result.success).toBe(true);
|
|
314
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
315
|
+
expect(paths).toContain("@snowflake");
|
|
316
|
+
await server.stop();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("resolve() fully replaces the response", async () => {
|
|
320
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
321
|
+
registry.register(echoAgent);
|
|
322
|
+
registry.register(mathAgent);
|
|
323
|
+
|
|
324
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
325
|
+
event.resolve({
|
|
326
|
+
success: true,
|
|
327
|
+
total: 1,
|
|
328
|
+
agents: [
|
|
329
|
+
{
|
|
330
|
+
path: "@custom-only",
|
|
331
|
+
name: "Custom",
|
|
332
|
+
description: "Only this one",
|
|
333
|
+
tools: ["do_stuff"],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const server = createAgentServer(registry, { port: PORT + 2 });
|
|
340
|
+
await server.start();
|
|
341
|
+
|
|
342
|
+
const result = await mcpCall(`http://localhost:${PORT + 2}`, "list_agents", {});
|
|
343
|
+
|
|
344
|
+
expect(result.success).toBe(true);
|
|
345
|
+
expect(result.agents.length).toBe(1);
|
|
346
|
+
expect(result.agents[0].path).toBe("@custom-only");
|
|
347
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
348
|
+
expect(paths).not.toContain("@echo");
|
|
349
|
+
expect(paths).not.toContain("@math");
|
|
350
|
+
await server.stop();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("no listener = default behavior unchanged", async () => {
|
|
354
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
355
|
+
registry.register(echoAgent);
|
|
356
|
+
registry.register(mathAgent);
|
|
357
|
+
|
|
358
|
+
const server = createAgentServer(registry, { port: PORT + 3 });
|
|
359
|
+
await server.start();
|
|
360
|
+
|
|
361
|
+
const result = await mcpCall(`http://localhost:${PORT + 3}`, "list_agents", {});
|
|
362
|
+
|
|
363
|
+
expect(result.success).toBe(true);
|
|
364
|
+
expect(result.total).toBe(2);
|
|
365
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
366
|
+
expect(paths).toContain("@echo");
|
|
367
|
+
expect(paths).toContain("@math");
|
|
368
|
+
await server.stop();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("deduplicates by path — injected agent overrides local", async () => {
|
|
372
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
373
|
+
registry.register(echoAgent);
|
|
374
|
+
|
|
375
|
+
const overrideEcho = defineAgent({
|
|
376
|
+
path: "@echo",
|
|
377
|
+
entrypoint: "Overridden echo",
|
|
378
|
+
config: { name: "Echo Override", description: "This replaced the original" },
|
|
379
|
+
visibility: "public" as const,
|
|
380
|
+
tools: [
|
|
381
|
+
defineTool({
|
|
382
|
+
name: "echo",
|
|
383
|
+
description: "Overridden echo tool",
|
|
384
|
+
inputSchema: { type: "object", properties: { message: { type: "string" } } },
|
|
385
|
+
execute: async () => ({ overridden: true }),
|
|
386
|
+
}),
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
391
|
+
await event.next([overrideEcho]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const server = createAgentServer(registry, { port: PORT + 4 });
|
|
395
|
+
await server.start();
|
|
396
|
+
|
|
397
|
+
const result = await mcpCall(`http://localhost:${PORT + 4}`, "list_agents", {});
|
|
398
|
+
|
|
399
|
+
expect(result.success).toBe(true);
|
|
400
|
+
expect(result.total).toBe(1);
|
|
401
|
+
expect(result.agents[0].description).toBe("This replaced the original");
|
|
402
|
+
await server.stop();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("event carries query/limit/cursor params", async () => {
|
|
406
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
407
|
+
registry.register(echoAgent);
|
|
408
|
+
|
|
409
|
+
let capturedEvent: {
|
|
410
|
+
query?: string;
|
|
411
|
+
limit?: number;
|
|
412
|
+
cursor?: string;
|
|
413
|
+
baseAgentCount: number;
|
|
414
|
+
} | null = null;
|
|
415
|
+
|
|
416
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
417
|
+
capturedEvent = {
|
|
418
|
+
query: event.query,
|
|
419
|
+
limit: event.limit,
|
|
420
|
+
cursor: event.cursor,
|
|
421
|
+
baseAgentCount: event.baseAgents.length,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const server = createAgentServer(registry, { port: PORT + 5 });
|
|
426
|
+
await server.start();
|
|
427
|
+
|
|
428
|
+
await mcpCall(`http://localhost:${PORT + 5}`, "list_agents", {
|
|
429
|
+
query: "echo",
|
|
430
|
+
limit: 5,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(capturedEvent).not.toBeNull();
|
|
434
|
+
expect(capturedEvent!.query).toBe("echo");
|
|
435
|
+
expect(capturedEvent!.limit).toBe(5);
|
|
436
|
+
expect(capturedEvent!.baseAgentCount).toBe(1);
|
|
437
|
+
await server.stop();
|
|
438
|
+
});
|
|
439
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -90,12 +90,23 @@ export type {
|
|
|
90
90
|
SecurityScheme,
|
|
91
91
|
AgentResource,
|
|
92
92
|
SecuritySchemeSummary,
|
|
93
|
+
AuthClientCredentialsTokenResult,
|
|
94
|
+
AuthSecretValue,
|
|
95
|
+
ExchangeTokenLinkedSuccess,
|
|
96
|
+
ExchangeTokenNeedsIdentity,
|
|
97
|
+
ExchangeTokenRejected,
|
|
98
|
+
ExchangeTokenToolResult,
|
|
93
99
|
IntegrationMethods,
|
|
94
100
|
IntegrationMethodResult,
|
|
95
101
|
IntegrationMethodContext,
|
|
96
102
|
IntegrationHooks,
|
|
97
103
|
Visibility,
|
|
98
104
|
} from "./types.js";
|
|
105
|
+
export {
|
|
106
|
+
isCallAgentErrorResponse,
|
|
107
|
+
isExchangeTokenLinkedSuccess,
|
|
108
|
+
isExchangeTokenNeedsIdentity,
|
|
109
|
+
} from "./types.js";
|
|
99
110
|
|
|
100
111
|
// Define functions
|
|
101
112
|
export { defineAgent, defineTool } from "./define.js";
|
|
@@ -124,7 +135,9 @@ export type {
|
|
|
124
135
|
ToolErrorEvent,
|
|
125
136
|
StepEvent,
|
|
126
137
|
InvokeEvent,
|
|
127
|
-
|
|
138
|
+
CallAgentToolCallEvent,
|
|
139
|
+
ListAgentsToolCallEvent,
|
|
140
|
+
ListAgentsResult,
|
|
128
141
|
EventMap,
|
|
129
142
|
SystemEventMap,
|
|
130
143
|
ListenerEntry,
|
package/src/key-manager.ts
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
* - Signs JWTs with the active key
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type JWK,
|
|
18
|
+
type JWTPayload,
|
|
19
|
+
SignJWT,
|
|
20
|
+
exportJWK,
|
|
21
|
+
generateKeyPair,
|
|
22
|
+
importJWK,
|
|
23
|
+
} from "jose";
|
|
17
24
|
|
|
18
25
|
// ── Types ──
|
|
19
26
|
|
|
@@ -236,7 +243,7 @@ export async function createKeyManager(
|
|
|
236
243
|
|
|
237
244
|
async signJwt(claims: Record<string, unknown>): Promise<string> {
|
|
238
245
|
const key = getActiveKey();
|
|
239
|
-
let builder = new SignJWT({ ...claims } as
|
|
246
|
+
let builder = new SignJWT({ ...claims } as JWTPayload)
|
|
240
247
|
.setProtectedHeader({ alg: ALG, kid: key.kid })
|
|
241
248
|
.setIssuer(issuer)
|
|
242
249
|
.setIssuedAt();
|
package/src/registry-consumer.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registry Consumer — Connects to registries and resolves refs.
|
|
3
3
|
*
|
|
4
|
-
* The consumer reads a `ConsumerConfig`,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The consumer reads a `ConsumerConfig`, connects to each registry at its MCP URL,
|
|
5
|
+
* resolves refs to agent definitions, and provides a unified interface for calling
|
|
6
|
+
* tools across all connected agents. Registry metadata comes from the MCP
|
|
7
|
+
* `initialize` handshake (`discover()`).
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```typescript
|
|
@@ -181,16 +182,15 @@ function buildRegistryAuthHeaders(
|
|
|
181
182
|
// Registry Discovery Types
|
|
182
183
|
// ============================================
|
|
183
184
|
|
|
184
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Registry configuration derived from the MCP `initialize` response and the registry
|
|
187
|
+
* URL (OAuth/JWKS paths follow the usual layout under `issuer`).
|
|
188
|
+
*/
|
|
185
189
|
export interface RegistryConfiguration {
|
|
186
190
|
issuer: string;
|
|
187
191
|
jwks_uri?: string;
|
|
188
192
|
token_endpoint?: string;
|
|
189
|
-
agents_endpoint?: string;
|
|
190
|
-
call_endpoint?: string;
|
|
191
193
|
supported_grant_types?: string[];
|
|
192
|
-
/** @deprecated Use agents_endpoint + GET /list instead */
|
|
193
|
-
agents?: string[];
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/** An agent definition as listed by a registry */
|
|
@@ -367,6 +367,73 @@ async function listFromMcpServer(
|
|
|
367
367
|
}];
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
function issuerFromMcpUrlAndServerInfo(
|
|
371
|
+
serverUrl: string,
|
|
372
|
+
serverInfo?: { name?: string },
|
|
373
|
+
): string {
|
|
374
|
+
const name = serverInfo?.name;
|
|
375
|
+
if (name && /^https?:\/\//.test(name)) {
|
|
376
|
+
return name.replace(/\/$/, "");
|
|
377
|
+
}
|
|
378
|
+
return new URL(serverUrl).origin;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Load registry OAuth-facing metadata via MCP initialize (same URL as tools/call).
|
|
383
|
+
*/
|
|
384
|
+
async function discoverRegistryViaMcp(
|
|
385
|
+
registryUrl: string,
|
|
386
|
+
authHeaders: Record<string, string>,
|
|
387
|
+
fetchFn: typeof globalThis.fetch,
|
|
388
|
+
): Promise<RegistryConfiguration> {
|
|
389
|
+
const serverUrl = registryUrl.replace(/\/$/, "");
|
|
390
|
+
const headers: Record<string, string> = {
|
|
391
|
+
"Content-Type": "application/json",
|
|
392
|
+
...authHeaders,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
let reqId = 0;
|
|
396
|
+
async function rpc(method: string, params?: Record<string, unknown>) {
|
|
397
|
+
const res = await fetchFn(serverUrl, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers,
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
jsonrpc: "2.0",
|
|
402
|
+
id: ++reqId,
|
|
403
|
+
method,
|
|
404
|
+
...(params && { params }),
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
throw new Error(`MCP initialize to ${serverUrl} failed: ${res.status}`);
|
|
409
|
+
}
|
|
410
|
+
const json = (await res.json()) as { result?: unknown; error?: { message: string } };
|
|
411
|
+
if (json.error) {
|
|
412
|
+
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
413
|
+
}
|
|
414
|
+
return json.result;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const initResult = (await rpc("initialize", {
|
|
418
|
+
protocolVersion: "2024-11-05",
|
|
419
|
+
capabilities: {},
|
|
420
|
+
clientInfo: { name: "agents-sdk-consumer", version: "1.0.0" },
|
|
421
|
+
})) as {
|
|
422
|
+
serverInfo?: { name?: string; version?: string };
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
await rpc("notifications/initialized").catch(() => {});
|
|
426
|
+
|
|
427
|
+
const issuer = issuerFromMcpUrlAndServerInfo(serverUrl, initResult?.serverInfo);
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
issuer,
|
|
431
|
+
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
432
|
+
token_endpoint: `${issuer}/oauth/token`,
|
|
433
|
+
supported_grant_types: ["client_credentials", "jwt_exchange"],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
370
437
|
/**
|
|
371
438
|
* Call a tool on a direct MCP server.
|
|
372
439
|
*/
|
|
@@ -571,17 +638,15 @@ export async function createRegistryConsumer(
|
|
|
571
638
|
const cached = discoveryCache.get(registryUrl);
|
|
572
639
|
if (cached) return cached;
|
|
573
640
|
|
|
574
|
-
const
|
|
575
|
-
const headers: Record<string, string> = registry
|
|
641
|
+
const authHeaders = registry
|
|
576
642
|
? buildRegistryAuthHeaders(registry, options.token)
|
|
577
643
|
: (options.token ? { Authorization: `Bearer ${options.token}` } : {});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const configuration = (await res.json()) as RegistryConfiguration;
|
|
644
|
+
|
|
645
|
+
const configuration = await discoverRegistryViaMcp(
|
|
646
|
+
registryUrl,
|
|
647
|
+
authHeaders,
|
|
648
|
+
fetchFn,
|
|
649
|
+
);
|
|
585
650
|
discoveryCache.set(registryUrl, configuration);
|
|
586
651
|
return configuration;
|
|
587
652
|
}
|
|
@@ -591,9 +656,7 @@ export async function createRegistryConsumer(
|
|
|
591
656
|
registry: ResolvedRegistry,
|
|
592
657
|
query?: string,
|
|
593
658
|
): Promise<AgentListing[]> {
|
|
594
|
-
const
|
|
595
|
-
const mcpUrl =
|
|
596
|
-
configuration.call_endpoint ?? registry.url.replace(/\/$/, "");
|
|
659
|
+
const mcpUrl = registry.url.replace(/\/$/, "");
|
|
597
660
|
|
|
598
661
|
const response = await callMcpTool(
|
|
599
662
|
mcpUrl,
|
|
@@ -624,9 +687,7 @@ export async function createRegistryConsumer(
|
|
|
624
687
|
registry: ResolvedRegistry,
|
|
625
688
|
request: CallAgentRequest,
|
|
626
689
|
): Promise<unknown> {
|
|
627
|
-
const
|
|
628
|
-
const mcpUrl =
|
|
629
|
-
configuration.call_endpoint ?? registry.url.replace(/\/$/, "");
|
|
690
|
+
const mcpUrl = registry.url.replace(/\/$/, "");
|
|
630
691
|
|
|
631
692
|
const headers: Record<string, string> = {
|
|
632
693
|
"Content-Type": "application/json",
|
|
@@ -840,7 +901,7 @@ export async function createRegistryConsumer(
|
|
|
840
901
|
const data = (await callRegistry(registry, {
|
|
841
902
|
action: "describe_tools",
|
|
842
903
|
path: agentPath,
|
|
843
|
-
tools:
|
|
904
|
+
tools: undefined,
|
|
844
905
|
})) as { tools?: unknown[]; description?: string } | null;
|
|
845
906
|
if (!data) return null;
|
|
846
907
|
return {
|