# A resume for two audiences

By Derik Callahan · 2026-10-16 · https://derikcallahan.com/writing/a-resume-for-two-audiences

> More and more, the first reader of a resume is an AI agent. So I built this site to be read by both people and agents — Markdown twins, llms.txt, JSON Resume, content negotiation, and a small read-only MCP server.

Recruiters, sourcing tools, and hiring managers increasingly put an AI between themselves and a resume. The model reads it first and summarizes it for the person. So I built this site for two readers: a person skimming in a browser, and an agent reading it on that person's behalf.

## The rule: one source, many renderings

Everything about me lives in one typed data file and a folder of Markdown posts. Every surface is built from those two inputs at build time:

- **HTML pages** for people.
- **A Markdown twin of every page.** Add `.md` to any URL, for example [/resume.md](/resume.md) or [/writing/the-agent-is-an-identity.md](/writing/the-agent-is-an-identity.md). Each page also announces its twin with `<link rel="alternate" type="text/markdown">`.
- **[/llms.txt](/llms.txt)** and **[/llms-full.txt](/llms-full.txt)**, following the llms.txt convention: a short index, and everything in one file for a single fetch.
- **[/resume.json](/resume.json)** in the JSON Resume schema, for tools that want structure instead of prose.
- **schema.org JSON-LD** on every page: `Person`, `ProfilePage`, and `BlogPosting`.
- **Content negotiation.** Request any page with `Accept: text/markdown` and you get the Markdown version at the same URL.

The "Agent view" toggle in the header shows the exact Markdown an agent receives for the page you're on. A person can see the same thing a model sees.

## A read-only MCP server

There's also a small [Model Context Protocol](https://modelcontextprotocol.io) endpoint at `/api/mcp`. It speaks stateless Streamable HTTP and offers a few tools: `get_profile`, `get_resume`, `list_posts`, `get_post`, and `search`. Point an MCP client at it and an agent can query my background directly instead of scraping HTML.

I wrote it by hand without an SDK, which was a deliberate security choice:

- **Read-only by construction.** Every tool returns data that's already public on this site. There's nothing to write, no secrets, and no state between requests.
- **Small and auditable.** It's one file you can read in a few minutes. It parses JSON-RPC, checks the method, and returns content. Unknown methods get a standard error.
- **Bounded.** Request bodies are size-capped, search queries are length-capped, and every response comes from a fixed in-memory corpus. There's no database or outbound call to abuse.

It's the same thing I ask for from any MCP server at work: least privilege, a narrow surface, and nothing that would be a problem if an attacker controlled the input.

## Boring security, done properly

A security leader's website should pass its own review:

- The site is fully static except for the MCP function, so there is very little attack surface.
- Strict headers: a Content-Security-Policy with no `unsafe-inline` scripts, HSTS, `frame-ancestors 'none'`, and a tight Permissions-Policy.
- No third-party scripts, no trackers, and fonts hosted on this domain.
- A [security.txt](/.well-known/security.txt) so researchers know how to reach me.
- `robots.txt` explicitly welcomes AI crawlers. On this site, I want to be read.

The [source is on GitHub](https://github.com/dcallahan/resumewebsite). Every change goes through a pull request with a Vercel preview build before it ships.
