@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.
Files changed (44) hide show
  1. package/dist/adk-tools.d.ts +1 -1
  2. package/dist/adk-tools.d.ts.map +1 -1
  3. package/dist/adk-tools.js +122 -26
  4. package/dist/adk-tools.js.map +1 -1
  5. package/dist/adk.js +1 -1
  6. package/dist/adk.js.map +1 -1
  7. package/dist/cjs/adk-tools.js +122 -26
  8. package/dist/cjs/adk-tools.js.map +1 -1
  9. package/dist/cjs/config-store.js +10 -1
  10. package/dist/cjs/config-store.js.map +1 -1
  11. package/dist/cjs/define-config.js +9 -2
  12. package/dist/cjs/define-config.js.map +1 -1
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/init.js +1 -1
  15. package/dist/cjs/init.js.map +1 -1
  16. package/dist/cjs/registry-consumer.js +14 -13
  17. package/dist/cjs/registry-consumer.js.map +1 -1
  18. package/dist/config-store.d.ts +3 -3
  19. package/dist/config-store.d.ts.map +1 -1
  20. package/dist/config-store.js +10 -1
  21. package/dist/config-store.js.map +1 -1
  22. package/dist/define-config.d.ts +23 -3
  23. package/dist/define-config.d.ts.map +1 -1
  24. package/dist/define-config.js +9 -2
  25. package/dist/define-config.js.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/init.js +1 -1
  30. package/dist/init.js.map +1 -1
  31. package/dist/registry-consumer.d.ts +41 -20
  32. package/dist/registry-consumer.d.ts.map +1 -1
  33. package/dist/registry-consumer.js +14 -13
  34. package/dist/registry-consumer.js.map +1 -1
  35. package/dist/validate.d.ts +8 -8
  36. package/package.json +1 -1
  37. package/src/adk-tools.ts +177 -36
  38. package/src/adk.ts +1 -1
  39. package/src/config-store.ts +15 -6
  40. package/src/define-config.ts +25 -4
  41. package/src/index.ts +3 -0
  42. package/src/init.ts +1 -1
  43. package/src/ref-naming.test.ts +351 -0
  44. package/src/registry-consumer.ts +68 -44
@@ -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
- /** Agent definition path (resolved from registries) */
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
- /** Local alias for this instance (required for multi-instance) */
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
- /** Normalize a ref entry to its full form */
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
@@ -324,6 +324,9 @@ export type {
324
324
  RegistryConsumer,
325
325
  RegistryConsumerOptions,
326
326
  RegistryConfiguration,
327
+ AgentBase,
328
+ AgentListEntry,
329
+ AgentInspection,
327
330
  AgentListing,
328
331
  SecretResolver,
329
332
  } from "./registry-consumer.js";
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.tools?.length ?? 0;
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
+ });
@@ -209,21 +209,14 @@ export interface RegistryConfiguration {
209
209
  supported_grant_types?: string[];
210
210
  }
211
211
 
212
- /** An agent definition as listed by a registry */
213
- export interface AgentListing {
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
- /** Slim tool summaries (when describe_tools called without full: true) */
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
- /** Integration config if applicable */
246
- integration?: {
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<AgentListing, "publisher" | "tools"> & {
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<AgentListing[]> {
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 with all tools
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
- tools: toolsResult?.tools ?? [],
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): AgentListing[] {
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
- tools: [{
527
- name: "call",
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<AgentListing[]>;
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<AgentListing[]>;
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<AgentListing | null>;
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<AgentListing[]>;
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<AgentListing[]> {
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
- ...agent,
720
- ...agent,
721
- // Normalize tools: strings become { name } objects
722
- tools: agent.tools?.map((t) =>
723
- typeof t === "string" ? { name: t } : t,
724
- ),
725
- publisher: registry.publisher,
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<AgentListing[]> {
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<AgentListing[]> {
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<AgentListing | null> {
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 AgentListing;
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<AgentListing[]> {
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));