package seo import ( "net/http" apicontrollers "git.golem15.com/golem15/sm-seo-plugin/controllers/api" "git.golem15.com/golem15/summercms/modules/pact" ) var _ pact.HasMCP = (*Plugin)(nil) // mcpGuard is the guard every SEO tool runs as: the backend account linked // to the MCP user. The routes resolve that administrator themselves. const mcpGuard = "backend" // mcpPermission is the one SEO permission (D-15) every tool is listed with; // the routes check it on every call. const mcpPermission = apicontrollers.PermissionManage // MCP declares the SEO tools (D-10, P-24): one per operation of the SEO // admin (operations.go maps each admin operation to its tool), each bound // to a /_seo/api/v1 management route, run as the backend account linked to // the MCP user and listed only when that account has golem15.seo.manage. // pier prefixes them with seo_. GET tools are read-only, so a read grant // sees only those; deletes and "mark as gone" are destructive. func (p *Plugin) MCP() pact.MCP { destructive := true return pact.MCP{Tools: []pact.MCPTool{ // Pages addressed by URL (D-11, D-13). { Name: "get_url_seo", Title: "Get the SEO of a URL", Description: "Shows the effective SEO of a page addressed by URL: every value with its source (URL rule, record, template, global settings, content or default), the redirect decision, the owner a change would be written to (the record or a URL rule) and the warnings of the SEO tab.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/seo", Input: apicontrollers.GetSEOInput{}, Permissions: []string{mcpPermission}, }, { Name: "set_url_seo", Title: "Set the SEO of a URL", Description: "Changes the SEO values of a page addressed by URL. A record behind the URL gets the change in its SEO tab in the URL's locale; otherwise, or with force_rule, an exact URL rule is written. The answer says where it was written.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/seo", Input: apicontrollers.SetSEOInput{}, Permissions: []string{mcpPermission}, }, { Name: "get_graph", Title: "Get the structured data of a URL", Description: "Shows the JSON-LD @graph a page addressed by URL emits (the nodes the CMS generates, the custom JSON-LD merged in) and every node's missing required and recommended properties per Google's rules; rules false marks a type Google has no rich-result rules for. With schema, previews an unsaved structured data value. Fix through seo_set_url_seo (schema, primary_parent) or seo_update_settings (publisher, local business).", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/graph", Input: apicontrollers.GetGraphInput{}, Permissions: []string{mcpPermission}, }, // URL rules (D-02). { Name: "list_rules", Title: "List URL rules", Description: "Lists the URL rules (SEO for an address with or without a record behind it) by address, with a search text and the match kind.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/rules", Input: apicontrollers.ListRulesInput{}, Permissions: []string{mcpPermission}, }, { Name: "show_rule", Title: "Show a URL rule", Description: "Shows one URL rule by id.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/rules/{id}", Input: apicontrollers.RuleIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "create_rule", Title: "Create a URL rule", Description: "Creates a URL rule: an exact address or a pattern where * matches any text, with the values it sets. A rule wins over the record's SEO tab.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/rules", Input: apicontrollers.CreateRuleInput{}, Permissions: []string{mcpPermission}, }, { Name: "update_rule", Title: "Update a URL rule", Description: "Changes the fields sent of one URL rule by id.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/rules/{id}", Input: apicontrollers.UpdateRuleInput{}, Permissions: []string{mcpPermission}, }, { Name: "delete_rule", Title: "Delete a URL rule", Description: "Deletes one URL rule by id; history keeps its values.", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/rules/{id}", Input: apicontrollers.RuleIDInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, // Redirects (D-16 to D-19). { Name: "list_redirects", Title: "List redirects", Description: "Lists redirects, newest change first, with a search text and the status (301, 302, 410) and origin filters.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/redirects", Input: apicontrollers.ListRedirectsInput{}, Permissions: []string{mcpPermission}, }, { Name: "show_redirect", Title: "Show a redirect", Description: "Shows one redirect by id.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/redirects/{id}", Input: apicontrollers.RedirectIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "create_redirect", Title: "Create a redirect", Description: "Creates a 301, 302 or 410 redirect from a path, a pattern or a regular expression. A redirect that would create a loop is refused with redirect_loop and the loop's path; a chain is saved with a warning.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/redirects", Input: apicontrollers.CreateRedirectInput{}, Permissions: []string{mcpPermission}, }, { Name: "update_redirect", Title: "Update a redirect", Description: "Changes the fields sent of one redirect by id; a loop is refused like a create.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/redirects/{id}", Input: apicontrollers.UpdateRedirectInput{}, Permissions: []string{mcpPermission}, }, { Name: "delete_redirect", Title: "Delete a redirect", Description: "Deletes one redirect by id; history keeps its values.", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/redirects/{id}", Input: apicontrollers.RedirectIDInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, // The page registry (D-33). { Name: "list_pages", Title: "List registry pages", Description: "Lists the page registry (the site's own frontend pages such as the home page, contact or offer pages) by address, with every locale's address and name, the kind, the origin (manual, reported by the site, MCP) and whether the site's newest route report missed the page. Filters: search text, id, kind, origin, missing.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/pages", Input: apicontrollers.ListPagesInput{}, Permissions: []string{mcpPermission}, }, { Name: "create_page", Title: "Create a registry page", Description: "Adds a frontend page to the registry, so it gets SEO values (set them with set_url_seo on its address) and a place in the sitemaps: its path per locale, name, kind (page, offer, service, landing), parent and content date. Refused for an address another page or another content type (a post, a category) already has.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/pages", Input: apicontrollers.CreatePageInput{}, Permissions: []string{mcpPermission}, }, { Name: "update_page", Title: "Update a registry page", Description: "Changes the fields sent of one registry page by id; in url and title only the locales sent. A changed address gets a 301 from the old one.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/pages/{id}", Input: apicontrollers.UpdatePageInput{}, Permissions: []string{mcpPermission}, }, { Name: "delete_page", Title: "Delete a registry page", Description: "Deletes one registry page by id with its SEO values; its address leaves the sitemaps on the next rebuild. A page the site reports comes back with its next route report.", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/pages/{id}", Input: apicontrollers.PageIDInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, // Auto-links (D-82): phrases texts link to a page. { Name: "list_autolinks", Title: "List auto-links", Description: "Lists the auto-link phrases by phrase: each links its first occurrence in a text to its address, in one locale or all (locale \"\"), unless inactive. Filters: search text, locale (all for every locale), origin (manual, keyphrase, mcp) and active.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/autolinks", Input: apicontrollers.ListAutolinksInput{}, Permissions: []string{mcpPermission}, }, { Name: "create_autolink", Title: "Create an auto-link", Description: "Adds a phrase that texts link at its first occurrence (whole words, any letter case, never in headings, existing links or on the page it links to), at most autolink_max per text (seo_update_settings). The phrase needs 3 characters and may exist once per locale; the address is a path or a full http(s) address.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/autolinks", Input: apicontrollers.CreateAutolinkInput{}, Permissions: []string{mcpPermission}, }, { Name: "update_autolink", Title: "Update an auto-link", Description: "Changes the fields sent of one auto-link (phrase, address, locale, active, note), checked like a create.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/autolinks/{id}", Input: apicontrollers.UpdateAutolinkInput{}, Permissions: []string{mcpPermission}, }, { Name: "delete_autolinks", Title: "Delete auto-links", Description: "Deletes auto-links by id; texts stop linking their phrases when their pages are next rendered.", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/autolinks", Input: apicontrollers.DeleteAutolinksInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, { Name: "autolinks_from_keyphrases", Title: "Add auto-links from focus keyphrases", Description: "Adds one auto-link per record whose focus keyphrase in a locale is not yet a phrase in that locale, linking to the record's address (origin keyphrase), and answers the count added. Running it again adds only new keyphrases.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/autolinks/from-keyphrases", Input: apicontrollers.AutolinksFromKeyphrasesInput{}, Permissions: []string{mcpPermission}, }, // 404 log (D-20). { Name: "list_not_found", Title: "List the 404 log", Description: "Lists the addresses that answered 404, most hits first, with whether a redirect answers them now (status open or resolved).", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/notfound", Input: apicontrollers.ListNotFoundInput{}, Permissions: []string{mcpPermission}, }, { Name: "redirect_not_found", Title: "Redirect a 404 address", Description: "Creates a 301 from a logged 404 address to a target. Refused when a rule already answers the address.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/notfound/{id}/redirect", Input: apicontrollers.RedirectNotFoundInput{}, Permissions: []string{mcpPermission}, }, { Name: "gone_not_found", Title: "Mark a 404 address as gone", Description: "Creates a 410 for a logged 404 address, which tells search engines to drop it. Refused when a rule already answers the address.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/notfound/{id}/gone", Input: apicontrollers.NotFoundIDInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, { Name: "delete_not_found", Title: "Remove a 404 log entry", Description: "Removes one entry from the 404 log; it comes back on its next 404.", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/notfound/{id}", Input: apicontrollers.NotFoundIDInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, // robots.txt and AI bots (D-22, D-24, D-25, D-99). { Name: "show_robots", Title: "Show robots.txt", Description: "Shows the robots.txt rules, the AI bot switches, the file crawlers get in production and whether the site runs outside production (crawlers then get Disallow: /).", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/robots", Input: struct{}{}, Permissions: []string{mcpPermission}, }, { Name: "update_robots", Title: "Update robots.txt", Description: "Changes the robots.txt rules and the AI bot switches sent. The answer lists noindex addresses the new rules keep Googlebot from.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/robots", Input: apicontrollers.UpdateRobotsInput{}, Permissions: []string{mcpPermission}, }, { Name: "test_robots", Title: "Test an address against robots.txt", Description: "Answers whether a crawler may fetch an address under the saved production robots.txt, with Google's matcher and the deciding line.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/robots/test", Input: apicontrollers.TestRobotsInput{}, Permissions: []string{mcpPermission}, }, // Settings (D-03, D-05, D-07, D-20b, D-21, D-23, D-104). { Name: "show_settings", Title: "Show the SEO settings", Description: "Shows the SEO settings (templates, defaults, the query parameter list, indexing paths, the address form, verification) and the settings of every content type.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/settings", Input: struct{}{}, Permissions: []string{mcpPermission}, }, { Name: "update_settings", Title: "Update the SEO settings", Description: "Changes the SEO settings sent and, under types, the settings of content types by key, with the checks of the settings screen.", Guard: mcpGuard, Method: http.MethodPut, Path: "/_seo/api/v1/settings", Input: apicontrollers.UpdateSettingsInput{}, Permissions: []string{mcpPermission}, }, // History (D-13). { Name: "list_history", Title: "List SEO history", Description: "Lists SEO changes, newest first, with author, time, via (admin, mcp, import, auto, undo), before and after values, filtered by via, kind or batch.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/history", Input: apicontrollers.ListHistoryInput{}, Permissions: []string{mcpPermission}, }, { Name: "show_history", Title: "Show an SEO change", Description: "Shows one SEO change by id with its before and after values.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/history/{id}", Input: apicontrollers.HistoryIDInput{}, Permissions: []string{mcpPermission}, }, // Undo (D-13). { Name: "undo_change", Title: "Undo an SEO change", Description: "Restores the values before one SEO change from history; the undo is itself recorded. Refused with undo_stale when a later change touched the same values (naming who and when), undo_loop when the restore would create a redirect loop, undo_gone when the record is gone.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/history/{id}/undo", Input: apicontrollers.HistoryIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "undo_batch", Title: "Undo a batch", Description: "Undoes every change of a batch or an import (by batch id) as a whole, or none of them, with the refusals of undo_change.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/history/batches/{batch}/undo", Input: apicontrollers.HistoryBatchInput{}, Permissions: []string{mcpPermission}, }, // Bulk changes and CSV (D-12, D-13). { Name: "apply_batch", Title: "Apply a batch of SEO changes", Description: "Applies a list of changes (set_seo by URL, redirect and rule creates, updates and deletes) all or nothing. With dry_run it answers the diff and the conflicts (redirect loops and chains, noindex on a published page, a canonical pointing to a redirected page) and writes nothing; a loop refuses the whole batch.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/batch", Input: apicontrollers.ApplyBatchInput{}, Permissions: []string{mcpPermission}, }, { Name: "export_csv", Title: "Export SEO data as CSV", Description: "Exports redirects, URL rules or the SEO of records (by URL) as CSV, UTF-8 with a BOM, 1000 rows per page; cells that start a formula are prefixed with an apostrophe.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/export", Input: apicontrollers.ExportInput{}, Permissions: []string{mcpPermission}, }, { Name: "import_csv", Title: "Import SEO data from CSV", Description: "Imports redirects, URL rules or the SEO of records (rows address pages by URL; no record means a URL rule) from CSV of at most 900 KB. With dry_run it previews every row as new, changed, unchanged or refused with notes; otherwise every row is applied as one batch, or nothing while a row is refused (a loop, an invalid value).", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/import", Input: apicontrollers.ImportInput{}, Permissions: []string{mcpPermission}, }, // Site audit and crawls (D-13, D-14, SC-3). { Name: "list_issues", Title: "List SEO issues", Description: "Lists the site audit's current issues, errors first: what the newest crawl found per URL (statuses, sitemaps, canonicals, titles and descriptions, headings, structured data, hreflang, links, images, content that needs JavaScript, orphan pages) and the CMS-data checks (missing and duplicate titles, URL rules over records, redirect chains, canonicals to noindex or redirected addresses, robots.txt blocks, images without alt text, H1 in content). Each issue names the MCP tool that fixes it (fix tool), or fix code with details.what naming the template element the site's developer changes. Ignored issues are left out unless status is ignored or all.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/issues", Input: apicontrollers.ListIssuesInput{}, Permissions: []string{mcpPermission}, }, { Name: "recheck_url", Title: "Check an issue's address again", Description: "Fetches the address of one issue again through the crawler and replaces that address's issues; answers the issues left for it. Use it after fixing an issue to confirm the fix.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/issues/{id}/recheck", Input: apicontrollers.IssueIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "ignore_issues", Title: "Ignore SEO issues", Description: "Ignores issues by id: they stay hidden in later crawls until seo_unignore_issues shows them again.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/issues/ignore", Input: apicontrollers.IssueIDsInput{}, Permissions: []string{mcpPermission}, }, { Name: "unignore_issues", Title: "Stop ignoring SEO issues", Description: "Shows ignored issues in the audit again, by id.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/issues/unignore", Input: apicontrollers.IssueIDsInput{}, Permissions: []string{mcpPermission}, }, { Name: "start_crawl", Title: "Start a site crawl", Description: "Starts a crawl of the site: the crawler fetches the site's own pages like a search engine (two requests at a time, within the Site audit limits) and stores what it finds as issues. One crawl runs at a time; a second start answers crawls.already_running. Follow it with seo_get_crawl; when it finishes, read the issues with seo_list_issues.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/crawls", Input: apicontrollers.StartCrawlInput{}, Permissions: []string{mcpPermission}, }, { Name: "cancel_crawl", Title: "Cancel a crawl", Description: "Cancels a queued or running crawl by id; the pages fetched so far and their issues are kept.", Guard: mcpGuard, Method: http.MethodPost, Path: "/_seo/api/v1/crawls/{id}/cancel", Input: apicontrollers.CrawlIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "list_crawls", Title: "List crawls", Description: "Lists the crawls, newest first, with their status, pages, issue count, who started them and the comparison with the JavaScript-rendered pages (render: skipped, done or failed).", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/crawls", Input: apicontrollers.ListCrawlsInput{}, Permissions: []string{mcpPermission}, }, { Name: "get_crawl", Title: "Get a crawl", Description: "Shows one crawl: status, pages fetched and whether the page limit stopped it, the error of a failed crawl, the issues per kind and severity, whether the staging lock's noindex was left out, and render {status, pages, note}: whether the pages were compared with their JavaScript-rendered versions or that comparison was skipped (no Chrome path configured) or failed.", Guard: mcpGuard, Method: http.MethodGet, Path: "/_seo/api/v1/crawls/{id}", Input: apicontrollers.CrawlIDInput{}, Permissions: []string{mcpPermission}, }, { Name: "delete_crawls", Title: "Delete crawls", Description: "Deletes crawls by id with their pages and issues; a queued or running crawl refuses the call (crawls.delete_running).", Guard: mcpGuard, Method: http.MethodDelete, Path: "/_seo/api/v1/crawls", Input: apicontrollers.DeleteCrawlsInput{}, Permissions: []string{mcpPermission}, Destructive: &destructive, }, }} }