106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { z } from "zod";
|
|
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
|
|
import { mcpResponse } from "@/server/mcp/formatters";
|
|
import { buildProjectMeta } from "@/server/mcp/context";
|
|
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
|
|
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
|
|
import {
|
|
DEFAULT_LANGUAGE_CODE,
|
|
DEFAULT_LOCATION_CODE,
|
|
languageCodeSchema,
|
|
locationCodeSchema,
|
|
projectIdSchema,
|
|
} from "@/server/mcp/schemas";
|
|
|
|
const inputSchema = {
|
|
projectId: projectIdSchema,
|
|
keywords: z
|
|
.array(z.string().min(1))
|
|
.min(1)
|
|
.max(100)
|
|
.describe("Keywords to save (1-100)."),
|
|
tags: z
|
|
.array(z.string().min(1).max(64))
|
|
.max(20)
|
|
.optional()
|
|
.describe(
|
|
"Optional tags to attach to every saved keyword. Ask the user for explicit confirmation before using this, especially when saving many keywords or creating new tag names.",
|
|
),
|
|
tagMode: z
|
|
.enum(["append", "replace"])
|
|
.optional()
|
|
.describe(
|
|
"How to apply tags. Defaults to append. Use replace to remove existing tags from these saved keywords before applying the provided tags.",
|
|
),
|
|
locationCode: locationCodeSchema.optional(),
|
|
languageCode: languageCodeSchema.optional(),
|
|
} as const;
|
|
|
|
type Args = z.infer<z.ZodObject<typeof inputSchema>>;
|
|
|
|
export const saveKeywordsTool = {
|
|
name: "save_keywords",
|
|
config: {
|
|
title: "Save keywords",
|
|
description:
|
|
"Save keywords to a project's saved-keywords list. Free — does not call DataForSEO. Idempotent: re-saving an existing keyword is a no-op. If tags are provided, missing tags may be created. By default tags are appended; set tagMode=replace to remove existing tags from these saved keywords before applying the provided tags, which is useful for reorganizing keywords into page/topic clusters. Ask the user for confirmation before applying or replacing tags broadly.",
|
|
inputSchema,
|
|
outputSchema: {
|
|
projectId: z.string(),
|
|
savedCount: z.number(),
|
|
keywords: z.array(z.string()),
|
|
tags: z.array(z.string()),
|
|
tagMode: z.enum(["append", "replace"]),
|
|
locationCode: z.number(),
|
|
languageCode: z.string(),
|
|
...optionalMetaOutputSchema,
|
|
},
|
|
annotations: {
|
|
readOnlyHint: false,
|
|
openWorldHint: false,
|
|
destructiveHint: false,
|
|
},
|
|
},
|
|
handler: withMcpProjectAuth(async (args: Args, context) => {
|
|
if (args.tagMode === "replace" && (args.tags?.length ?? 0) === 0) {
|
|
throw new Error("Replacement tags are required when tagMode is replace.");
|
|
}
|
|
|
|
const locationCode = args.locationCode ?? DEFAULT_LOCATION_CODE;
|
|
const languageCode = args.languageCode ?? DEFAULT_LANGUAGE_CODE;
|
|
|
|
await KeywordResearchService.saveKeywords({
|
|
projectId: args.projectId,
|
|
keywords: args.keywords,
|
|
tags: args.tags,
|
|
tagMode: args.tagMode ?? "append",
|
|
locationCode,
|
|
languageCode,
|
|
});
|
|
|
|
const tagText =
|
|
args.tags && args.tags.length > 0
|
|
? ` with tag(s): ${args.tags.join(", ")}`
|
|
: "";
|
|
const modeText = args.tagMode === "replace" ? " Replaced tags." : "";
|
|
|
|
return mcpResponse({
|
|
text: `Saved ${args.keywords.length} keyword(s)${tagText} to project ${args.projectId}.${modeText}`,
|
|
meta: buildProjectMeta(
|
|
context,
|
|
args.projectId,
|
|
`/p/${args.projectId}/saved`,
|
|
),
|
|
structuredContent: {
|
|
projectId: args.projectId,
|
|
savedCount: args.keywords.length,
|
|
keywords: args.keywords,
|
|
tags: args.tags ?? [],
|
|
tagMode: args.tagMode ?? "append",
|
|
locationCode,
|
|
languageCode,
|
|
},
|
|
});
|
|
}),
|
|
};
|