ITADN
jmrGrav/mcp-hugo-server-go
README.md

mcp-hugo-server-go

Go Version Latest Release CI Deploy to Production License: MIT Security Policy MCP MCP stdio npx Claude Desktop ChatGPT Claude Le Chat Agent Ready

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.

What it does

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:

Installation

The same binary supports two transport modes. Pick based on who you are — they are not interchangeable and exist for different purposes.

Local, single-user (most people): stdio transport

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-go

Or download the binary directly:

  1. Download the mcp-hugo-server-go binary for your OS/arch from the latest release.
  2. Configure it either via a config.yaml (see docs/operator-guide.md for the full field reference) with transport: 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 the MCP_HUGO_SITE_ROOT / MCP_HUGO_HUGO_ROOT / MCP_HUGO_CONTENT_ROOT / MCP_HUGO_SITE_URL / MCP_HUGO_SITE_NAME environment variables instead. A file's values always win; env vars only fill in whatever the file (or an absent file) leaves empty.
  3. Point your MCP host at the binary as its launch command. See manifest.json in this repository for the shape a desktop-extension host expects.
  4. 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.

Shared/remote, multi-user (advanced): HTTP + OAuth transport

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.

Access model

The 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.

Tool inventory

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 formats: slug vs source_key

Tool 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.)
  • To avoid reformatting by hand, every read tool that returns 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:

  1. list_content_types — confirm the content type and required front matter.
  2. 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).
  3. create_page — write the page (create_page's own description also cross-references suggest_links as a pre-write step).
  4. verify_publication — confirm the change actually went live after a build.

Multi-page editorial changes

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):

  1. For each page: 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.
  2. Track the 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.
  3. Once every page in the batch has been applied, call 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.

Security model

  • Anonymous callers and read-scoped callers see the same tool set — read carries 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 write scope is required for mutating and operational tools.
  • write is never exposed to anonymous or read-scoped callers.
  • Legacy scope aliases (mcp, reader, content.read, content.write, site.admin, system.admin, ...) are accepted for compatibility, but only read/write are advertised as canonical.

Privacy policy

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:

FeatureConfig fieldExternal service called
Post-build webhookspost_build_hooksWhatever URL(s) you configure
AI hero-image generationimage_gen_url / image_gen_keyWhatever image-generation API you configure
Cloudflare cache purgecloudflare.*Cloudflare's API
IndexNow search-engine pingindexnow.*IndexNow's API (or your configured endpoint)
Google Search Console indexinggoogle_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 and MCP

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:

  • read
  • write

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 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.md
  • https://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.

Validation

The repository is expected to pass:

go test ./...
go test -race ./...
go vet ./...
staticcheck ./...
go build ./...
govulncheck ./...
gitleaks detect --no-banner --redact --source .

Release flow

Production promotion is intentionally split into three explicit stages:

  1. Merge to main and wait for CI to go green.
  2. Run Deploy to Production for the exact main commit you want live.
  3. Run Release only after production deployment succeeds. The release workflow refuses to publish unless:
    • the requested ref resolves to the current origin/main HEAD;
    • CHANGELOG.md contains the requested version;
    • README.md still uses dynamic latest-release metadata;
    • the target SHA already has a successful production deployment record.

Project lineage

mcp-hugo-server-go is the canonical unified successor of those repositories.

Architecture

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.

Security contact

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).

Agent identity flow

Agents authenticate via the identity assertion flow:

  1. Agent POSTs to /agent/identity with {"type":"anonymous"}.
  2. If 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.
  3. If self-registration is disabled, the response includes 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.
  4. The approved assertion then exchanges at /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).

API reference

EndpointMethodDescription
/mcpGET/POST/DELETEMCP Streamable HTTP transport
/.well-known/oauth-authorization-serverGETOAuth 2.0 authorization server metadata (RFC 8414)
/.well-known/oauth-protected-resourceGETProtected resource metadata (RFC 9728)
/.well-known/mcp/server-card.jsonGETMCP server card
/.well-known/mcp.jsonGETMCP server card (alias)
/.well-known/agent.jsonGETAgent card (Google A2A schema)
/.well-known/security.txtGETSecurity contact (RFC 9116)
/robots.txtGETRobots exclusion
/llms.txtGETLLM discovery
/auth.mdGETAuthentication guide
/metricsGETPrometheus metrics
/registerPOSTOAuth dynamic client registration
/authorizeGET/POSTOAuth authorization endpoint
/tokenPOSTOAuth token endpoint
/agent/identityPOSTRegister agent identity
/agent/identity/claimPOSTInitiate agent claim
/agent/identity/verifyGET/POSTOperator agent approval page
/agent/event/notifyPOSTAgent event notifications

Documentation