From 8312ea34433afa66fd160313595f4726b88d2c5e Mon Sep 17 00:00:00 2001 From: Ben Rabinovich Date: Fri, 9 Jan 2026 14:27:38 +0200 Subject: [PATCH 1/3] chore: startup config validation --- src/server/createMcpServer.ts | 28 ++++++++++++++++++++++++++++ tests/createMcpServer.test.ts | 15 +++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/server/createMcpServer.ts b/src/server/createMcpServer.ts index b7ef665..da3ecfb 100644 --- a/src/server/createMcpServer.ts +++ b/src/server/createMcpServer.ts @@ -10,6 +10,7 @@ import { FastifyTransport, type FastifyTransportOptions, } from "../http/FastifyTransport.js"; +import { z } from "zod"; export interface CreateMcpServerOptions { catalog: ToolSetCatalog; @@ -28,7 +29,34 @@ export interface CreateMcpServerOptions { configSchema?: object; } +/** + * Zod schema for validating startup configuration. + * Uses strict mode to reject unknown properties like 'initialToolsets'. + */ +const startupConfigSchema = z + .object({ + mode: z.enum(["DYNAMIC", "STATIC"]).optional(), + toolsets: z.union([z.array(z.string()), z.literal("ALL")]).optional(), + }) + .strict(); + export async function createMcpServer(options: CreateMcpServerOptions) { + // Validate startup configuration if provided + if (options.startup) { + try { + startupConfigSchema.parse(options.startup); + } catch (error) { + if (error instanceof z.ZodError) { + const formatted = error.format(); + throw new Error( + `Invalid startup configuration:\n${JSON.stringify(formatted, null, 2)}\n\n` + + `Hint: Common mistake - use "toolsets" not "initialToolsets"` + ); + } + throw error; + } + } + const mode: Exclude = options.startup?.mode ?? "DYNAMIC"; if (typeof options.createServer !== "function") { throw new Error("createMcpServer: `createServer` (factory) is required"); diff --git a/tests/createMcpServer.test.ts b/tests/createMcpServer.test.ts index 4d8184b..4a0875f 100644 --- a/tests/createMcpServer.test.ts +++ b/tests/createMcpServer.test.ts @@ -206,4 +206,19 @@ describe("createMcpServer", () => { const base = f.created[0]; expect(base.calls.filter((n) => n === "testset.test_tool").length).toBe(1); }); + + it("rejects invalid startup properties with Zod validation", async () => { + const { createServer } = makeFakeServerFactory(); + + // Type cast to bypass TypeScript checking (simulates user with loose types) + const invalidOptions = { + catalog: { core: { name: "Core", description: "", tools: [] } }, + startup: { mode: "STATIC", initialToolsets: ["core"] }, // Wrong property! + createServer, + } as any; + + await expect(createMcpServer(invalidOptions)).rejects.toThrow( + /Invalid startup configuration/ + ); + }); }); From 63c95c816dfc3b741101ad445c14cd38551622f6 Mon Sep 17 00:00:00 2001 From: Ben Rabinovich Date: Fri, 9 Jan 2026 14:31:12 +0200 Subject: [PATCH 2/3] chore: startup config validation tests --- tests/createMcpServer.test.ts | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/createMcpServer.test.ts b/tests/createMcpServer.test.ts index 4a0875f..c01e5f0 100644 --- a/tests/createMcpServer.test.ts +++ b/tests/createMcpServer.test.ts @@ -221,4 +221,68 @@ describe("createMcpServer", () => { /Invalid startup configuration/ ); }); + + it("successfully parses valid startup config with 'mode' and 'toolsets' properties", async () => { + const f = makeFakeServerFactory(); + const staticCatalog = { + core: { + name: "Core", + description: "", + tools: [ + { + name: "ping", + description: "", + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: "pong" }] }), + }, + ], + }, + } as any; + + await expect( + createMcpServer({ + catalog: staticCatalog, + startup: { mode: "STATIC", toolsets: ["core"] }, + createServer: f.createServer, + }) + ).resolves.toBeTruthy(); + + const base = f.created[0]; + expect(base.calls.filter((n) => n === "core.ping").length).toBe(1); + }); + + it("throws a Zod validation error for completely malformed startup config object", async () => { + const { createServer } = makeFakeServerFactory(); + + // Non-object startup value + const bad1 = { + catalog, + startup: 42 as any, + createServer, + } as any; + + // Object with wrong types for both fields + const bad2 = { + catalog, + startup: { mode: 123, toolsets: 555 } as any, + createServer, + } as any; + + await expect(createMcpServer(bad1)).rejects.toThrow(/Invalid startup configuration/); + await expect(createMcpServer(bad2)).rejects.toThrow(/Invalid startup configuration/); + }); + + it("accepts missing startup config without error (defaults to DYNAMIC)", async () => { + const f = makeFakeServerFactory(); + await expect( + createMcpServer({ + catalog, + createServer: f.createServer, + }) + ).resolves.toBeTruthy(); + + // Default behavior in DYNAMIC mode is to register meta-tools + const base = f.created[0]; + expect(base.calls.includes("list_tools")).toBe(true); + }); }); From 70036024941aa67cdd80a4ab7f36b6ea1161dea5 Mon Sep 17 00:00:00 2001 From: Ben Rabinovich Date: Fri, 9 Jan 2026 14:32:52 +0200 Subject: [PATCH 3/3] chore: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cc3eda..629f9a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "toolception", - "version": "0.5.1", + "version": "0.5.2", "private": false, "type": "module", "main": "dist/index.js",