Are you the author? Sign in to claim
Canonical unified MCP server in golang for Hugo sites — oauth register and self registered token
Canonical unified MCP server for Hugo sites.
Public endpoint: https://mcp.arleo.eu/mcp
This MCP is far more than a remote Markdown editor for Hugo: it's an intelligent content-management interface. It gives AI agents structured understanding and safe operations on a Hugo site. Example site using this MCP: www.arleo.eu.
Ce MCP est bien plus qu'un éditeur à distance de Markdown pour Hugo : il est une interface de gestion intelligente du contenu. Il donne aux agents IA une compréhension structurée et des opérations sûres sur un site Hugo. Exemple de site utilisant ce MCP : www.arleo.eu.
Content mostly written with Claude Code and Codex. / Contenu majoritairement codé avec Claude Code et Codex.
mcp-hugo-server-go exposes a Hugo site through the Model Context Protocol with public discovery, OAuth-backed scopes, and strict separation between read, write, and admin operations.
It is the unified successor of:
hugo-public-mcp for public discovery, OAuth, and auth.mdhugo-mcp-go for content and administration toolsmcp-runtime-go for MCP transport/runtime behaviorThe server enforces exactly two internal scopes (#450):
read: full visibility, including drafts and other source-only/pre-publication
content. Requires no secret and is auto-registrable (self-service, the same
mechanism the old reader profile used).write: requires a registered OAuth client (client_id + client_secret).
Implies read — a write token gets everything, including build/site/integrity/
diagnostic operations that used to require a separate site.admin scope.Legacy clients may still send any scope string from the pre-#450 four-tier model
(reader, content.read, content.write, site.admin, system.admin, ...) or the
original mcp alias. The server accepts all of them as deprecated compatibility
aliases, resolved to read/write via oauth.CanonicalScope, but only read and
write are ever advertised as canonical scopes. See
docs/mcp-contract.md §6.12
for the full mapping and rationale.
The current tool inventory is documented in docs/tools.md and should be treated as the source of truth for scope mapping and tool naming.
slug vs source_keyTool payloads use two different shapes for the same page identity, and mixing them up is a recurring source of confusion (#610):
slug on read-tool outputs (list_pages, search_pages, get_recent_posts, get_sitemap, get_feed, get_page, etc.) is the canonical public URL form, e.g. /posts/my-article/ (or /en/posts/my-article/ for a non-default language).slug on write-tool inputs (create_page, update_page, delete_page, upload_page_asset, delete_page_asset) expects the source-relative source_key form, e.g. posts/my-article — no leading/trailing slashes, no language prefix. (suggest_links, a read-scoped tool, is the exception: its slug input takes the public URL form, matching read-tool outputs.)slug also returns source_key alongside it, in exactly the form the write tools' slug input expects. Feed source_key from a read-tool result straight into a write tool's slug parameter.See docs/mcp-contract.md for the full per-tool field reference.
For a fresh article, the suggested call order is:
list_content_types — confirm the content type and required front matter.suggest_links(tags, categories, body) — run against your draft tags/body before writing, to surface internal-linking candidates while the content is still easy to adjust (#623).create_page — write the page (create_page's own description also cross-references suggest_links as a pre-write step).verify_publication — confirm the change actually went live after a build.When a single logical change spans several pages (e.g. renaming a category across an entire series, or a coordinated cross-linking pass), don't call publish_changes after each page — it triggers a full site build, so publishing once per page instead of once for the whole batch costs a build per page for no benefit and makes a half-applied batch briefly visible on the live site between builds.
The recommended shape (#631):
plan_content_change → review the returned preview/diff → if it looks right, apply_content_plan immediately.
Apply each plan right after previewing it rather than collecting previews for the whole batch first — plan_content_change's plan_id is a single-use preview with a 5-minute TTL (data.plan_expires_at), so a plan-everything-then-apply-everything ordering risks the earliest plans expiring before you get to them on a large batch.plan_id/revision returned for each page as you go — apply_content_plan fails closed with revision_conflict if a page changed since its plan was made, and rollback_change (per page, using the tracked revision) is how you undo any single page in the batch if something downstream turns out wrong.publish_changes once for the whole site.No new orchestration tool is needed for this — apply_content_plan's existing per-page revision pinning and rollback_change's per-page undo already compose into this pattern; a batch-level primitive would just be a wrapper around the same three calls.
read-scoped callers see the same tool set — read carries no additional visibility restriction (#450).write scope is required for mutating and operational tools.write is never exposed to anonymous or read-scoped callers.mcp, reader, content.read, content.write, site.admin, system.admin, ...) are accepted for compatibility, but only read/write are advertised as canonical.Claude Desktop and Claude.ai can connect directly to the public MCP endpoint above.
The server card and OAuth discovery advertise canonical internal scopes only:
readwriteThey also publish additive reader / operator access-profile metadata so
clients can understand the simplified external contract without treating those
profile names as direct OAuth scope strings. (reader's internal_scopes is
now ["read"] and operator's is ["read", "write"].)
Public compatibility discovery for external scanners lives on the website surface as well:
https://www.arleo.eu/auth.mdhttps://www.arleo.eu/.well-known/oauth-protected-resourceThat www surface is served through Hugo static files plus OpenResty, not only
through the Go MCP runtime. The operator recovery notes live in
docs/agent-ready-howto.md.
The repository is expected to pass:
go test ./...
go test -race ./...
go vet ./...
staticcheck ./...
go build ./...
govulncheck ./...
gitleaks detect --no-banner --redact --source .
Production promotion is intentionally split into three explicit stages:
main and wait for CI to go green.Deploy to Production for the exact main commit you want live.Release only after production deployment succeeds. The release workflow refuses to publish unless:
origin/main HEAD;CHANGELOG.md contains the requested version;README.md still uses dynamic latest-release metadata;production deployment record.auth.mdmcp-hugo-server-go is the canonical unified successor of those repositories.
mcp.arleo.eu
├── read (anonymous or self-service token) full content visibility, including drafts
└── write (registered OAuth client only) content creation/editing plus build, site, integrity, and diagnostic operations
The MCP transport is streamable HTTP at /mcp.
To report a vulnerability, set security_contact in your server config (e.g., security_contact: "mailto:security@example.com"). This populates /.well-known/security.txt per RFC 9116. The server requires Contact and Expires — Canonical is set automatically from site_url (or oauth.issuer if site_url is blank).
Agents authenticate via the identity assertion flow:
/agent/identity with {"type":"anonymous"}.oauth.allow_reader_self_registration is enabled, the response is immediately exchangeable at /token (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for a read Bearer token.claim_token + verification_uri; the agent POSTs to /agent/identity/claim, then an operator visits the verification_uri (or POSTs to /agent/identity/verify) with a write Bearer token and the claim_token to approve./token for the configured read token.This flow yields the internal read scope. The published reader / operator
profile language is an external contract layer over the same underlying
read/write scope strings, not a separate mechanism.
The POST to /agent/identity/verify requires operator authentication via the Authorization: Bearer <admin-token> header (or admin_token form field for browser submissions).
| Endpoint | Method | Description |
|---|---|---|
/mcp | GET/POST/DELETE | MCP Streamable HTTP transport |
/.well-known/oauth-authorization-server | GET | OAuth 2.0 authorization server metadata (RFC 8414) |
/.well-known/oauth-protected-resource | GET | Protected resource metadata (RFC 9728) |
/.well-known/mcp/server-card.json | GET | MCP server card |
/.well-known/mcp.json | GET | MCP server card (alias) |
/.well-known/agent.json | GET | Agent card (Google A2A schema) |
/.well-known/security.txt | GET | Security contact (RFC 9116) |
/robots.txt | GET | Robots exclusion |
/llms.txt | GET | LLM discovery |
/auth.md | GET | Authentication guide |
/metrics | GET | Prometheus metrics |
/register | POST | OAuth dynamic client registration |
/authorize | GET/POST | OAuth authorization endpoint |
/token | POST | OAuth token endpoint |
/agent/identity | POST | Register agent identity |
/agent/identity/claim | POST | Initiate agent claim |
/agent/identity/verify | GET/POST | Operator agent approval page |
/agent/event/notify | POST | Agent event notifications |
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows