Skip to content

Commit a9a040b

Browse files
authored
Merge pull request #14 from code-rabi/benr/param-validation
Benr/param validation
2 parents 4fe1dce + 7003602 commit a9a040b

3 files changed

Lines changed: 108 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "toolception",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"private": false,
55
"type": "module",
66
"main": "dist/index.js",

src/server/createMcpServer.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
FastifyTransport,
1111
type FastifyTransportOptions,
1212
} from "../http/FastifyTransport.js";
13+
import { z } from "zod";
1314

1415
export interface CreateMcpServerOptions {
1516
catalog: ToolSetCatalog;
@@ -28,7 +29,34 @@ export interface CreateMcpServerOptions {
2829
configSchema?: object;
2930
}
3031

32+
/**
33+
* Zod schema for validating startup configuration.
34+
* Uses strict mode to reject unknown properties like 'initialToolsets'.
35+
*/
36+
const startupConfigSchema = z
37+
.object({
38+
mode: z.enum(["DYNAMIC", "STATIC"]).optional(),
39+
toolsets: z.union([z.array(z.string()), z.literal("ALL")]).optional(),
40+
})
41+
.strict();
42+
3143
export async function createMcpServer(options: CreateMcpServerOptions) {
44+
// Validate startup configuration if provided
45+
if (options.startup) {
46+
try {
47+
startupConfigSchema.parse(options.startup);
48+
} catch (error) {
49+
if (error instanceof z.ZodError) {
50+
const formatted = error.format();
51+
throw new Error(
52+
`Invalid startup configuration:\n${JSON.stringify(formatted, null, 2)}\n\n` +
53+
`Hint: Common mistake - use "toolsets" not "initialToolsets"`
54+
);
55+
}
56+
throw error;
57+
}
58+
}
59+
3260
const mode: Exclude<Mode, "ALL"> = options.startup?.mode ?? "DYNAMIC";
3361
if (typeof options.createServer !== "function") {
3462
throw new Error("createMcpServer: `createServer` (factory) is required");

tests/createMcpServer.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,83 @@ describe("createMcpServer", () => {
206206
const base = f.created[0];
207207
expect(base.calls.filter((n) => n === "testset.test_tool").length).toBe(1);
208208
});
209+
210+
it("rejects invalid startup properties with Zod validation", async () => {
211+
const { createServer } = makeFakeServerFactory();
212+
213+
// Type cast to bypass TypeScript checking (simulates user with loose types)
214+
const invalidOptions = {
215+
catalog: { core: { name: "Core", description: "", tools: [] } },
216+
startup: { mode: "STATIC", initialToolsets: ["core"] }, // Wrong property!
217+
createServer,
218+
} as any;
219+
220+
await expect(createMcpServer(invalidOptions)).rejects.toThrow(
221+
/Invalid startup configuration/
222+
);
223+
});
224+
225+
it("successfully parses valid startup config with 'mode' and 'toolsets' properties", async () => {
226+
const f = makeFakeServerFactory();
227+
const staticCatalog = {
228+
core: {
229+
name: "Core",
230+
description: "",
231+
tools: [
232+
{
233+
name: "ping",
234+
description: "",
235+
inputSchema: {},
236+
handler: async () => ({ content: [{ type: "text", text: "pong" }] }),
237+
},
238+
],
239+
},
240+
} as any;
241+
242+
await expect(
243+
createMcpServer({
244+
catalog: staticCatalog,
245+
startup: { mode: "STATIC", toolsets: ["core"] },
246+
createServer: f.createServer,
247+
})
248+
).resolves.toBeTruthy();
249+
250+
const base = f.created[0];
251+
expect(base.calls.filter((n) => n === "core.ping").length).toBe(1);
252+
});
253+
254+
it("throws a Zod validation error for completely malformed startup config object", async () => {
255+
const { createServer } = makeFakeServerFactory();
256+
257+
// Non-object startup value
258+
const bad1 = {
259+
catalog,
260+
startup: 42 as any,
261+
createServer,
262+
} as any;
263+
264+
// Object with wrong types for both fields
265+
const bad2 = {
266+
catalog,
267+
startup: { mode: 123, toolsets: 555 } as any,
268+
createServer,
269+
} as any;
270+
271+
await expect(createMcpServer(bad1)).rejects.toThrow(/Invalid startup configuration/);
272+
await expect(createMcpServer(bad2)).rejects.toThrow(/Invalid startup configuration/);
273+
});
274+
275+
it("accepts missing startup config without error (defaults to DYNAMIC)", async () => {
276+
const f = makeFakeServerFactory();
277+
await expect(
278+
createMcpServer({
279+
catalog,
280+
createServer: f.createServer,
281+
})
282+
).resolves.toBeTruthy();
283+
284+
// Default behavior in DYNAMIC mode is to register meta-tools
285+
const base = f.created[0];
286+
expect(base.calls.includes("list_tools")).toBe(true);
287+
});
209288
});

0 commit comments

Comments
 (0)