Files
sm-seo-plugin/mcp.go
Jakub Zych aa0c3bcacf feat: management API and an MCP tool per SEO admin operation
- /_seo/api/v1 routes for the effective SEO of a URL with sources and
  owner, URL rules, redirects (loops refused with redirect_loop), the
  404 log actions, robots.txt and its tester, settings and history
- the CMS-data issue report: missing and duplicate titles and
  descriptions, rules over records, redirect chains, canonicals to
  noindex or redirected addresses, noindex addresses robots.txt blocks
- 24 MCP tools on those routes (backend guard, golem15.seo.manage)
- AdminOperations maps every admin operation to its tool;
  TestEveryAdminOperationHasTool and TestMCPReadToolsAreGET guard drift
- robots.txt writes go through classes.SaveRobots from the admin too
2026-10-27 12:17:36 +01:00

290 lines
11 KiB
Go

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},
},
// 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,
},
// 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},
},
// Audit (D-13, D-14 CMS part).
{
Name: "list_issues",
Title: "List SEO issues",
Description: "Lists the CMS-data SEO issues: missing and duplicate titles and descriptions, URL rules overriding a record, redirect chains, canonicals pointing to noindex or redirected addresses, and noindex addresses robots.txt blocks; each names the tool that fixes it.",
Guard: mcpGuard,
Method: http.MethodGet,
Path: "/_seo/api/v1/issues",
Input: apicontrollers.ListIssuesInput{},
Permissions: []string{mcpPermission},
},
}}
}