diff --git a/src/server/mcp/server.ts b/src/server/mcp/server.ts index 806d1d500..49d4365f7 100644 --- a/src/server/mcp/server.ts +++ b/src/server/mcp/server.ts @@ -41,6 +41,7 @@ import { updateProjectContextTool, } from "@/server/mcp/tools/project-context"; import { listSavedKeywordsTool } from "@/server/mcp/tools/list-saved-keywords"; +import { removeSavedKeywordsTool } from "@/server/mcp/tools/remove-saved-keywords"; import { findSerpCompetitorsTool, getGoogleBusinessQuestionsTool, @@ -165,6 +166,7 @@ export function createOpenSeoMcpServer(authProps: McpProps) { register(listSavedKeywordsTool); register(researchKeywordsTool); register(saveKeywordsTool); + register(removeSavedKeywordsTool); register(getDomainOverviewTool); register(getDomainKeywordSuggestionsTool); register(getBacklinksOverviewTool); diff --git a/src/server/mcp/tools/list-saved-keywords.ts b/src/server/mcp/tools/list-saved-keywords.ts index 44515e094..ce52f1fd4 100644 --- a/src/server/mcp/tools/list-saved-keywords.ts +++ b/src/server/mcp/tools/list-saved-keywords.ts @@ -69,7 +69,7 @@ export const listSavedKeywordsTool = { row.tags.length > 0 ? ` tags:${row.tags.map((tag) => tag.name).join(",")}` : ""; - return `- ${row.keyword} vol:${row.searchVolume ?? "?"} kd:${row.keywordDifficulty ?? "?"} cpc:${row.cpc != null ? `$${row.cpc.toFixed(2)}` : "?"}${tagText}`; + return `- ${row.keyword} id:${row.id} vol:${row.searchVolume ?? "?"} kd:${row.keywordDifficulty ?? "?"} cpc:${row.cpc != null ? `$${row.cpc.toFixed(2)}` : "?"}${tagText}`; }) .join("\n"); return mcpResponse({ diff --git a/src/server/mcp/tools/remove-saved-keywords.ts b/src/server/mcp/tools/remove-saved-keywords.ts new file mode 100644 index 000000000..754efe658 --- /dev/null +++ b/src/server/mcp/tools/remove-saved-keywords.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService"; +import { buildProjectMeta } from "@/server/mcp/context"; +import { mcpResponse } from "@/server/mcp/formatters"; +import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas"; +import { withMcpProjectAuth } from "@/server/mcp/project-auth"; +import { projectIdSchema } from "@/server/mcp/schemas"; + +const inputSchema = { + projectId: projectIdSchema, + savedKeywordIds: z + .array(z.string().min(1)) + .min(1) + .max(2000) + .describe( + "Saved-keyword row IDs to delete. Use the `id` values returned by list_saved_keywords.", + ), +} as const; + +type Args = z.infer>; + +export const removeSavedKeywordsTool = { + name: "remove_saved_keywords", + config: { + title: "Remove saved keywords", + description: + "Permanently deletes keywords from a project's saved-keywords list by their row ID. Uses no credits — does not call DataForSEO. This does not affect Rank Tracking; use remove_rank_tracking_keywords for that list separately.", + inputSchema, + outputSchema: z + .object({ + projectId: z.string(), + requested: z.number(), + deletedCount: z.number(), + ...optionalMetaOutputSchema, + }) + .passthrough(), + annotations: { + readOnlyHint: false, + openWorldHint: false, + destructiveHint: true, + }, + }, + handler: withMcpProjectAuth(async (args: Args, context) => { + const requested = args.savedKeywordIds.length; + const result = await KeywordResearchService.removeSavedKeywords( + args.projectId, + { projectId: args.projectId, savedKeywordIds: args.savedKeywordIds }, + ); + return mcpResponse({ + text: `Deleted ${result.deletedCount} of ${requested} requested saved keyword${requested === 1 ? "" : "s"}.`, + meta: buildProjectMeta( + context, + args.projectId, + `/p/${args.projectId}/saved`, + ), + structuredContent: { + projectId: args.projectId, + requested, + deletedCount: result.deletedCount, + }, + }); + }), +};