From b592dc504344263013a3d78424167cd562d524b2 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:39:09 -0400 Subject: [PATCH] fix(mcp): classify project input validation (#455) --- src/server/mcp/tools/create-project.test.ts | 18 +++++++++++++++++- src/server/mcp/tools/create-project.ts | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/server/mcp/tools/create-project.test.ts b/src/server/mcp/tools/create-project.test.ts index f76ba76..3faeef6 100644 --- a/src/server/mcp/tools/create-project.test.ts +++ b/src/server/mcp/tools/create-project.test.ts @@ -75,7 +75,23 @@ describe("create_project MCP tool", () => { { name: "Bad market", languageCode: "en" }, toolContext, ), - ).rejects.toThrow(); + ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + expect(mocks.createProject).not.toHaveBeenCalled(); + }); + + it("rejects an unsupported location as a readable validation error", async () => { + const call = () => + createProjectTool.handler( + { name: "Bad location", locationCode: 999999 }, + toolContext, + ); + + await expect(call()).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + // The message must name the offending field so the calling agent can + // retry with a supported code, not just repeat the bare error code. + await expect(call()).rejects.toThrow( + "Unsupported DataForSEO location code", + ); expect(mocks.createProject).not.toHaveBeenCalled(); }); }); diff --git a/src/server/mcp/tools/create-project.ts b/src/server/mcp/tools/create-project.ts index f3e7f2e..55101bc 100644 --- a/src/server/mcp/tools/create-project.ts +++ b/src/server/mcp/tools/create-project.ts @@ -1,4 +1,5 @@ import { ProjectService } from "@/server/features/projects/services/ProjectService"; +import { AppError } from "@/server/lib/errors"; import { mcpResponse } from "@/server/mcp/formatters"; import { type ToolContext } from "@/server/mcp/context"; import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas"; @@ -65,9 +66,17 @@ export const createProjectTool = { handler: async (args: Args, context: ToolContext) => { const { baseUrl, ...auth } = context.auth; // Reuse the app's create schema so the market pair rule (a languageCode - // requires a locationCode) is enforced identically to the dashboard, and - // the domain is normalized the same way. - const input = createProjectSchema.parse(args); + // requires a locationCode) and domain normalization match the dashboard. + // A rejection is bad caller input, not a fault: VALIDATION_ERROR keeps it + // out of error reporting while still naming the bad field. + const parsedInput = createProjectSchema.safeParse(args); + if (!parsedInput.success) { + throw new AppError( + "VALIDATION_ERROR", + z.prettifyError(parsedInput.error), + ); + } + const input = parsedInput.data; const project = await ProjectService.createProject( auth.organizationId, input,