---
title: "How to publish /.well-known/mcp/server-card.json on Hugo"
slug: mcp-server-card-hugo
published: 2026-10-08T13:20:30.042168+00:00
updated: 2026-10-08T13:20:42.768357+00:00
author: "AIScan Content Team"
author_url: https://aiscan.site/about
category: "AI Readiness"
tags: MCP, server card, Hugo, well-known, check:P2, platform:hugo
description: "Publish /.well-known/mcp/server-card.json on Hugo: a file in static/ or a card generated from hugo.toml, plus the build mistake that hides it, with real output."
url: https://aiscan.site/blog/mcp-server-card-hugo
---

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](https://aiscan.site/blog/publish-mcp-server-card) for the fields. This post is only the Hugo mechanics for check P2, which [AIScan grades](https://aiscan.site/).

## 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](https://gohugo.io/getting-started/directory-structure/) 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**

```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](https://gohugo.io/configuration/output-formats/) let one page render as JSON at a path you choose.

**1. Add the values and the format to hugo.toml:**

```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:**

```yaml
---
title: "MCP server card"
url: /.well-known/mcp/
outputs: ["ServerCard"]
---
```

**3. Create layouts/mcp/list.servercard.json:**

```go-html-template
{{- $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:

```bash
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:

```bash
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 no `url:` line. List `public/.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 needs `list.servercard.json`, and `single.servercard.json` does not match it.
- **200 but the body is not JSON.** Your host rewrites unknown paths to a page. Look at `content-type` and 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](https://aiscan.site/guides). The [capabilities checks reference](https://aiscan.site/docs/checks/capabilities) lists P1 to P4 and E2. If you also need llms.txt on Hugo, [the llms.txt guide for Hugo](https://aiscan.site/blog/llms-txt-hugo) uses the same output-format idea, and the [Hugo setup guide](https://aiscan.site/blog/ai-readiness-setup-hugo) covers the wider checklist. The [Astro version of this guide](https://aiscan.site/blog/mcp-server-card-astro) covers the same check on another static generator.

