Fix MCP saved keyword metrics (#483)
This commit is contained in:
parent
72eaa09c32
commit
db45a0dd6d
@ -10,6 +10,7 @@ import {
|
|||||||
locationCodeSchema,
|
locationCodeSchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
} from "@/server/mcp/schemas";
|
} from "@/server/mcp/schemas";
|
||||||
|
import { savedKeywordMetricSchema } from "@/types/schemas/keywords";
|
||||||
|
|
||||||
const inputSchema = {
|
const inputSchema = {
|
||||||
projectId: projectIdSchema,
|
projectId: projectIdSchema,
|
||||||
@ -18,6 +19,13 @@ const inputSchema = {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.max(100)
|
.max(100)
|
||||||
.describe("Keywords to save (1-100)."),
|
.describe("Keywords to save (1-100)."),
|
||||||
|
metrics: z
|
||||||
|
.array(savedKeywordMetricSchema)
|
||||||
|
.max(100)
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Optional metrics for the saved keywords. Copy keyword, searchVolume, keywordDifficulty, cpc, competition, and intent from research_keywords rows; map each row's trend to monthlySearches. Match each metric using its keyword field.",
|
||||||
|
),
|
||||||
tags: z
|
tags: z
|
||||||
.array(z.string().min(1).max(64))
|
.array(z.string().min(1).max(64))
|
||||||
.max(20)
|
.max(20)
|
||||||
@ -70,6 +78,7 @@ export const saveKeywordsTool = {
|
|||||||
await KeywordResearchService.saveKeywords({
|
await KeywordResearchService.saveKeywords({
|
||||||
projectId: args.projectId,
|
projectId: args.projectId,
|
||||||
keywords: args.keywords,
|
keywords: args.keywords,
|
||||||
|
metrics: args.metrics,
|
||||||
tags: args.tags,
|
tags: args.tags,
|
||||||
tagMode: args.tagMode ?? "append",
|
tagMode: args.tagMode ?? "append",
|
||||||
locationCode,
|
locationCode,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { z } from "zod";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { listSavedKeywordsTool } from "./list-saved-keywords";
|
import { listSavedKeywordsTool } from "./list-saved-keywords";
|
||||||
import { saveKeywordsTool } from "./save-keywords";
|
import { saveKeywordsTool } from "./save-keywords";
|
||||||
@ -63,6 +64,43 @@ describe("saved keyword MCP tools", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts and passes keyword metrics through save_keywords", async () => {
|
||||||
|
mocks.saveKeywords.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
savedKeywordIds: ["saved_1"],
|
||||||
|
});
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
keyword: "technical seo",
|
||||||
|
searchVolume: 120,
|
||||||
|
keywordDifficulty: 18,
|
||||||
|
cpc: 2.5,
|
||||||
|
competition: 0.42,
|
||||||
|
intent: "commercial" as const,
|
||||||
|
monthlySearches: [
|
||||||
|
{ year: 2026, month: 7, searchVolume: 110 },
|
||||||
|
{ year: 2026, month: 8, searchVolume: 120 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const args = z.object(saveKeywordsTool.config.inputSchema).parse({
|
||||||
|
projectId: "project_1",
|
||||||
|
keywords: ["technical seo"],
|
||||||
|
metrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveKeywordsTool.handler(args, toolContext);
|
||||||
|
|
||||||
|
expect(mocks.saveKeywords).toHaveBeenCalledWith({
|
||||||
|
projectId: "project_1",
|
||||||
|
keywords: ["technical seo"],
|
||||||
|
metrics,
|
||||||
|
tagMode: "append",
|
||||||
|
locationCode: 2840,
|
||||||
|
languageCode: "en",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("replaces tags through save_keywords when requested", async () => {
|
it("replaces tags through save_keywords when requested", async () => {
|
||||||
mocks.saveKeywords.mockResolvedValue({
|
mocks.saveKeywords.mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -31,28 +31,12 @@ export const researchKeywordsSchema = z.object({
|
|||||||
clickstream: z.boolean().optional().default(false),
|
clickstream: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const saveKeywordsSchema = z
|
export const savedKeywordMetricSchema = z.object({
|
||||||
.object({
|
|
||||||
projectId: z.string().min(1),
|
|
||||||
keywords: z.array(z.string().min(1)).min(1).max(500),
|
|
||||||
locationCode: z.number().int().positive().optional(),
|
|
||||||
languageCode: z.string().min(2).max(8).optional(),
|
|
||||||
tags: z.array(savedKeywordTagSchema).max(20).optional(),
|
|
||||||
tagMode: z.enum(["append", "replace"]).optional(),
|
|
||||||
metrics: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
keyword: z.string().min(1),
|
keyword: z.string().min(1),
|
||||||
searchVolume: z.number().int().nonnegative().nullable().optional(),
|
searchVolume: z.number().int().nonnegative().nullable().optional(),
|
||||||
cpc: z.number().nonnegative().nullable().optional(),
|
cpc: z.number().nonnegative().nullable().optional(),
|
||||||
competition: z.number().min(0).max(1).nullable().optional(),
|
competition: z.number().min(0).max(1).nullable().optional(),
|
||||||
keywordDifficulty: z
|
keywordDifficulty: z.number().int().min(0).max(100).nullable().optional(),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(0)
|
|
||||||
.max(100)
|
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
intent: z
|
intent: z
|
||||||
.enum([
|
.enum([
|
||||||
"informational",
|
"informational",
|
||||||
@ -72,10 +56,17 @@ export const saveKeywordsSchema = z
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
}),
|
});
|
||||||
)
|
|
||||||
.max(500)
|
export const saveKeywordsSchema = z
|
||||||
.optional(),
|
.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
keywords: z.array(z.string().min(1)).min(1).max(500),
|
||||||
|
locationCode: z.number().int().positive().optional(),
|
||||||
|
languageCode: z.string().min(2).max(8).optional(),
|
||||||
|
tags: z.array(savedKeywordTagSchema).max(20).optional(),
|
||||||
|
tagMode: z.enum(["append", "replace"]).optional(),
|
||||||
|
metrics: z.array(savedKeywordMetricSchema).max(500).optional(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(value) => value.tagMode !== "replace" || (value.tags?.length ?? 0) > 0,
|
(value) => value.tagMode !== "replace" || (value.tags?.length ?? 0) > 0,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user