hehrmann.com for developers and agents
Everything on this site is public, read-only, and available by machine. This page lists the doors, what comes back through each one, and the rules that keep the service predictable.
Start here
Three requests cover most of what an agent needs.
# Any page as Markdown
curl -H "Accept: text/markdown" https://hehrmann.com/services/
# The MCP server's tools
curl -X POST https://hehrmann.com/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-11-25" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Liveness
curl https://hehrmann.com/api/health
No API key, no OAuth, no account. The access model is spelled out in auth.md, and the short version is that nothing here takes a token because nothing here writes.
MCP server
https://hehrmann.com/mcp speaks the Model Context Protocol over Streamable HTTP. It is stateless and read-only: four tools, one resource per page, no session id, no authentication. Protocol versions 2026-07-28 (per-request _meta) and 2025-03-26 through 2025-11-25 (the initialize handshake) are both served. The server card at /.well-known/mcp/server-card.json describes the server, its tools, and their schemas; the same document answers at /mcp/server-card.
| Tool | What it does | Arguments |
|---|---|---|
list_pages | Every public page with its canonical URL and Markdown twin | optional section, for example notes |
get_page | One page as Markdown | path, for example /services/ |
search_site | Full-text search across every page and note, with the URL to cite | query, optional limit (1 to 8) |
contact_instructions | How to reach Erik on a user’s behalf; sends nothing | optional purpose: discovery-call, engagement, or question |
Client configuration, in the shape most MCP clients accept:
{
"mcpServers": {
"hehrmann-com": {
"type": "streamable-http",
"url": "https://hehrmann.com/mcp"
}
}
}
REST endpoints
The OpenAPI 3.1 description lives at /api/openapi.json. The RFC 9727 catalog at /.well-known/api-catalog lists this surface and the MCP server, and every page response carries an RFC 8288 Link header naming the catalog, llms.txt, the capability manifest, and the page’s own Markdown twin.
| Path | Method | Returns |
|---|---|---|
/{page}/ | GET | HTML, or Markdown for Accept: text/markdown |
/{page}/index.md | GET | The Markdown twin directly |
/llms.txt | GET | Agent guide with links to every entry point |
/llms-full.txt | GET | Every page in one plain-text file |
/pricing.md | GET | The fee table for the seven engagements |
/sitemap.xml | GET | Canonical URL list |
/api/health | GET | Liveness, JSON |
/mcp | POST | JSON-RPC 2.0, MCP |
/.well-known/api-catalog, /.well-known/ai-catalog.json, /.well-known/agent-skills/index.json, /.well-known/mcp/server-card.json | GET | Discovery documents |
Markdown for every page
Four routes lead to the same Markdown twin of a page, so whichever convention an agent tries first works:
Accept: text/markdownon the page URL, the negotiated route; both representations carryVary: Accept.?mode=agenton the page URL, for clients that cannot set headers.- The page URL plus
index.md, for examplehttps://hehrmann.com/services/index.md. - The page URL with
.mdappended, for examplehttps://hehrmann.com/services.mdorhttps://hehrmann.com/notes/timesheets.md.
Each Markdown document opens with a frontmatter block (title, description, canonical, last-updated), so the metadata arrives without scraping. The machine documents take .md as well: /api/openapi.json.md, /api/health.md, or /.well-known/api-catalog.md return the same JSON inside a fenced block with a heading and the canonical URL.
Three scoped indexes cover one area each, in the llms.txt shape: /developers/llms.txt for this page, /services/llms.txt for the engagements and fees, and /notes/llms.txt for the case studies.
Errors
HTTP-level errors on /api/*, /mcp, and /.well-known/* come back as RFC 9457 problem details with Content-Type: application/problem+json:
{
"type": "https://hehrmann.com/developers/#error-not-found",
"title": "Not Found",
"status": 404,
"detail": "No endpoint at /api/nope. The OpenAPI description at /api/openapi.json lists what exists; /developers/ explains it.",
"instance": "/api/nope",
"see": "https://hehrmann.com/api/openapi.json"
}
type is stable and safe to branch on, detail is for people, and extension members such as see, allow, and available point at the recovery. Inside the MCP endpoint, protocol errors stay JSON-RPC error objects, as the protocol requires. Page URLs keep their own 404: a short Markdown recovery body for agents and the designed HTML page for browsers. The types:
bad-request
Status 400. The request could not be read.
not-found
Status 404. No document or endpoint at that path. see names the catalog that lists what exists.
method-not-allowed
Status 405. Wrong HTTP method. The Allow header and the allow member list the accepted ones.
not-acceptable
Status 406. The Accept header ruled out every representation of a page. available lists them; a wildcard in Accept avoids this.
payload-too-large
Status 413. An MCP request body over 64 KiB.
rate-limited
Status 429. The request budget below is used up. Retry-After says when to try again.
service-unavailable
Status 503. The asset layer behind the site did not answer; /api/health reports the same condition.
Rate limits
/api/* and /mcp allow 100 requests per minute per client IP, counted by the edge instance that serves the connection (a client that opens a new connection per request can land on several instances, each with its own count; Cloudflare’s own protections cover the aggregate). Every response on those paths carries the IETF rate-limit fields, and a 429 adds Retry-After:
RateLimit-Policy: "api";q=100;w=60
RateLimit: "api";r=97;t=41
Read r (requests left) and t (seconds until the window resets) as the count of the instance that answered, pace to them, and treat a 429 with Retry-After as the authoritative signal. Pages, llms.txt, llms-full.txt, and the discovery documents are not metered; Cloudflare’s edge protections apply to everything. One request per page is enough, and llms-full.txt replaces crawling.
Versioning and deprecation
The REST surface is version 1 and carries no version in its paths on purpose: nothing in it has changed shape since launch, and additive changes (a new field, a new endpoint) ship without notice. A breaking change would get a new path such as /api/v2/, and the old one would keep answering for at least 180 days with Deprecation (RFC 9745) and Sunset (RFC 8594) headers pointing at the replacement, announced here and in the info.description of the OpenAPI document. The MCP server lists its protocol versions in the server card and adds new ones next to old ones; a version is retired the same way, 180 days after its Sunset date is published.
Sandbox
There is none, because there is nothing to protect: every endpoint is read-only, idempotent, and safe to call against production as often as the rate limit allows. The contact form is the one write path on the site, a person submits it, and the contact_instructions tool and the contact-erik-hehrmann skill explain how to hand a user to it.
Source, skills, and plugins
The two agent skills that describe this site, use-hehrmann-com and contact-erik-hehrmann, are published at /.well-known/agent-skills/index.json and mirrored in the public repository ehehrmann/hehrmann-agent-skills, which also carries an AGENTS.md for coding agents and an Agent Plugins manifest that bundles the MCP server. Install the skills with:
npx skills add ehehrmann/hehrmann-agent-skills
The site itself is a Hugo build behind a Cloudflare Worker, and that repository stays private.
Status and contact
Liveness: /api/health. Questions about any of this go to erik@hehrmann.com. Privacy notes for the site are at /privacy/; the fee table for people is on /services/.