fix(mcp): classify project input validation (#455)

This commit is contained in:
Ben Senescu 2026-08-26 09:39:09 -04:00 committed by GitHub
parent 7f9d73102e
commit b592dc5043
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 29 additions and 4 deletions

View File

@ -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();
});
});

View File

@ -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,