@slashfi/agents-sdk 0.73.0 → 0.75.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/dist/adk-tools.d.ts +1 -1
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +122 -26
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +1 -1
- package/dist/adk.js.map +1 -1
- package/dist/cjs/adk-tools.js +122 -26
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/config-store.js +10 -1
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +9 -2
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/init.js +1 -1
- package/dist/cjs/init.js.map +1 -1
- package/dist/cjs/registry-consumer.js +14 -13
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +3 -3
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +10 -1
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +23 -3
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +9 -2
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/registry-consumer.d.ts +41 -20
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +14 -13
- package/dist/registry-consumer.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +177 -36
- package/src/adk.ts +1 -1
- package/src/config-store.ts +15 -6
- package/src/define-config.ts +25 -4
- package/src/index.ts +3 -0
- package/src/init.ts +1 -1
- package/src/ref-naming.test.ts +351 -0
- package/src/registry-consumer.ts +68 -44
package/src/define-config.ts
CHANGED
|
@@ -64,16 +64,30 @@ export type RefConfig = Record<string, unknown>;
|
|
|
64
64
|
|
|
65
65
|
/** A ref entry — describes how to connect to an agent */
|
|
66
66
|
export type RefEntry = {
|
|
67
|
-
/**
|
|
67
|
+
/** Canonical agent path on the remote registry (e.g. `notion`, `linear`). */
|
|
68
68
|
ref: string;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Local identifier for this ref. Used by all operations
|
|
72
|
+
* (call/remove/auth/update/…) to look up the entry. If omitted,
|
|
73
|
+
* the canonical `ref` string is used as the identifier — the
|
|
74
|
+
* common case "one local instance per agent" requires only
|
|
75
|
+
* `{ ref: 'notion', ... }`. Set `name` to a different value only
|
|
76
|
+
* when you need multiple local instances of the same remote
|
|
77
|
+
* agent (e.g. `{ ref: 'notion', name: 'work-notion' }`).
|
|
78
|
+
*/
|
|
79
|
+
name?: string;
|
|
80
|
+
|
|
70
81
|
/** Connection scheme */
|
|
71
82
|
scheme?: 'mcp' | 'https' | 'registry';
|
|
72
83
|
|
|
73
84
|
/** Direct URL to the agent (e.g. https://mcp.notion.com/mcp) */
|
|
74
85
|
url?: string;
|
|
75
86
|
|
|
76
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Use `name` instead. `as` is preserved for reading
|
|
89
|
+
* old consumer-config.json files; new writes emit `name`.
|
|
90
|
+
*/
|
|
77
91
|
as?: string;
|
|
78
92
|
|
|
79
93
|
/** Per-instance config (headers, secrets, etc. — values support {{secret-uri}} templates) */
|
|
@@ -176,11 +190,18 @@ export interface ResolvedConfig {
|
|
|
176
190
|
// Helpers
|
|
177
191
|
// ============================================
|
|
178
192
|
|
|
179
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Normalize a ref entry to its full form.
|
|
195
|
+
*
|
|
196
|
+
* Local identifier resolution order: `entry.name` → `entry.as` (legacy)
|
|
197
|
+
* → `entry.ref` (canonical). This order makes the tool/API surface
|
|
198
|
+
* consistent with the `ref.add({ ref, name })` contract while still
|
|
199
|
+
* reading old `{ ref, as }` entries from pre-0.74 consumer-config.json.
|
|
200
|
+
*/
|
|
180
201
|
export function normalizeRef(entry: RefEntry): ResolvedRef {
|
|
181
202
|
return {
|
|
182
203
|
...entry,
|
|
183
|
-
name: entry.as ?? entry.ref,
|
|
204
|
+
name: entry.name ?? entry.as ?? entry.ref,
|
|
184
205
|
config: entry.config ?? {},
|
|
185
206
|
};
|
|
186
207
|
}
|
package/src/index.ts
CHANGED
package/src/init.ts
CHANGED
|
@@ -185,7 +185,7 @@ export async function runInit(adk: Adk, targets: SkillTarget[]): Promise<void> {
|
|
|
185
185
|
if (agents.length > 0) {
|
|
186
186
|
console.log(`\n${agents.length} agent(s) available on ${DEFAULT_REGISTRY_URL}:\n`);
|
|
187
187
|
for (const a of agents) {
|
|
188
|
-
const toolCount = a.
|
|
188
|
+
const toolCount = a.toolCount ?? 0;
|
|
189
189
|
console.log(` ${a.path} (${toolCount} tools)`);
|
|
190
190
|
if (a.description) console.log(` ${a.description.slice(0, 120)}`);
|
|
191
191
|
console.log();
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `ref` naming contract introduced in 0.74:
|
|
3
|
+
*
|
|
4
|
+
* - `RefEntry` gains an optional `name?` field for the local identifier.
|
|
5
|
+
* The legacy `as?` field still parses for backward compat.
|
|
6
|
+
* - `normalizeRef` resolves the identifier as `name ?? as ?? ref`, so
|
|
7
|
+
* all lookup paths (get, list, update, remove) accept entries written
|
|
8
|
+
* in either shape.
|
|
9
|
+
* - `createRefTool` (the adk-tools.ts MCP tool) drops `as` from its
|
|
10
|
+
* schema and defaults `ref` to `name` on add, so "Add a ref called X"
|
|
11
|
+
* via an LLM that picks either field lands on the same stored
|
|
12
|
+
* `{ ref: 'X' }` entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
|
+
import { createAdkTools } from "./adk-tools";
|
|
17
|
+
import type { FsStore } from "./agent-definitions/config";
|
|
18
|
+
import { createAdk } from "./index";
|
|
19
|
+
import type { ToolContext } from "./types";
|
|
20
|
+
|
|
21
|
+
function createMemoryFs(): FsStore {
|
|
22
|
+
const files = new Map<string, string>();
|
|
23
|
+
return {
|
|
24
|
+
async readFile(path: string) {
|
|
25
|
+
return files.get(path) ?? null;
|
|
26
|
+
},
|
|
27
|
+
async writeFile(path: string, content: string) {
|
|
28
|
+
files.set(path, content);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read a known-present file and parse it as JSON, with typed assertion. */
|
|
34
|
+
async function readJson<T = unknown>(fs: FsStore, path: string): Promise<T> {
|
|
35
|
+
const raw = await fs.readFile(path);
|
|
36
|
+
if (raw === null) {
|
|
37
|
+
throw new Error(`Expected ${path} to exist`);
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(raw) as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── RefEntry: name field on write ───────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("ref.add — identifier field", () => {
|
|
45
|
+
test("single-instance case stores only `ref` (no `name`/`as`)", async () => {
|
|
46
|
+
const fs = createMemoryFs();
|
|
47
|
+
const adk = createAdk(fs);
|
|
48
|
+
|
|
49
|
+
await adk.ref.add({
|
|
50
|
+
ref: "test-ref",
|
|
51
|
+
scheme: "mcp",
|
|
52
|
+
url: "http://localhost:12345",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
56
|
+
fs,
|
|
57
|
+
"consumer-config.json",
|
|
58
|
+
);
|
|
59
|
+
expect(parsed.refs).toHaveLength(1);
|
|
60
|
+
expect(parsed.refs[0].ref).toBe("test-ref");
|
|
61
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
62
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("aliasing case stores both `ref` and `name`", async () => {
|
|
66
|
+
const fs = createMemoryFs();
|
|
67
|
+
const adk = createAdk(fs);
|
|
68
|
+
|
|
69
|
+
await adk.ref.add({
|
|
70
|
+
ref: "notion",
|
|
71
|
+
name: "work-notion",
|
|
72
|
+
scheme: "mcp",
|
|
73
|
+
url: "http://localhost:12345",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
77
|
+
fs,
|
|
78
|
+
"consumer-config.json",
|
|
79
|
+
);
|
|
80
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
81
|
+
expect(parsed.refs[0].name).toBe("work-notion");
|
|
82
|
+
// Legacy `as` field is never emitted on new writes.
|
|
83
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Lookup compatibility: new `name` field works end-to-end ─────────
|
|
88
|
+
|
|
89
|
+
describe("ref lookup — name/as/ref resolution", () => {
|
|
90
|
+
test("entries written with `name` are findable by `name`", async () => {
|
|
91
|
+
const fs = createMemoryFs();
|
|
92
|
+
const adk = createAdk(fs);
|
|
93
|
+
|
|
94
|
+
await adk.ref.add({
|
|
95
|
+
ref: "notion",
|
|
96
|
+
name: "work-notion",
|
|
97
|
+
scheme: "mcp",
|
|
98
|
+
url: "http://localhost:12345",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const entry = await adk.ref.get("work-notion");
|
|
102
|
+
expect(entry).not.toBeNull();
|
|
103
|
+
expect(entry?.ref).toBe("notion");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("entries written with `name` return name field when listed", async () => {
|
|
107
|
+
const fs = createMemoryFs();
|
|
108
|
+
const adk = createAdk(fs);
|
|
109
|
+
|
|
110
|
+
await adk.ref.add({
|
|
111
|
+
ref: "notion",
|
|
112
|
+
name: "work-notion",
|
|
113
|
+
scheme: "mcp",
|
|
114
|
+
url: "http://localhost:12345",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const refs = await adk.ref.list();
|
|
118
|
+
expect(refs).toHaveLength(1);
|
|
119
|
+
expect(refs[0]?.name).toBe("work-notion");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("legacy entries written with `as` remain findable by `as`", async () => {
|
|
123
|
+
// Simulate a consumer-config.json produced by a pre-0.74 client that
|
|
124
|
+
// still writes the `as` field. The read path must still resolve it.
|
|
125
|
+
const fs = createMemoryFs();
|
|
126
|
+
await fs.writeFile(
|
|
127
|
+
"consumer-config.json",
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
refs: [
|
|
130
|
+
{
|
|
131
|
+
ref: "notion",
|
|
132
|
+
as: "work-notion",
|
|
133
|
+
scheme: "mcp",
|
|
134
|
+
url: "http://localhost:12345",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
const adk = createAdk(fs);
|
|
140
|
+
|
|
141
|
+
const entry = await adk.ref.get("work-notion");
|
|
142
|
+
expect(entry).not.toBeNull();
|
|
143
|
+
expect(entry?.ref).toBe("notion");
|
|
144
|
+
expect(entry?.as).toBe("work-notion");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("when both `name` and `as` are present, `name` wins", async () => {
|
|
148
|
+
const fs = createMemoryFs();
|
|
149
|
+
await fs.writeFile(
|
|
150
|
+
"consumer-config.json",
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
refs: [
|
|
153
|
+
{
|
|
154
|
+
ref: "notion",
|
|
155
|
+
name: "new-identifier",
|
|
156
|
+
as: "legacy-identifier",
|
|
157
|
+
scheme: "mcp",
|
|
158
|
+
url: "http://localhost:12345",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
const adk = createAdk(fs);
|
|
164
|
+
|
|
165
|
+
const byNew = await adk.ref.get("new-identifier");
|
|
166
|
+
expect(byNew).not.toBeNull();
|
|
167
|
+
expect(byNew?.ref).toBe("notion");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── ref.update: renaming clears the legacy `as` field ───────────────
|
|
172
|
+
|
|
173
|
+
describe("ref.update — name/as handling", () => {
|
|
174
|
+
test("passing `name` in updates sets name and clears legacy `as`", async () => {
|
|
175
|
+
const fs = createMemoryFs();
|
|
176
|
+
await fs.writeFile(
|
|
177
|
+
"consumer-config.json",
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
refs: [
|
|
180
|
+
{
|
|
181
|
+
ref: "notion",
|
|
182
|
+
as: "old-alias",
|
|
183
|
+
scheme: "mcp",
|
|
184
|
+
url: "http://localhost:12345",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const adk = createAdk(fs);
|
|
190
|
+
|
|
191
|
+
const ok = await adk.ref.update("old-alias", { name: "new-alias" });
|
|
192
|
+
expect(ok).toBe(true);
|
|
193
|
+
|
|
194
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
195
|
+
fs,
|
|
196
|
+
"consumer-config.json",
|
|
197
|
+
);
|
|
198
|
+
expect(parsed.refs[0].name).toBe("new-alias");
|
|
199
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
200
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("passing only `as` updates the legacy field (pre-0.74 callers)", async () => {
|
|
204
|
+
const fs = createMemoryFs();
|
|
205
|
+
await fs.writeFile(
|
|
206
|
+
"consumer-config.json",
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
refs: [
|
|
209
|
+
{
|
|
210
|
+
ref: "notion",
|
|
211
|
+
as: "first",
|
|
212
|
+
scheme: "mcp",
|
|
213
|
+
url: "http://localhost:12345",
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
const adk = createAdk(fs);
|
|
219
|
+
|
|
220
|
+
const ok = await adk.ref.update("first", { as: "second" });
|
|
221
|
+
expect(ok).toBe(true);
|
|
222
|
+
|
|
223
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
224
|
+
fs,
|
|
225
|
+
"consumer-config.json",
|
|
226
|
+
);
|
|
227
|
+
expect(parsed.refs[0].as).toBe("second");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── Tool surface: the LLM non-determinism scenario ──────────────────
|
|
232
|
+
//
|
|
233
|
+
// When an LLM is prompted with "Add a ref called X", its tool-call
|
|
234
|
+
// arguments can land on either `{ ref: 'X', … }` or `{ name: 'X', … }`
|
|
235
|
+
// depending on sampling. Pre-0.74, the former stored `{ ref: 'X' }`
|
|
236
|
+
// and the latter stored `{ ref: undefined }` (broken lookup). The
|
|
237
|
+
// `add` handler now defaults `ref ??= name` so both paths converge on
|
|
238
|
+
// the same stored entry.
|
|
239
|
+
|
|
240
|
+
describe("ref tool — add operation defaults ref to name", () => {
|
|
241
|
+
function makeRefTool(adk: ReturnType<typeof createAdk>) {
|
|
242
|
+
const tools = createAdkTools({ resolveScope: () => adk });
|
|
243
|
+
const ref = tools.find((t) => t.name === "ref");
|
|
244
|
+
if (!ref) throw new Error("ref tool not found");
|
|
245
|
+
return ref;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
test("LLM sends only `name` → ref defaults to name, entry is findable", async () => {
|
|
249
|
+
const fs = createMemoryFs();
|
|
250
|
+
const adk = createAdk(fs);
|
|
251
|
+
const refTool = makeRefTool(adk);
|
|
252
|
+
|
|
253
|
+
const ctx = {} as ToolContext;
|
|
254
|
+
await refTool.execute(
|
|
255
|
+
{
|
|
256
|
+
operation: "add",
|
|
257
|
+
name: "test-identity-ref",
|
|
258
|
+
scheme: "mcp",
|
|
259
|
+
url: "http://127.0.0.1:33469",
|
|
260
|
+
},
|
|
261
|
+
ctx,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
265
|
+
fs,
|
|
266
|
+
"consumer-config.json",
|
|
267
|
+
);
|
|
268
|
+
expect(parsed.refs).toHaveLength(1);
|
|
269
|
+
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
270
|
+
// name was not stored because it equals ref (single-instance case)
|
|
271
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
272
|
+
|
|
273
|
+
const entry = await adk.ref.get("test-identity-ref");
|
|
274
|
+
expect(entry).not.toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("LLM sends only `ref` → same resulting entry", async () => {
|
|
278
|
+
const fs = createMemoryFs();
|
|
279
|
+
const adk = createAdk(fs);
|
|
280
|
+
const refTool = makeRefTool(adk);
|
|
281
|
+
|
|
282
|
+
const ctx = {} as ToolContext;
|
|
283
|
+
await refTool.execute(
|
|
284
|
+
{
|
|
285
|
+
operation: "add",
|
|
286
|
+
ref: "test-identity-ref",
|
|
287
|
+
scheme: "mcp",
|
|
288
|
+
url: "http://127.0.0.1:33469",
|
|
289
|
+
},
|
|
290
|
+
ctx,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
294
|
+
fs,
|
|
295
|
+
"consumer-config.json",
|
|
296
|
+
);
|
|
297
|
+
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
298
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("LLM sends `ref` + different `name` → stored as canonical + alias", async () => {
|
|
302
|
+
const fs = createMemoryFs();
|
|
303
|
+
const adk = createAdk(fs);
|
|
304
|
+
const refTool = makeRefTool(adk);
|
|
305
|
+
|
|
306
|
+
const ctx = {} as ToolContext;
|
|
307
|
+
await refTool.execute(
|
|
308
|
+
{
|
|
309
|
+
operation: "add",
|
|
310
|
+
ref: "notion",
|
|
311
|
+
name: "work-notion",
|
|
312
|
+
scheme: "mcp",
|
|
313
|
+
url: "http://127.0.0.1:33469",
|
|
314
|
+
},
|
|
315
|
+
ctx,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
319
|
+
fs,
|
|
320
|
+
"consumer-config.json",
|
|
321
|
+
);
|
|
322
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
323
|
+
expect(parsed.refs[0].name).toBe("work-notion");
|
|
324
|
+
|
|
325
|
+
// The entry is findable by the local identifier (name), not the
|
|
326
|
+
// canonical ref.
|
|
327
|
+
expect(await adk.ref.get("work-notion")).not.toBeNull();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("LLM omits both `ref` and `name` → loud error (no silent bad write)", async () => {
|
|
331
|
+
const fs = createMemoryFs();
|
|
332
|
+
const adk = createAdk(fs);
|
|
333
|
+
const refTool = makeRefTool(adk);
|
|
334
|
+
|
|
335
|
+
const ctx = {} as ToolContext;
|
|
336
|
+
await expect(
|
|
337
|
+
refTool.execute(
|
|
338
|
+
{
|
|
339
|
+
operation: "add",
|
|
340
|
+
scheme: "mcp",
|
|
341
|
+
url: "http://127.0.0.1:33469",
|
|
342
|
+
},
|
|
343
|
+
ctx,
|
|
344
|
+
),
|
|
345
|
+
).rejects.toThrow(/ref|name/);
|
|
346
|
+
|
|
347
|
+
// Nothing got written.
|
|
348
|
+
const raw = await fs.readFile("consumer-config.json");
|
|
349
|
+
expect(raw).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
package/src/registry-consumer.ts
CHANGED
|
@@ -209,21 +209,14 @@ export interface RegistryConfiguration {
|
|
|
209
209
|
supported_grant_types?: string[];
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
/**
|
|
213
|
-
export interface
|
|
212
|
+
/** Fields common to every agent reference a registry can return. */
|
|
213
|
+
export interface AgentBase {
|
|
214
214
|
/** Agent path (e.g., '@notion') */
|
|
215
215
|
path: string;
|
|
216
216
|
/** Description */
|
|
217
217
|
description?: string;
|
|
218
218
|
/** Publisher (registry name) */
|
|
219
219
|
publisher: string;
|
|
220
|
-
/** Tools available */
|
|
221
|
-
tools?: Array<{
|
|
222
|
-
name: string;
|
|
223
|
-
description?: string;
|
|
224
|
-
}>;
|
|
225
|
-
/** Whether it requires auth */
|
|
226
|
-
requiresAuth?: boolean;
|
|
227
220
|
/** Security scheme summary (machine-readable auth type) */
|
|
228
221
|
security?: SecuritySchemeSummary;
|
|
229
222
|
/** Available resources (e.g., AUTH.md) */
|
|
@@ -232,7 +225,39 @@ export interface AgentListing {
|
|
|
232
225
|
name?: string;
|
|
233
226
|
mimeType?: string;
|
|
234
227
|
}>;
|
|
235
|
-
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Lightweight agent entry returned by `list_agents` / `consumer.list()` /
|
|
232
|
+
* `consumer.browse()` / `consumer.available()`. Registries emit a `toolCount`
|
|
233
|
+
* instead of the full `tools` array to keep listings small; call `inspect()`
|
|
234
|
+
* to get the per-tool details.
|
|
235
|
+
*/
|
|
236
|
+
export interface AgentListEntry extends AgentBase {
|
|
237
|
+
/** Number of tools this agent exposes */
|
|
238
|
+
toolCount?: number;
|
|
239
|
+
/** Whether the agent requires auth (populated only for direct MCP/HTTPS refs) */
|
|
240
|
+
requiresAuth?: boolean;
|
|
241
|
+
/** Integration config if applicable */
|
|
242
|
+
integration?: {
|
|
243
|
+
provider: string;
|
|
244
|
+
displayName: string;
|
|
245
|
+
category?: string;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Full agent detail returned by `describe_tools` / `consumer.inspect()`.
|
|
251
|
+
* Carries the actual tool definitions (or slim summaries) and extra context
|
|
252
|
+
* the listing endpoint omits.
|
|
253
|
+
*/
|
|
254
|
+
export interface AgentInspection extends AgentBase {
|
|
255
|
+
/** Full tool details (returned when `full: true`) */
|
|
256
|
+
tools?: Array<{
|
|
257
|
+
name: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
}>;
|
|
260
|
+
/** Slim tool summaries (returned when `full` is not set) */
|
|
236
261
|
toolSummaries?: Array<{
|
|
237
262
|
name: string;
|
|
238
263
|
description: string;
|
|
@@ -242,16 +267,16 @@ export interface AgentListing {
|
|
|
242
267
|
context?: string;
|
|
243
268
|
/** Upstream MCP/API URL for direct connections */
|
|
244
269
|
upstream?: string;
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
provider: string;
|
|
248
|
-
displayName: string;
|
|
249
|
-
category?: string;
|
|
250
|
-
};
|
|
270
|
+
/** Agent mode (redirect | proxy | api) */
|
|
271
|
+
mode?: string;
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
/** @deprecated Prefer `AgentListEntry` (for listings) or `AgentInspection` (for inspect results). */
|
|
275
|
+
export type AgentListing = AgentListEntry | AgentInspection;
|
|
276
|
+
|
|
253
277
|
/** Raw agent entry returned by the list_agents MCP tool (before normalization). */
|
|
254
|
-
type ListAgentsEntry = Omit<
|
|
278
|
+
type ListAgentsEntry = Omit<AgentListEntry, "publisher"> & {
|
|
279
|
+
/** Legacy field — older registries emitted `tools` instead of `toolCount`. */
|
|
255
280
|
tools?: Array<{ name: string; description?: string } | string>;
|
|
256
281
|
};
|
|
257
282
|
|
|
@@ -333,7 +358,7 @@ async function listFromMcpServer(
|
|
|
333
358
|
url: string,
|
|
334
359
|
auth: { token?: string; headers?: Record<string, string> },
|
|
335
360
|
fetchFn: FetchFn,
|
|
336
|
-
): Promise<
|
|
361
|
+
): Promise<AgentListEntry[]> {
|
|
337
362
|
const serverUrl = url.replace(/\/$/, "");
|
|
338
363
|
|
|
339
364
|
const headers: Record<string, string> = {
|
|
@@ -383,12 +408,13 @@ async function listFromMcpServer(
|
|
|
383
408
|
|
|
384
409
|
const serverName = initResult?.serverInfo?.name ?? new URL(serverUrl).hostname;
|
|
385
410
|
|
|
386
|
-
// Return as a single agent listing
|
|
411
|
+
// Return as a single agent listing — toolCount keeps listings lightweight;
|
|
412
|
+
// callers that need per-tool detail should use `inspect()`.
|
|
387
413
|
return [{
|
|
388
414
|
path: serverName,
|
|
389
415
|
description: `MCP server at ${serverUrl}`,
|
|
390
416
|
publisher: serverName,
|
|
391
|
-
|
|
417
|
+
toolCount: toolsResult?.tools?.length ?? 0,
|
|
392
418
|
requiresAuth: false,
|
|
393
419
|
}];
|
|
394
420
|
}
|
|
@@ -517,16 +543,14 @@ async function callMcpTool(
|
|
|
517
543
|
* Returns a single generic 'call' tool since we can't auto-discover REST endpoints
|
|
518
544
|
* without an OpenAPI spec.
|
|
519
545
|
*/
|
|
520
|
-
function listFromHttpsApi(url: string):
|
|
546
|
+
function listFromHttpsApi(url: string): AgentListEntry[] {
|
|
521
547
|
const hostname = new URL(url).hostname;
|
|
522
548
|
return [{
|
|
523
549
|
path: hostname,
|
|
524
550
|
description: `REST API at ${url}`,
|
|
525
551
|
publisher: hostname,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
description: "Make an HTTP request to the API. Params: method, path, body, headers.",
|
|
529
|
-
}],
|
|
552
|
+
// Single generic `call` tool — callers can use `inspect()` for details.
|
|
553
|
+
toolCount: 1,
|
|
530
554
|
requiresAuth: false,
|
|
531
555
|
}];
|
|
532
556
|
}
|
|
@@ -598,7 +622,7 @@ export interface RegistryConsumerOptions {
|
|
|
598
622
|
|
|
599
623
|
export interface RegistryConsumer {
|
|
600
624
|
/** List all available agents across all connected registries */
|
|
601
|
-
list(): Promise<
|
|
625
|
+
list(): Promise<AgentListEntry[]>;
|
|
602
626
|
|
|
603
627
|
/** List configured refs (from the consumer's config) */
|
|
604
628
|
refs(): ResolvedRef[];
|
|
@@ -617,14 +641,14 @@ export interface RegistryConsumer {
|
|
|
617
641
|
discover(registryUrl: string): Promise<RegistryConfiguration>;
|
|
618
642
|
|
|
619
643
|
/** Browse agents from a specific registry (or all if url omitted), with optional BM25 search */
|
|
620
|
-
browse(registryUrl?: string, query?: string): Promise<
|
|
644
|
+
browse(registryUrl?: string, query?: string): Promise<AgentListEntry[]>;
|
|
621
645
|
|
|
622
646
|
/** Inspect a specific agent — returns tools, auth requirements, resources */
|
|
623
647
|
inspect(
|
|
624
648
|
agentPath: string,
|
|
625
649
|
registryUrl?: string,
|
|
626
650
|
options?: { full?: boolean },
|
|
627
|
-
): Promise<
|
|
651
|
+
): Promise<AgentInspection | null>;
|
|
628
652
|
|
|
629
653
|
/** Resolve a secret URL to its value */
|
|
630
654
|
resolveSecret(url: string): Promise<string>;
|
|
@@ -644,7 +668,7 @@ export interface RegistryConsumer {
|
|
|
644
668
|
index(): ResolvedConfig;
|
|
645
669
|
|
|
646
670
|
/** Diff: what's available vs what's configured */
|
|
647
|
-
available(): Promise<
|
|
671
|
+
available(): Promise<AgentListEntry[]>;
|
|
648
672
|
}
|
|
649
673
|
|
|
650
674
|
/**
|
|
@@ -693,7 +717,7 @@ export async function createRegistryConsumer(
|
|
|
693
717
|
async function listFromRegistry(
|
|
694
718
|
registry: ResolvedRegistry,
|
|
695
719
|
query?: string,
|
|
696
|
-
): Promise<
|
|
720
|
+
): Promise<AgentListEntry[]> {
|
|
697
721
|
const mcpUrl = registry.url.replace(/\/$/, "");
|
|
698
722
|
|
|
699
723
|
const response = await callMcpTool(
|
|
@@ -715,15 +739,15 @@ export async function createRegistryConsumer(
|
|
|
715
739
|
) as ListAgentsResponse;
|
|
716
740
|
const agents = data.agents ?? [];
|
|
717
741
|
|
|
718
|
-
return agents.map((agent) =>
|
|
719
|
-
|
|
720
|
-
...agent
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
})
|
|
742
|
+
return agents.map((agent) => {
|
|
743
|
+
// Legacy registries may still emit `tools` instead of `toolCount`; back-fill.
|
|
744
|
+
const { tools, toolCount, ...rest } = agent;
|
|
745
|
+
return {
|
|
746
|
+
...rest,
|
|
747
|
+
publisher: registry.publisher,
|
|
748
|
+
toolCount: toolCount ?? tools?.length,
|
|
749
|
+
};
|
|
750
|
+
});
|
|
727
751
|
}
|
|
728
752
|
|
|
729
753
|
// Send any call_agent request through a registry's MCP endpoint
|
|
@@ -821,7 +845,7 @@ export async function createRegistryConsumer(
|
|
|
821
845
|
|
|
822
846
|
// Build the consumer
|
|
823
847
|
const consumer: RegistryConsumer = {
|
|
824
|
-
async list(): Promise<
|
|
848
|
+
async list(): Promise<AgentListEntry[]> {
|
|
825
849
|
// Collect from standard registries
|
|
826
850
|
const registryResults = await Promise.allSettled(
|
|
827
851
|
resolvedRegistries.map((r) => listFromRegistry(r)),
|
|
@@ -925,7 +949,7 @@ export async function createRegistryConsumer(
|
|
|
925
949
|
return discover(registryUrl, registry);
|
|
926
950
|
},
|
|
927
951
|
|
|
928
|
-
async browse(registryUrl?: string, query?: string): Promise<
|
|
952
|
+
async browse(registryUrl?: string, query?: string): Promise<AgentListEntry[]> {
|
|
929
953
|
// List agents from a specific registry, or all registries if not specified
|
|
930
954
|
const targets = registryUrl
|
|
931
955
|
? resolvedRegistries.filter(
|
|
@@ -945,7 +969,7 @@ export async function createRegistryConsumer(
|
|
|
945
969
|
agentPath: string,
|
|
946
970
|
registryUrl?: string,
|
|
947
971
|
options?: { full?: boolean },
|
|
948
|
-
): Promise<
|
|
972
|
+
): Promise<AgentInspection | null> {
|
|
949
973
|
const targetRegistries = registryUrl
|
|
950
974
|
? resolvedRegistries.filter((r) => r.url === registryUrl || r.name === registryUrl)
|
|
951
975
|
: resolvedRegistries;
|
|
@@ -988,7 +1012,7 @@ export async function createRegistryConsumer(
|
|
|
988
1012
|
context: data.context,
|
|
989
1013
|
...(data.upstream && { upstream: data.upstream }),
|
|
990
1014
|
...(data.mode && { mode: data.mode }),
|
|
991
|
-
} as
|
|
1015
|
+
} as AgentInspection;
|
|
992
1016
|
}),
|
|
993
1017
|
);
|
|
994
1018
|
|
|
@@ -1018,7 +1042,7 @@ export async function createRegistryConsumer(
|
|
|
1018
1042
|
};
|
|
1019
1043
|
},
|
|
1020
1044
|
|
|
1021
|
-
async available(): Promise<
|
|
1045
|
+
async available(): Promise<AgentListEntry[]> {
|
|
1022
1046
|
const all = await consumer.list();
|
|
1023
1047
|
const configuredRefs = new Set(resolvedRefs.map((r) => r.ref));
|
|
1024
1048
|
return all.filter((a) => !configuredRefs.has(a.path));
|