@slashfi/agents-sdk 0.33.1 → 0.34.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/call-agent-schema.d.ts +327 -13
- package/dist/call-agent-schema.d.ts.map +1 -1
- package/dist/call-agent-schema.js +68 -7
- package/dist/call-agent-schema.js.map +1 -1
- package/dist/cjs/call-agent-schema.js +72 -8
- package/dist/cjs/call-agent-schema.js.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry-consumer.js +2 -1
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +22 -6
- package/dist/cjs/server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +2 -1
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +23 -7
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/call-agent-schema.test.ts +306 -0
- package/src/call-agent-schema.ts +92 -17
- package/src/index.ts +6 -0
- package/src/registry-consumer.ts +10 -3
- package/src/server.ts +45 -7
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
stripNulls,
|
|
5
|
+
nullTolerant,
|
|
6
|
+
zodToOpenAiJsonSchema,
|
|
7
|
+
callAgentValidationSchema,
|
|
8
|
+
callAgentRequestSchema,
|
|
9
|
+
listAgentsValidationSchema,
|
|
10
|
+
} from "./call-agent-schema";
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// stripNulls
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("stripNulls", () => {
|
|
17
|
+
test("converts null to undefined", () => {
|
|
18
|
+
expect(stripNulls(null)).toBe(undefined);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("passes through primitives", () => {
|
|
22
|
+
expect(stripNulls("hello")).toBe("hello");
|
|
23
|
+
expect(stripNulls(42)).toBe(42);
|
|
24
|
+
expect(stripNulls(true)).toBe(true);
|
|
25
|
+
expect(stripNulls(undefined)).toBe(undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("strips null from flat objects", () => {
|
|
29
|
+
expect(stripNulls({ a: null, b: "hello" })).toEqual({ b: "hello" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("strips null from nested objects", () => {
|
|
33
|
+
expect(stripNulls({ a: { b: null, c: "ok" }, d: null })).toEqual({
|
|
34
|
+
a: { c: "ok" },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("strips null from arrays", () => {
|
|
39
|
+
expect(stripNulls([1, null, 3])).toEqual([1, undefined, 3]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("handles deeply nested structures", () => {
|
|
43
|
+
const input = {
|
|
44
|
+
action: "execute_tool",
|
|
45
|
+
path: "/agents/@test",
|
|
46
|
+
tool: "my_tool",
|
|
47
|
+
params: null,
|
|
48
|
+
callerId: null,
|
|
49
|
+
callerType: null,
|
|
50
|
+
metadata: { nested: null, keep: "this" },
|
|
51
|
+
};
|
|
52
|
+
const result = stripNulls(input) as Record<string, unknown>;
|
|
53
|
+
expect(result.action).toBe("execute_tool");
|
|
54
|
+
expect(result.path).toBe("/agents/@test");
|
|
55
|
+
expect(result.params).toBe(undefined);
|
|
56
|
+
expect(result.callerId).toBe(undefined);
|
|
57
|
+
expect(result.callerType).toBe(undefined);
|
|
58
|
+
expect(result.metadata).toEqual({ keep: "this" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("handles empty objects and arrays", () => {
|
|
62
|
+
expect(stripNulls({})).toEqual({});
|
|
63
|
+
expect(stripNulls([])).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// nullTolerant
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe("nullTolerant", () => {
|
|
72
|
+
const schema = z.object({
|
|
73
|
+
name: z.string().optional(),
|
|
74
|
+
count: z.number().optional(),
|
|
75
|
+
tags: z.array(z.string()).optional(),
|
|
76
|
+
nested: z
|
|
77
|
+
.object({
|
|
78
|
+
value: z.string().optional(),
|
|
79
|
+
})
|
|
80
|
+
.optional(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("original schema rejects null", () => {
|
|
84
|
+
expect(() => schema.parse({ name: null })).toThrow();
|
|
85
|
+
expect(() => schema.parse({ tags: null })).toThrow();
|
|
86
|
+
expect(() => schema.parse({ count: null })).toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("tolerant schema accepts null for optional string", () => {
|
|
90
|
+
const tolerant = nullTolerant(schema);
|
|
91
|
+
const result = tolerant.parse({ name: null });
|
|
92
|
+
expect(result).toEqual({});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("tolerant schema accepts null for optional array", () => {
|
|
96
|
+
const tolerant = nullTolerant(schema);
|
|
97
|
+
const result = tolerant.parse({ tags: null });
|
|
98
|
+
expect(result).toEqual({});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("tolerant schema accepts null for optional number", () => {
|
|
102
|
+
const tolerant = nullTolerant(schema);
|
|
103
|
+
const result = tolerant.parse({ count: null });
|
|
104
|
+
expect(result).toEqual({});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("tolerant schema accepts null for nested optional", () => {
|
|
108
|
+
const tolerant = nullTolerant(schema);
|
|
109
|
+
const result = tolerant.parse({ nested: { value: null } });
|
|
110
|
+
expect(result).toEqual({ nested: {} });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("tolerant schema still validates real values", () => {
|
|
114
|
+
const tolerant = nullTolerant(schema);
|
|
115
|
+
const result = tolerant.parse({
|
|
116
|
+
name: "test",
|
|
117
|
+
count: 5,
|
|
118
|
+
tags: ["a", "b"],
|
|
119
|
+
});
|
|
120
|
+
expect(result).toEqual({ name: "test", count: 5, tags: ["a", "b"] });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("tolerant schema still rejects invalid types", () => {
|
|
124
|
+
const tolerant = nullTolerant(schema);
|
|
125
|
+
expect(() => tolerant.parse({ name: 123 })).toThrow();
|
|
126
|
+
expect(() => tolerant.parse({ count: "not a number" })).toThrow();
|
|
127
|
+
expect(() => tolerant.parse({ tags: "not an array" })).toThrow();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("all nulls at once", () => {
|
|
131
|
+
const tolerant = nullTolerant(schema);
|
|
132
|
+
const result = tolerant.parse({
|
|
133
|
+
name: null,
|
|
134
|
+
count: null,
|
|
135
|
+
tags: null,
|
|
136
|
+
nested: null,
|
|
137
|
+
});
|
|
138
|
+
expect(result).toEqual({});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
// callAgentValidationSchema — real-world scenarios
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe("callAgentValidationSchema", () => {
|
|
147
|
+
test("accepts execute_tool with null optional fields (the original bug)", () => {
|
|
148
|
+
const input = {
|
|
149
|
+
request: {
|
|
150
|
+
action: "execute_tool",
|
|
151
|
+
path: "/agents/@librarian",
|
|
152
|
+
tool: "search_skill",
|
|
153
|
+
params: { query: "agent registry" },
|
|
154
|
+
callerId: null,
|
|
155
|
+
callerType: null,
|
|
156
|
+
metadata: null,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
160
|
+
expect(result.success).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("accepts describe_tools with tools: null (the original bug)", () => {
|
|
164
|
+
const input = {
|
|
165
|
+
request: {
|
|
166
|
+
action: "describe_tools",
|
|
167
|
+
path: "/agents/@librarian",
|
|
168
|
+
tools: null,
|
|
169
|
+
callerId: null,
|
|
170
|
+
callerType: null,
|
|
171
|
+
metadata: null,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
175
|
+
expect(result.success).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("accepts invoke with null sessionId and branchAttributes", () => {
|
|
179
|
+
const input = {
|
|
180
|
+
request: {
|
|
181
|
+
action: "invoke",
|
|
182
|
+
path: "/agents/@compactor",
|
|
183
|
+
prompt: "Hello",
|
|
184
|
+
sessionId: null,
|
|
185
|
+
branchAttributes: null,
|
|
186
|
+
callerId: null,
|
|
187
|
+
callerType: null,
|
|
188
|
+
metadata: null,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("accepts ask with null optional fields", () => {
|
|
196
|
+
const input = {
|
|
197
|
+
request: {
|
|
198
|
+
action: "ask",
|
|
199
|
+
path: "/agents/@worker",
|
|
200
|
+
prompt: "What time is it?",
|
|
201
|
+
sessionId: null,
|
|
202
|
+
branchAttributes: null,
|
|
203
|
+
callerId: null,
|
|
204
|
+
callerType: null,
|
|
205
|
+
metadata: null,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
209
|
+
expect(result.success).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("accepts list_resources with null optional fields", () => {
|
|
213
|
+
const input = {
|
|
214
|
+
request: {
|
|
215
|
+
action: "list_resources",
|
|
216
|
+
path: "/agents/@agent-fs",
|
|
217
|
+
callerId: null,
|
|
218
|
+
callerType: null,
|
|
219
|
+
metadata: null,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
223
|
+
expect(result.success).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("accepts read_resources with null optional fields", () => {
|
|
227
|
+
const input = {
|
|
228
|
+
request: {
|
|
229
|
+
action: "read_resources",
|
|
230
|
+
path: "/agents/@agent-fs",
|
|
231
|
+
uris: ["AUTH.md"],
|
|
232
|
+
callerId: null,
|
|
233
|
+
callerType: null,
|
|
234
|
+
metadata: null,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const result = callAgentValidationSchema.safeParse(input);
|
|
238
|
+
expect(result.success).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("still rejects truly invalid input", () => {
|
|
242
|
+
const result = callAgentValidationSchema.safeParse({
|
|
243
|
+
request: {
|
|
244
|
+
action: "execute_tool",
|
|
245
|
+
// missing required 'path' and 'tool'
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
expect(result.success).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("still rejects unknown action", () => {
|
|
252
|
+
const result = callAgentValidationSchema.safeParse({
|
|
253
|
+
request: {
|
|
254
|
+
action: "unknown_action",
|
|
255
|
+
path: "/agents/@test",
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
expect(result.success).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
// listAgentsValidationSchema
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
describe("listAgentsValidationSchema", () => {
|
|
267
|
+
test("accepts all-null optional fields", () => {
|
|
268
|
+
const result = listAgentsValidationSchema.parse({
|
|
269
|
+
query: null,
|
|
270
|
+
limit: null,
|
|
271
|
+
cursor: null,
|
|
272
|
+
});
|
|
273
|
+
expect(result).toEqual({});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("accepts mix of null and real values", () => {
|
|
277
|
+
const result = listAgentsValidationSchema.parse({
|
|
278
|
+
query: "notion",
|
|
279
|
+
limit: null,
|
|
280
|
+
cursor: null,
|
|
281
|
+
});
|
|
282
|
+
expect(result).toEqual({ query: "notion" });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
// zodToOpenAiJsonSchema
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe("zodToOpenAiJsonSchema", () => {
|
|
291
|
+
test("produces JSON schema with nullable optional fields", () => {
|
|
292
|
+
const schema = z.object({
|
|
293
|
+
required: z.string(),
|
|
294
|
+
optional: z.string().optional(),
|
|
295
|
+
});
|
|
296
|
+
const jsonSchema = zodToOpenAiJsonSchema(schema) as any;
|
|
297
|
+
|
|
298
|
+
// Required field should be in required array
|
|
299
|
+
expect(jsonSchema.required).toContain("required");
|
|
300
|
+
// Optional field should also be required (openAi convention)
|
|
301
|
+
expect(jsonSchema.required).toContain("optional");
|
|
302
|
+
// Optional field should allow null
|
|
303
|
+
const optProp = jsonSchema.properties.optional;
|
|
304
|
+
expect(optProp.anyOf || optProp.type).toBeTruthy();
|
|
305
|
+
});
|
|
306
|
+
});
|
package/src/call-agent-schema.ts
CHANGED
|
@@ -12,7 +12,69 @@
|
|
|
12
12
|
* Response types live in types.ts (they're output shapes, not validated input).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { z } from "zod";
|
|
15
|
+
import { z, type ZodTypeAny } from "zod";
|
|
16
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// OpenAI null-tolerance transform
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recursively strip null values from an object, converting them to undefined.
|
|
24
|
+
* This is the inverse of zod-to-json-schema's openAi target behavior, which
|
|
25
|
+
* converts .optional() fields to nullable+required in JSON Schema.
|
|
26
|
+
*
|
|
27
|
+
* Used as a z.preprocess() step so that Zod's .optional() (which accepts
|
|
28
|
+
* undefined but not null) works correctly with LLM outputs that send null
|
|
29
|
+
* for "no value" per the OpenAI function calling convention.
|
|
30
|
+
*/
|
|
31
|
+
export function stripNulls(obj: unknown): unknown {
|
|
32
|
+
if (obj === null) return undefined;
|
|
33
|
+
if (Array.isArray(obj)) return obj.map(stripNulls);
|
|
34
|
+
if (typeof obj === "object") {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [
|
|
37
|
+
k,
|
|
38
|
+
stripNulls(v),
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return obj;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a Zod schema with a preprocess step that converts null → undefined.
|
|
47
|
+
* This makes the schema "null-tolerant" — matching what the OpenAI JSON Schema
|
|
48
|
+
* target promises to LLMs (nullable fields) while keeping Zod's .optional()
|
|
49
|
+
* semantics internally.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const schema = z.object({ name: z.string().optional() });
|
|
54
|
+
* const tolerant = nullTolerant(schema);
|
|
55
|
+
* tolerant.parse({ name: null }); // { name: undefined } — no error
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function nullTolerant<T extends ZodTypeAny>(schema: T) {
|
|
59
|
+
return z.preprocess(stripNulls, schema) as unknown as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert a Zod schema to JSON Schema using the OpenAI target,
|
|
64
|
+
* which makes all optional fields nullable+required.
|
|
65
|
+
*
|
|
66
|
+
* This is the standard way to generate input schemas for MCP tools
|
|
67
|
+
* that will be called by LLMs.
|
|
68
|
+
*/
|
|
69
|
+
export function zodToOpenAiJsonSchema(
|
|
70
|
+
schema: ZodTypeAny,
|
|
71
|
+
): Record<string, unknown> {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
return zodToJsonSchema(schema as any, { target: "openAi" }) as Record<
|
|
74
|
+
string,
|
|
75
|
+
unknown
|
|
76
|
+
>;
|
|
77
|
+
}
|
|
16
78
|
|
|
17
79
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
80
|
// Base schemas
|
|
@@ -85,12 +147,20 @@ export const loadActionSchema = callAgentBaseSchema.extend({
|
|
|
85
147
|
|
|
86
148
|
/** List resources: discover available resources on an agent */
|
|
87
149
|
export const listResourcesActionSchema = callAgentBaseSchema.extend({
|
|
88
|
-
action: z
|
|
150
|
+
action: z
|
|
151
|
+
.literal("list_resources")
|
|
152
|
+
.describe(
|
|
153
|
+
"List all resources available on an agent — docs, auth instructions, config schemas, etc.",
|
|
154
|
+
),
|
|
89
155
|
});
|
|
90
156
|
|
|
91
157
|
/** Read resources: fetch one or more resources by URI */
|
|
92
158
|
export const readResourcesActionSchema = callAgentBaseSchema.extend({
|
|
93
|
-
action: z
|
|
159
|
+
action: z
|
|
160
|
+
.literal("read_resources")
|
|
161
|
+
.describe(
|
|
162
|
+
"Fetch one or more resources by URI. Use list_resources first to discover available URIs.",
|
|
163
|
+
),
|
|
94
164
|
uris: z
|
|
95
165
|
.array(z.string())
|
|
96
166
|
.describe("Resource URIs to read (e.g., ['AUTH.md'])"),
|
|
@@ -140,15 +210,13 @@ export type CallerType = z.infer<typeof callerTypeSchema>;
|
|
|
140
210
|
/** All supported action strings as a const array. */
|
|
141
211
|
export const CALL_AGENT_ACTIONS: AgentAction[] =
|
|
142
212
|
callAgentRequestSchema.options.map(
|
|
143
|
-
(s) => (s.shape as { action: z.ZodLiteral<AgentAction> }).action.value
|
|
213
|
+
(s) => (s.shape as { action: z.ZodLiteral<AgentAction> }).action.value,
|
|
144
214
|
);
|
|
145
215
|
|
|
146
216
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
217
|
// JSON Schema for MCP (derived from zod)
|
|
148
218
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
219
|
|
|
150
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
151
|
-
|
|
152
220
|
/**
|
|
153
221
|
* Zod schema for the full MCP tool input (wraps request in an outer object).
|
|
154
222
|
* This is the schema that gets converted to JSON Schema for the LLM.
|
|
@@ -163,10 +231,17 @@ export const callAgentToolInputSchema = z.object({
|
|
|
163
231
|
*
|
|
164
232
|
* Fully derived from the zod schemas — no hand-written JSON Schema.
|
|
165
233
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
234
|
+
export const callAgentInputSchema = zodToOpenAiJsonSchema(
|
|
235
|
+
callAgentToolInputSchema,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Null-tolerant validation schema for `call_agent`.
|
|
240
|
+
* Accepts null values where the JSON Schema promises nullable,
|
|
241
|
+
* converting them to undefined before Zod validation.
|
|
242
|
+
*/
|
|
243
|
+
export const callAgentValidationSchema = nullTolerant(
|
|
244
|
+
callAgentToolInputSchema,
|
|
170
245
|
);
|
|
171
246
|
|
|
172
247
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -183,9 +258,7 @@ export const listAgentsToolInputSchema = z.object({
|
|
|
183
258
|
limit: z
|
|
184
259
|
.number()
|
|
185
260
|
.optional()
|
|
186
|
-
.describe(
|
|
187
|
-
"Maximum number of results per page (default: 20)",
|
|
188
|
-
),
|
|
261
|
+
.describe("Maximum number of results per page (default: 20)"),
|
|
189
262
|
cursor: z
|
|
190
263
|
.string()
|
|
191
264
|
.optional()
|
|
@@ -196,8 +269,10 @@ export const listAgentsToolInputSchema = z.object({
|
|
|
196
269
|
|
|
197
270
|
export type ListAgentsInput = z.infer<typeof listAgentsToolInputSchema>;
|
|
198
271
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
272
|
+
export const listAgentsInputSchema = zodToOpenAiJsonSchema(
|
|
273
|
+
listAgentsToolInputSchema,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
export const listAgentsValidationSchema = nullTolerant(
|
|
277
|
+
listAgentsToolInputSchema,
|
|
203
278
|
);
|
package/src/index.ts
CHANGED
|
@@ -384,6 +384,8 @@ export type { IntrospectOptions } from "./introspect.js";
|
|
|
384
384
|
export {
|
|
385
385
|
callAgentInputSchema,
|
|
386
386
|
callAgentRequestSchema,
|
|
387
|
+
callAgentValidationSchema,
|
|
388
|
+
callAgentToolInputSchema,
|
|
387
389
|
invokeActionSchema,
|
|
388
390
|
askActionSchema,
|
|
389
391
|
executeToolActionSchema,
|
|
@@ -393,6 +395,10 @@ export {
|
|
|
393
395
|
readResourcesActionSchema,
|
|
394
396
|
callerTypeSchema,
|
|
395
397
|
CALL_AGENT_ACTIONS,
|
|
398
|
+
nullTolerant,
|
|
399
|
+
stripNulls,
|
|
400
|
+
zodToOpenAiJsonSchema,
|
|
401
|
+
listAgentsValidationSchema,
|
|
396
402
|
} from "./call-agent-schema.js";
|
|
397
403
|
|
|
398
404
|
// ============================================
|
package/src/registry-consumer.ts
CHANGED
|
@@ -229,8 +229,13 @@ type ListAgentsEntry = Omit<AgentListing, "publisher" | "tools"> & {
|
|
|
229
229
|
tools?: Array<{ name: string; description?: string } | string>;
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
-
/** Response shape from list_agents —
|
|
233
|
-
type ListAgentsResponse =
|
|
232
|
+
/** Response shape from list_agents — wrapped object with agents array. */
|
|
233
|
+
type ListAgentsResponse = {
|
|
234
|
+
agents: ListAgentsEntry[];
|
|
235
|
+
total?: number;
|
|
236
|
+
nextCursor?: string;
|
|
237
|
+
success?: boolean;
|
|
238
|
+
};
|
|
234
239
|
|
|
235
240
|
// ============================================
|
|
236
241
|
// Secret Resolver
|
|
@@ -590,7 +595,7 @@ export async function createRegistryConsumer(
|
|
|
590
595
|
const mcpUrl =
|
|
591
596
|
configuration.call_endpoint ?? registry.url.replace(/\/$/, "");
|
|
592
597
|
|
|
593
|
-
const
|
|
598
|
+
const response = await callMcpTool(
|
|
594
599
|
mcpUrl,
|
|
595
600
|
"list_agents",
|
|
596
601
|
query ? { query } : {},
|
|
@@ -601,6 +606,8 @@ export async function createRegistryConsumer(
|
|
|
601
606
|
fetchFn,
|
|
602
607
|
) as ListAgentsResponse;
|
|
603
608
|
|
|
609
|
+
const agents = response.agents ?? [];
|
|
610
|
+
|
|
604
611
|
return agents.map((agent) => ({
|
|
605
612
|
...agent,
|
|
606
613
|
...agent,
|
package/src/server.ts
CHANGED
|
@@ -48,8 +48,11 @@ import type { AgentDefinition, CallAgentRequest } from "./types.js";
|
|
|
48
48
|
|
|
49
49
|
import {
|
|
50
50
|
callAgentInputSchema,
|
|
51
|
+
callAgentValidationSchema,
|
|
51
52
|
listAgentsInputSchema,
|
|
52
|
-
|
|
53
|
+
listAgentsValidationSchema,
|
|
54
|
+
nullTolerant,
|
|
55
|
+
zodToOpenAiJsonSchema,
|
|
53
56
|
} from "./call-agent-schema.js";
|
|
54
57
|
|
|
55
58
|
// ============================================
|
|
@@ -146,6 +149,24 @@ export interface AgentServerOptions {
|
|
|
146
149
|
* ```
|
|
147
150
|
*/
|
|
148
151
|
resolveAuth?: (req: Request) => Promise<ResolvedAuth | null>;
|
|
152
|
+
/**
|
|
153
|
+
* Schema overrides for built-in MCP tools.
|
|
154
|
+
* When provided, these replace the default schemas for both
|
|
155
|
+
* JSON Schema generation (what LLMs see) and runtime validation.
|
|
156
|
+
*
|
|
157
|
+
* Schemas must be Zod schemas that are supersets of the defaults
|
|
158
|
+
* (e.g., extending the base action schemas with additional fields).
|
|
159
|
+
*
|
|
160
|
+
* The server automatically:
|
|
161
|
+
* - Converts to JSON Schema (openAi target) for tools/list
|
|
162
|
+
* - Wraps with nullTolerant() for validation
|
|
163
|
+
*/
|
|
164
|
+
schemas?: {
|
|
165
|
+
/** Override call_agent tool input schema (wraps callAgentRequestSchema) */
|
|
166
|
+
callAgent?: import("zod").ZodTypeAny;
|
|
167
|
+
/** Override list_agents tool input schema */
|
|
168
|
+
listAgents?: import("zod").ZodTypeAny;
|
|
169
|
+
};
|
|
149
170
|
/**
|
|
150
171
|
* Registry capabilities — advertised in MCP initialize response.
|
|
151
172
|
* When set, this server identifies as an agent registry (superset of MCP).
|
|
@@ -437,19 +458,23 @@ function resolveAgent(
|
|
|
437
458
|
// MCP Tool Definitions
|
|
438
459
|
// ============================================
|
|
439
460
|
|
|
440
|
-
function getToolDefinitions() {
|
|
461
|
+
function getToolDefinitions(schemas?: AgentServerOptions["schemas"]) {
|
|
441
462
|
return [
|
|
442
463
|
{
|
|
443
464
|
name: "call_agent",
|
|
444
465
|
description:
|
|
445
466
|
"Execute a tool on a registered agent. Provide the agent path and tool name.\n\nSupported actions:\n- invoke: Fire-and-forget agent invocation\n- ask: Invoke and wait for response\n- execute_tool: Call a specific tool on an agent\n- describe_tools: Get tool schemas for an agent\n- load: Get agent definition/system prompt\n- list_resources: List all resources available on an agent (docs, auth instructions, config schemas, etc.)\n- read_resources: Fetch one or more resources by URI",
|
|
446
|
-
inputSchema:
|
|
467
|
+
inputSchema: schemas?.callAgent
|
|
468
|
+
? zodToOpenAiJsonSchema(schemas.callAgent)
|
|
469
|
+
: callAgentInputSchema,
|
|
447
470
|
},
|
|
448
471
|
{
|
|
449
472
|
name: "list_agents",
|
|
450
473
|
description:
|
|
451
474
|
"List all registered agents and their available tools. Optionally search/filter by query using BM25 ranking.",
|
|
452
|
-
inputSchema:
|
|
475
|
+
inputSchema: schemas?.listAgents
|
|
476
|
+
? zodToOpenAiJsonSchema(schemas.listAgents)
|
|
477
|
+
: listAgentsInputSchema,
|
|
453
478
|
},
|
|
454
479
|
];
|
|
455
480
|
}
|
|
@@ -473,6 +498,15 @@ export function createAgentServer(
|
|
|
473
498
|
oauthIdentityProvider,
|
|
474
499
|
} = options;
|
|
475
500
|
|
|
501
|
+
// Build tool definitions and validation schemas from overrides
|
|
502
|
+
const toolDefs = getToolDefinitions(options.schemas);
|
|
503
|
+
const callAgentValidate = options.schemas?.callAgent
|
|
504
|
+
? nullTolerant(options.schemas.callAgent)
|
|
505
|
+
: callAgentValidationSchema;
|
|
506
|
+
const listAgentsValidate = options.schemas?.listAgents
|
|
507
|
+
? nullTolerant(options.schemas.listAgents)
|
|
508
|
+
: listAgentsValidationSchema;
|
|
509
|
+
|
|
476
510
|
// OIDC sign-in handler (if configured)
|
|
477
511
|
const oidcSignIn = options.oidcProvider
|
|
478
512
|
? createOIDCSignIn(options.oidcProvider)
|
|
@@ -518,7 +552,7 @@ export function createAgentServer(
|
|
|
518
552
|
|
|
519
553
|
case "tools/list":
|
|
520
554
|
return jsonRpcSuccess(request.id, {
|
|
521
|
-
tools:
|
|
555
|
+
tools: toolDefs,
|
|
522
556
|
});
|
|
523
557
|
|
|
524
558
|
case "tools/call": {
|
|
@@ -562,7 +596,11 @@ export function createAgentServer(
|
|
|
562
596
|
) {
|
|
563
597
|
switch (toolName) {
|
|
564
598
|
case "call_agent": {
|
|
565
|
-
|
|
599
|
+
// Validate + strip nulls (OpenAI convention: null = absent)
|
|
600
|
+
const parsed = callAgentValidate.safeParse(args);
|
|
601
|
+
const req = (parsed.success
|
|
602
|
+
? (parsed.data as Record<string, unknown>).request ?? parsed.data
|
|
603
|
+
: (args.request ?? args)) as CallAgentRequest;
|
|
566
604
|
|
|
567
605
|
// Inject auth context
|
|
568
606
|
if (auth) {
|
|
@@ -597,7 +635,7 @@ export function createAgentServer(
|
|
|
597
635
|
|
|
598
636
|
case "list_agents": {
|
|
599
637
|
const { query: listQuery, limit: listLimit, cursor: listCursor } =
|
|
600
|
-
|
|
638
|
+
listAgentsValidate.parse(args);
|
|
601
639
|
const agents = registry.list();
|
|
602
640
|
let visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
603
641
|
|