Canonical unified MCP server for Hugo sites.
Public endpoint: https://mcp.arleo.eu/mcp
ChatGPT compatibility notice (2026-08-20): the remote server remains MCP/OAuth interoperable, but ChatGPT Plus currently stops after a successful OAuth exchange and
initializeresponse without sendingnotifications/initializedortools/list; no actions appear in ChatGPT. This was reproduced with read-only, write, and admin grants and unchanged binaries from v1.8.8 through v1.9.2, while MCPJam completed the same DCR + PKCE + MCP flow and loaded 32 tools. ChatGPT remains in the compatibility matrix because other eligible plans may still work; the incident is scoped to the tested Plus account. OpenAI's current documentation is also inconsistent: the developer guide lists Plus as eligible for full developer-mode MCP, while the Help Center reserves full MCP to Business/Enterprise/Edu, gives Pro read/fetch-only access, and does not list Plus. See the client compatibility matrix for the evidence and current status. This is a ChatGPT plan/client availability warning, not a server-side protocol failure.
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.
The badge counts scc code lines from Git-tracked production .go files. It excludes comments, blank lines, untracked files, and every *_test.go file. CI recalculates and publishes the badge payload automatically after every push to main; contributors never edit the count in this README.
The badge reports the same go test -cover ./internal/... total the CI coverage gate enforces (currently a minimum of 85%; a run failing that gate never reaches the badge-publish step, so the badge can't advertise a number CI itself rejected). Published automatically after every push to main, same mechanism as the LOC badge above.
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-mcpfor public discovery, OAuth, andauth.mdhugo-mcp-gofor content and administration toolsmcp-runtime-gofor MCP transport/runtime behavior
The same binary supports two transport modes. Pick based on who you are — they are not interchangeable and exist for different purposes.
Use this if you manage your own Hugo site and want an MCP-capable client (Claude Desktop, or any other MCP host that can launch a local subprocess) to edit it directly on your machine. No OAuth, no server process to run or expose — the client launches the binary itself and talks to it over stdin/stdout.
Via npx/npm (npm/ in this repository — package README): downloads and checksum-verifies the matching release binary automatically, no manual OS/arch selection needed.
npx @jmrgrav/mcp-hugo-server-goOr download the binary directly:
- Download the
mcp-hugo-server-gobinary for your OS/arch from the latest release. - Configure it either via a
config.yaml(see docs/operator-guide.md for the full field reference) withtransport: stdio, or — if your MCP host can only inject environment variables, not a config file, which is the case for MCPB-style desktop extension installs — via theMCP_HUGO_SITE_ROOT/MCP_HUGO_HUGO_ROOT/MCP_HUGO_CONTENT_ROOT/MCP_HUGO_SITE_URL/MCP_HUGO_SITE_NAMEenvironment variables instead. A file's values always win; env vars only fill in whatever the file (or an absent file) leaves empty. - Point your MCP host at the binary as its launch command. See
manifest.jsonin this repository for the shape a desktop-extension host expects. - See Privacy policy below for exactly what this mode does and does not do with your data — nothing leaves your machine by default.
Or use the packaged .mcpb desktop extension: download the .mcpb file attached to the latest release and double-click it (or drag it into Claude Desktop's Settings → Extensions) — Claude Desktop prompts for the same user_config fields (site_root/hugo_root/content_root/site_url/site_name) described above. Submitted to the Claude Connectors Directory; not yet listed there pending review, but installable manually today via the release download. See the wiki's Installation Guide for a deeper walkthrough of all three install paths side by side.
Use this if you want a persistent, remotely-reachable instance — e.g. to let an agent running somewhere else (not on the machine with your Hugo site) manage the site, or to share access across multiple OAuth clients with read/write scoping. This is how this project's own instance at https://mcp.arleo.eu/mcp runs. It requires you to run and expose the server yourself (reverse proxy, TLS, OAuth client registration) — see docs/operator-guide.md for a full deployment walkthrough. This is a materially higher setup cost than stdio and is meant for that more advanced use case, not the default choice.
The server enforces exactly three internal scopes (#450, extended by #1039/#1050):
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 oldreaderprofile used).write: requires a registered OAuth client (client_id+client_secret). Impliesread— awritetoken gets everything, including build/site/integrity/ diagnostic operations that used to require a separatesite.adminscope.admin: requires an explicitly approved administrator OAuth client. Implieswriteand additionally gates the four managed Hugo binary lifecycle tools (stage_hugo_upgrade,activate_hugo,rollback_hugo,bootstrap_hugo).
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/admin via oauth.CanonicalScope
(site.admin/system.admin resolve to admin), but only read, write, and
admin 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.
Tool payloads use two different shapes for the same page identity, and mixing them up is a recurring source of confusion (#610):
slugon 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).slugon write-tool inputs (create_page,update_page,delete_page,upload_page_asset,delete_page_asset) expects the source-relativesource_keyform, e.g.posts/my-article— no leading/trailing slashes, no language prefix. (suggest_links, a read-scoped tool, is the exception: itssluginput takes the public URL form, matching read-tool outputs.)- To avoid reformatting by hand, every read tool that returns
slugalso returnssource_keyalongside it, in exactly the form the write tools'sluginput expects. Feedsource_keyfrom a read-tool result straight into a write tool'sslugparameter.
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-referencessuggest_linksas a pre-write step). If you're publishing every language translation of the article at once, usecreate_bundleinstead — it writes every translation atomically, so a validation failure on any one leaves no partial bundle on disk (#1038).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):
- For each page:
plan_content_change→ review the returned preview/diff → if it looks right,apply_content_planimmediately. Apply each plan right after previewing it rather than collecting previews for the whole batch first —plan_content_change'splan_idis 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. - Track the
plan_id/revisionreturned for each page as you go —apply_content_planfails closed withrevision_conflictif a page changed since its plan was made, androllback_change(per page, using the tracked revision) is how you undo any single page in the batch if something downstream turns out wrong. - Once every page in the batch has been applied, call
publish_changesonce 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.
This is the pattern for a change spanning several distinct pages. For editing every language translation of the same page bundle together, use the bundle-scoped tools instead (plan_bundle_change → apply_bundle_plan, or create_bundle/delete_bundle for whole-bundle create/delete) — they validate every translation before writing any, so a single atomic apply replaces what would otherwise be several separately-racing plan_content_change/apply_content_plan calls against sibling files (#854).
If several clients share the same OAuth principal and need their own in-flight batches tracked and published independently, call create_change_set once per batch and pass the returned change_set_id on every mutation and on publish_changes (#1135).
- Anonymous callers and
read-scoped callers see the same tool set —readcarries no additional visibility restriction (#450). - Reader-facing discovery is provider-neutral: capability differences depend on token trust, not on whether the client is ChatGPT, Claude, Gemini, Le Chat, Copilot, or another MCP consumer.
- An OAuth bearer token with
writescope is required for mutating and operational tools. writeis never exposed to anonymous orread-scoped callers.- An OAuth bearer token with
adminscope is required for the four managed Hugo binary lifecycle tools (stage_hugo_upgrade,activate_hugo,rollback_hugo,bootstrap_hugo);adminis never exposed toread- orwrite-scoped callers. - Legacy scope aliases (
mcp,reader,content.read,content.write,site.admin,system.admin, ...) are accepted for compatibility, but onlyread/write/adminare advertised as canonical.
This section applies specifically to the stdio transport (transport: stdio) — the mode used for a local, single-user install such as an MCPB desktop extension. It does not describe the operator-run mcp.arleo.eu HTTP+OAuth deployment, which is a separate, self-hosted service with its own operational practices.
Data collection: none. This server does not collect, transmit, or store any usage data, telemetry, or analytics about you or your content.
Data processing: all reads and writes happen entirely on your own machine, against the Hugo site directories (site_root/hugo_root/content_root) you configure. Content you create, edit, or delete through this server never leaves your machine as part of that operation.
External network calls — none by default, opt-in only: the base install (no optional config fields set) makes exactly one kind of external-adjacent call: invoking your local hugo CLI as a subprocess to build your site, which itself does not require network access. A handful of optional, individually-configured integrations do call external services, and only run if you explicitly set the corresponding config field:
| Feature | Config field | External service called |
|---|---|---|
| Post-build webhooks | post_build_hooks |
Whatever URL(s) you configure |
| AI hero-image generation | image_gen_url / image_gen_key |
Whatever image-generation API you configure |
| Preview ingress verification | preview_external_verification |
The operator-configured OAuth issuer (/preview/ only) |
| Cloudflare cache purge | cloudflare.* |
Cloudflare's API |
| IndexNow search-engine ping | indexnow.* |
IndexNow's API (or your configured endpoint) |
| Google Search Console indexing | google_indexing.* |
Google's Indexing API |
None of these are set by default. If you never configure them, this server makes no outbound network calls at all beyond running your local hugo build.
Data retention: any local state this server keeps (SQLite indexes, rate-limit counters, idempotency keys) lives entirely in files you control (db_path, etc.) on your own machine, and is deleted whenever you delete those files.
Third parties: none, beyond the optional integrations you explicitly configure above, each of which is subject to that third party's own privacy practices.
Contact: see Security contact below, or open an issue on this repository.
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:
readwrite
They 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
["read"] and operator's is ["write"] — write implies read, so no
second entry is needed.)
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-resource
That 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 a single manual workflow_dispatch on Deploy to Production. Every run, release or not, refuses to proceed unless the target ref is reachable from (an ancestor of) origin/main — production deploys must target a real main commit.
- Merge to
mainand wait forCIto go green. - Run
Deploy to Productionfor the exact ref you want live, withrelease_versionleft empty for an ad-hoc mainline deploy (e.g. a hotfix ahead of the next release) — this builds, tests, deploys, and smoke-tests, with no tag or GitHub release involved. - To cut a real release instead, first merge the release-notes PR (the
CHANGELOG.mdentry and matchingnpm/package.json/manifest.jsonversion bumps, plusREADME.mdstill using dynamic latest-release metadata rather than a hardcoded version — this workflow deliberately never writes that content itself), then runDeploy to Productionagain withrelease_versionset (e.g.v1.9.0). It refuses to proceed unless all of that is already true on the target ref; once the deploy succeeds it tags, publishes the GitHub release, attaches binaries and the.mcpbbundle, and publishes to npm — one call produces a fully consistent release.
A standalone Release workflow existed previously; it has since been absorbed into Deploy to Production (see the workflow's own header comment for the full rationale).
- hugo-public-mcp - public agent-ready discovery, OAuth, and
auth.md - hugo-mcp-go - Hugo content and administration tools
- mcp-runtime-go - MCP runtime and transport foundation
mcp-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 POSTs to
/agent/identitywith{"type":"anonymous"}. - If
oauth.allow_reader_self_registrationis enabled, the response is immediately exchangeable at/token(grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for areadBearer token. - If self-registration is disabled, the response includes
claim_token+verification_uri; the agent POSTs to/agent/identity/claim, then an operator visits theverification_uri(or POSTs to/agent/identity/verify) with awriteBearer token and theclaim_tokento approve. - The approved assertion then exchanges at
/tokenfor 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 |
/openapi.json |
GET | OpenAPI 3.1.0 document (OAuth/discovery endpoints only — the MCP tool catalog is dynamic and not enumerated here) |
/health |
GET | Liveness probe |
/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 |