Table of contents
Hugo has two ways to put an MCP server card at /.well-known/mcp/server-card.json, and one of them can build cleanly while writing the file to the wrong place. This guide covers the card for check P2 on Hugo. I built a site on Hugo v0.167.0 and ran both routes, so every path and output below comes from a real build.
The card is the JSON document that tells an agent where your MCP server lives. Its schema is still a draft, so read Publish an MCP Server Card So Agents Can Find Your Tools for the fields. This post is only the Hugo mechanics for check P2, which AIScan grades.
Quick summary
| If you want... | Do this | Time |
|---|---|---|
| A fixed card, edited by hand | Put the file in static/.well-known/mcp/server-card.json | 5 min |
A card built from hugo.toml values | Add a ServerCard output format, a section page with url: /.well-known/mcp/ and a layout | 15 min |
| To know which one wins when both exist | The generated one (tested below) | 1 min |
Check first: run npx aiscan-cli yoursite.com and read check P2 in the Capabilities group.
Option 1: a file in static/
Hugo copies everything in static/ to public/ on build, and the directory structure docs list robots.txt and ownership-verification files as typical contents. A dot-prefixed folder is no exception. Create:
static/.well-known/mcp/server-card.json
{
"serverInfo": { "name": "example-mcp", "version": "1.0.0" },
"description": "Search the example catalog",
"transport": { "type": "streamable-http", "url": "https://example.com/mcp" },
"tools": [{ "name": "search", "description": "Search the catalog" }]
}
Replace the example values with your real server name, endpoint and tools. The field names follow the shape in AIScan's own P2 guidance and are a convention, not a ratified schema. Running hugo produced public/.well-known/mcp/server-card.json with no flags and no config.
Option 2: generate the card from hugo.toml
Use this when the endpoint or version lives in your config and you do not want a second copy to drift. Hugo's output formats let one page render as JSON at a path you choose.
1. Add the values and the format to hugo.toml:
[params.mcp]
name = "example-mcp"
version = "1.0.0"
description = "Search the example catalog"
endpoint = "https://example.com/mcp"
[outputFormats.ServerCard]
mediaType = "application/json"
baseName = "server-card"
isPlainText = true
notAlternative = true
application/json is a built-in media type, so I did not need a [mediaTypes] block. Removing it changed nothing.
2. Create content/mcp/_index.md:
---
title: "MCP server card"
url: /.well-known/mcp/
outputs: ["ServerCard"]
---
3. Create layouts/mcp/list.servercard.json:
{{- $m := site.Params.mcp -}}
{{- dict "serverInfo" (dict "name" $m.name "version" $m.version) "description" $m.description "transport" (dict "type" "streamable-http" "url" $m.endpoint) "tools" (slice (dict "name" "search" "description" "Search the catalog")) | jsonify (dict "indent" " ") -}}
Build with hugo. The card appeared at public/.well-known/mcp/server-card.json. jsonify sorts keys alphabetically, so description comes before serverInfo, which does not matter to a parser. It also escapes & and < as & and <, and a JSON parser reads them back as the original characters. With --minify the file shrank to one line and stayed valid.
The mistake that builds cleanly
Leave out the url: line and Hugo builds without a warning, but writes the card to public/mcp/server-card.json. The path AIScan checks returns 404 and nothing in the build output tells you. Always list public/.well-known/mcp/ after a build.
If you use both
I put a different file in static/ at the same path, then built with the generated version in place. The generated card won and the static one was not used. Delete one of them so a future reader does not edit the wrong file.
Publish, then test the live domain
Hugo writes files and sets no headers. The content-type your visitors see comes from the host in front of public/, and I did not test any host. Deploy, then request the card on the domain agents will use.
Verify the card
Run AIScan first:
npx aiscan-cli yoursite.com
Find P2 · MCP Server Card in the Capabilities group. It should pass. By hand, one request does the same job:
curl -si https://yoursite.com/.well-known/mcp/server-card.json
Expected output, trimmed:
HTTP/2 200
content-type: application/json
{"description":"Search the example catalog","serverInfo":{...}
On hugo server, the card answered 200 with Content-Type: application/json, and a misspelled server-carb.json returned 404.
When the card still fails
- 404 at the right path. The file is in
static/well-known/without the leading dot, or the section page has nourl:line. Listpublic/.well-known/mcp/. - No card file, and a warning. Hugo prints
found no layout file for "servercard" for kind "section". The layout is named for the wrong page kind. A section page needslist.servercard.json, andsingle.servercard.jsondoes not match it. - 200 but the body is not JSON. Your host rewrites unknown paths to a page. Look at
content-typeand the first bytes, never only the status. - The card shows old values. You edited
static/while the generated card still exists. Remove one source.
Where to go next
Run npx aiscan-cli yoursite.com, confirm check P2, then browse the other fixes in the AIScan guides. The capabilities checks reference lists P1 to P4 and E2. If you also need llms.txt on Hugo, the llms.txt guide for Hugo uses the same output-format idea, and the Hugo setup guide covers the wider checklist. The Astro version of this guide covers the same check on another static generator.
Frequently asked questions
Where does the MCP server card go in a Hugo project?
Put it at static/.well-known/mcp/server-card.json, or generate it with a ServerCard output format and a section page whose url is /.well-known/mcp/. Both build to public/.well-known/mcp/server-card.json.
Does Hugo copy a folder that starts with a dot?
Yes. On Hugo v0.167.0 a static/.well-known/ folder was copied to public/.well-known/ with no configuration.
Why does my build succeed but the card path return 404?
The section page probably has no url line, so Hugo wrote the card to public/mcp/server-card.json. Add url: /.well-known/mcp/ to the front matter and list public/.well-known/mcp/ after the build.
Why does Hugo warn that it found no layout file for servercard?
The layout is named for the wrong page kind. A section page needs layouts/mcp/list.servercard.json. A file called single.servercard.json is not used for it, and no card is written.
Why does the card URL return 200 but AIScan still fails check P2?
Your host may answer unknown paths with an HTML page and a 200 status. Check that content-type is application/json and that the body starts with a JSON object.
What happens if I have both a static file and a generated card?
On Hugo v0.167.0 the generated card won and the static file at the same path was not used. Delete one source so nobody edits the wrong one.
Why do I see \u0026 in my generated card?
The jsonify function escapes ampersands and angle brackets. A JSON parser reads them back as the original characters, so the card is still valid.
Which fields should the card contain?
The shape in AIScan's P2 guidance uses serverInfo, description, transport and tools. The schema is still a draft, so treat the field names as a convention and keep the card small and accurate.
Related guides
UCP profiles: Shopify serves one, most other stores don't
Shopify stores answer on /.wellknown/ucp. Almost nobody else does. We checked 238 commerceactive sites in the AIScan corpus on 10 October 2026: 12 of 17 Shopify catalogue stores pass check M1 the…
How to publish /.well-known/mcp/server-card.json on Docusaurus
Docusaurus serves anything in static/ asis, so the MCP server card needs one file and no plugin. This guide covers check P2 on Docusaurus. I built a site on Docusaurus 3.10.2 and ran the dev server…
How to Test Whether an AI Agent Can Complete a Task on Your Site
Finding an MCP endpoint is the easy half. The half that matters is whether an agent can finish a job with it: ask a question, get an answer it can parse, and act on it. A server can answer tools/list…
How to publish /.well-known/mcp/server-card.json on Lovable
Lovable builds two kinds of project, and an MCP server card lives in a different place in each. Get the template wrong and you either edit a file that never ships or create a route that answers 404.…
