OPEN TO FULL-TIME ROLES (Available to start immediately. Remote preferred worldwide.)// STACK:WORDPRESS & WOOCOMMERCEREACT & NEXT.JSTYPESCRIPTWEB PERFORMANCEAI INTEGRATIONSAUTOMATION (N8N, MAKE)// PROTOCOL: MCP · JSON-RPC 2.0// STATUS: PRODUCTION READY
OPEN TO FULL-TIME ROLES (Available to start immediately. Remote preferred worldwide.)// STACK:WORDPRESS & WOOCOMMERCEREACT & NEXT.JSTYPESCRIPTWEB PERFORMANCEAI INTEGRATIONSAUTOMATION (N8N, MAKE)// PROTOCOL: MCP · JSON-RPC 2.0// STATUS: PRODUCTION READY
HDRX
• 14 min read
AEOMCPProduct EngineeringStructured DataAstro

Agent-Ready Portfolios: Building Developer Sites for MCP, LLMs, and Answer Engines

A technical guide to dual-layer portfolio architecture: MCP servers, llms.txt, agents.md, and structured APIs that make your site legible to AI agents and answer engines, not just human visitors.

A growing share of the traffic hitting developer portfolios isn’t a recruiter scrolling on a phone — it’s an agent. Cursor calling out to verify a claim. A recruiting tool running Claude or GPT-4 to triage candidates against a job description. Perplexity or ChatGPT answering “who is this person and what have they built” on a user’s behalf, without ever rendering your CSS.

Most portfolios are built exclusively for the first kind of visitor. This post covers the architecture for the second — the concrete mechanisms (MCP, llms.txt, agents.md, JSON-LD, structured REST endpoints) that make a site legible to machines, why each one exists, and where the tradeoffs actually bite.

Why Agent-Readability Is a Distinct Engineering Problem

Traditional SEO optimizes for a crawler that fetches HTML, extracts text and links, and indexes it for a ranked list of results a human then clicks through. Agentic crawling is a different consumption model entirely:

  • The agent often has a budget — a fixed number of tool calls, tokens, or page loads before it must produce an answer. Inefficient page structure directly costs the agent (and the user waiting on it) time.
  • The agent frequently needs to synthesize an answer, not just link to a source. If your data isn’t extractable cleanly, the agent either hallucinates a summary or skips you.
  • Some agents execute JavaScript and render the DOM (browser-use style agents); others do not and only fetch raw HTML or hit declared endpoints (most WebFetch-style tools, and virtually all crawlers behind chat products at scale, for cost reasons). Betting your discoverability entirely on client-side rendering excludes the second group outright.
  • Agents increasingly prefer calling a declared tool over parsing prose when one is available, because tool outputs are structured and typed, which removes an entire class of extraction error. This is the actual argument for exposing an MCP server instead of relying on the agent to scrape your About page.

The practical implication: a portfolio’s agent-readability isn’t a single artifact (like adding robots.txt) — it’s an architectural decision that spans rendering strategy, API design, and structured markup.

The Dual-Layer Architecture

This site implements what I call a Dual-Layer Architecture: one rendering path optimized for human perception and interaction, one path optimized for machine consumption, both backed by a single source of truth.

                        ┌────────────────────────┐
                        │      Public Domain      │
                        └────────────┬─────────────┘

                ┌────────────────────┴────────────────────┐
                ▼                                          ▼
       [ Human Layer ]                            [ Agent Layer ]
       • Astro 5 SSG/SSR hybrid                    • Stateless MCP Server (/api/mcp)
       • View Transitions SPA-feel nav             • /llms.txt discovery file
       • Semantic HTML + JSON-LD                   • /agents.md contribution guide
       • Design system, motion, imagery            • REST endpoints (/api/resume, /api/projects)
                │                                          │
                └────────────────────┬─────────────────────┘

                         /src/data/*.ts (single source of truth)

The critical constraint that makes this maintainable: both layers read from the same typed data module. The MCP server’s get_projects tool and the /projects page component pull from the identical projects.ts array. There is no separate “content for bots” that can drift out of sync with “content for humans” — a common failure mode in sites that bolt on an llms.txt as an afterthought and then never update it after the visible site changes.

Why Astro’s Hybrid Output Model Matters Here

Astro ships zero JavaScript by default and renders to static HTML at build time, with the option to opt specific routes into server rendering (output: "hybrid" with per-route export const prerender = false). For agent-readability this matters for a boring but decisive reason: content that exists in the initial HTML response is visible to every consumer, regardless of whether they execute JavaScript. A React SPA that mounts content client-side is invisible to any agent doing a plain fetch() — which, for cost and latency reasons, is the majority of them.

React islands (client:load, client:visible) are used here only for actual interactivity — the project filter, the chat widget — never for content that needs to be discoverable. That’s not a performance optimization first; it’s an information-architecture decision that happens to also improve Core Web Vitals.

Discovery Files: llms.txt and agents.md

What does llms.txt actually do?

llms.txt is a proposed convention — not a W3C or IETF standard, and not universally honored — for a plain-Markdown file at the site root that gives an LLM a curated, high-signal summary of the site: what it is, the key pages, and links to more detail. It’s the machine-readable analog of a sitemap, but written as prose/Markdown for a model to consume directly rather than as XML for a crawler to enumerate.

Its actual utility today is mixed and worth being honest about:

Aspect Reality
Adoption by major AI products Inconsistent — some crawlers fetch it, most general-purpose search/answer engines do not currently prioritize it the way they do robots.txt or sitemaps
Cost to implement Near zero — one static Markdown file
Failure mode if wrong Silent — no consumer enforces schema, so a stale file just quietly misleads whichever agent does read it
Best current use case Agent tooling that explicitly checks for it (coding agents, MCP clients, dev-tool integrations) rather than mainstream consumer AI search

Given the low cost and the optionality, it’s worth shipping — but don’t treat it as a guaranteed discovery channel. It’s a hedge, not infrastructure.

agents.md — the machine-facing README

agents.md (also seen as AGENTS.md) has better real-world traction because coding agents (Claude Code, Cursor, Copilot Workspace, etc.) actively look for it as an instruction file when operating inside a repository or against a project. On a portfolio, its role is narrower than the repo-level convention: it documents how an agent should interact with the site’s agent layer — the MCP endpoint URL, the tool names and their input schemas, rate limits, and any side-effecting operations that require care.

Practical content worth including:

  • The MCP transport endpoint and protocol version.
  • A list of available tools with one-line descriptions (the full schema is discoverable via the protocol itself — don’t duplicate it and risk drift).
  • Which operations are read-only versus which have side effects (this site has exactly one side-effecting tool: contact, which sends an email — everything else is read-only by design).
  • Rate limits and expected error behavior, so an integrating agent doesn’t need to reverse-engineer failure modes.

Do I need both files?

Yes, but they serve different consumers. llms.txt targets an agent doing general-purpose reading/summarization of your site. agents.md targets an agent (or developer) that wants to act — call your tools, integrate against your API. Treat them as a discovery layer and an integration-contract layer, respectively, not duplicates of each other.

The MCP Server: Exposing a Portfolio as Callable Tools

Model Context Protocol (MCP) standardizes how an LLM client (Claude Desktop, Cursor, a custom agent) discovers and calls external tools over a consistent JSON-RPC 2.0 interface, instead of every integration inventing its own bespoke API shape and the model having to be told about it out-of-band via prompt engineering.

Framed as a REST/JSON-RPC comparison, the actual value proposition is:

  • Self-describing. MCP clients call a discovery method (tools/list) and get back typed schemas for every available tool, at runtime, without a human reading API docs and writing a wrapper. A plain REST API requires either an OpenAPI spec plus a compatible client, or a human-written integration.
  • Uniform invocation. Every tool call and response follows the same JSON-RPC envelope regardless of what the tool does internally, which is what lets a single MCP client work against arbitrary MCP servers with zero per-server client code.
  • Model-native. Anthropic and other model providers train and optimize their agent harnesses against the MCP tool-calling pattern specifically, so a portfolio exposed as MCP tools is consumed more reliably by Claude-based agents than the same data exposed as an undocumented REST endpoint the model has to infer semantics for.

Protocol shape: JSON-RPC 2.0 over Streamable HTTP

The server on this site (src/pages/api/mcp.ts) implements the current MCP transport — Streamable HTTP, a single POST endpoint that accepts JSON-RPC 2.0 request objects and can respond either with a single JSON payload or a Server-Sent Events stream, replacing the older dual-endpoint HTTP+SSE transport from earlier protocol revisions. A request looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_projects",
    "arguments": { "tags": ["ai", "mcp"], "featured": true }
  }
}

And the response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      { "type": "text", "text": "{ \"projects\": [ ... ] }" }
    ]
  }
}

Why stateless, and what that actually costs you

MCP servers can be implemented as stateful (holding a session, maintaining conversational context server-side) or stateless (every request is self-contained; no session affinity required). This server is deliberately stateless:

  • Deployability. A stateless handler runs cleanly on Vercel Edge Functions / serverless functions with no session store, no sticky routing, and trivial horizontal scaling — each request can land on any instance.
  • Simplicity of the read-only tools. get_resume, get_projects, get_capabilities, check_availability, and get_manifest have no reason to remember prior calls; each is a pure function over the same src/data/*.ts source of truth used by the human-facing pages.
  • The tradeoff. Stateless design means no server-side conversation memory — any multi-turn context has to be carried by the client (re-sent with each call) or reconstructed from scratch per request. For a portfolio’s tool surface, where each tool is essentially a parameterized read, this cost is negligible. It would not be an acceptable tradeoff for a stateful workflow tool (e.g., a multi-step booking flow), which is why “should my MCP server be stateless” is a per-tool-surface decision, not a blanket rule.

The one tool with a side effect: contact

Every other tool here is read-only by explicit design — an agent evaluating a candidate should never be able to mutate anything. contact is the deliberate exception: it validates and sends an email via a transactional email provider. Because it’s the only mutating surface, it gets disproportionate scrutiny:

  • Strict input caps (name, contact method, and message length bounded server-side, not just client-side).
  • Rate limiting at the endpoint, independent of the rate limiting applied to the rest of the API surface, since a side-effecting tool is the highest-value target for abuse.
  • No stack traces or internal error detail returned on failure — errors are mapped to generic JSON-RPC error codes.

If you’re exposing an MCP server publicly, the read/write split should be visible in your own documentation (agents.md) and enforced in code, not just assumed. An agent operating autonomously on a user’s behalf will call whatever’s callable; the server is the only enforcement point that matters.

MCP vs. plain REST vs. GraphQL — when each makes sense

Plain REST GraphQL MCP
Discovery Requires external docs (OpenAPI, README) Schema introspection Native tools/list, part of the protocol
Best consumer Browsers, other backend services Frontends needing flexible queries LLM agents / MCP-compatible clients
Overhead to add a new operation New route + docs update New schema field/resolver New tool definition, auto-discoverable
Adoption by AI model providers Generic — models have to be told the shape Generic First-class, purpose-built

The honest takeaway: MCP doesn’t replace REST, it targets a different consumer. This site runs both — REST endpoints (/api/resume, /api/projects, /api/availability) for anything that wants a conventional HTTP call (including simpler agents and non-MCP tooling), and the MCP server for clients that speak the protocol natively. Maintaining both from the same data layer is what keeps this from becoming two systems to keep in sync.

Structured Data and Semantic HTML for Answer Engines

MCP and llms.txt cover agents that actively fetch your site or call your tools. A large fraction of AEO-relevant traffic never does either — it’s a search or answer engine (Google’s AI Overviews, Perplexity, Bing Copilot) that crawls conventionally and extracts an answer from the rendered page. That consumer needs different signals.

JSON-LD: the concrete implementation, not just “add structured data”

Schema.org’s Person and ProfilePage types (or JobPosting-adjacent context if the page describes availability for hire) let a crawler extract entities deterministically instead of inferring them from prose. A minimal, accurate example for a portfolio’s about/home page:

{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "Hendrix Garcia",
  "jobTitle": "Full-Stack Product Engineer",
  "url": "https://hdrx.com.br",
  "sameAs": [
    "https://github.com/TBD"
  ],
  "knowsAbout": [
    "React", "Astro", "TypeScript", "Model Context Protocol", "WordPress"
  ]
}

Two things matter more than the schema choice itself:

  1. Accuracy over completeness. Do not populate sameAs, alumniOf, or knowsAbout with anything not actually true — structured data is exactly where automated fact-checking against the visible page content is easiest for a crawler to perform, and a mismatch is a stronger negative signal than simply omitting the field.
  2. JSON-LD doesn’t replace semantic HTML. A <div> soup with a JSON-LD block bolted on is still worse for accessibility, for non-JSON-LD-aware crawlers, and for the actual human reading the page than well-structured <article>, <section>, and heading hierarchy with JSON-LD layered on top as reinforcement.

Answer engines and featured snippets favor content where a direct, self-contained answer sits immediately under a question-phrased heading, rather than being scattered across a paragraph or requiring the reader (human or model) to infer it. Practically, for a portfolio or technical blog:

  • Use H3s phrased as actual questions where genuine search intent is question-shaped (“What is MCP?”, “Does Astro support SSR?”) — not as a keyword-stuffing gimmick, only where it matches real intent.
  • Put the direct answer in the first sentence or two below the heading. Nuance, exceptions, and caveats come after, not interleaved before the answer.
  • Prefer lists and tables for anything inherently structured (comparisons, steps, specs) — extraction models handle these more reliably than prose enumerations.

Heading hierarchy as an SEO and accessibility primitive, not decoration

One H1 per page (the frontmatter title in a Markdown/Astro blog setup), then a clean H2 → H3 → H4 descent with no skipped levels. This is worth restating because it’s routinely violated by content optimized visually rather than structurally (e.g., using an H4 for a subheading because it “looks right” at that size, when semantically it’s a sibling H2). Skipped levels degrade both screen-reader navigation and the heading-based content models many extraction pipelines use to build a page outline.

Traditional Crawling vs. Agent-Based Crawling — the Practical Differences

Dimension Traditional crawler (Googlebot-class) Agent-based crawling (LLM tool use)
Fetch pattern Broad, scheduled, indexes for later ranking On-demand, triggered by a specific user query, budget-constrained
JS execution Increasingly yes (Googlebot renders), but with delay and cost Varies sharply by agent — many skip JS entirely for cost/latency
Preferred data shape HTML + structured data (JSON-LD, meta tags) Structured data if present; otherwise falls back to parsing HTML/Markdown; prefers a declared tool/API if one exists
Success metric for you Ranking position, click-through Whether the agent’s synthesized answer about you is accurate and complete
Robots directives robots.txt, X-Robots-Tag, meta robots — well-established No settled standard yet; llms.txt is the closest analog but not universally honored

The consequence for architecture: optimizing purely for Googlebot (structured data, sitemap, canonical tags) is necessary but not sufficient. Optimizing purely for agent tool-calling (MCP, llms.txt) misses the answer-engine traffic that still crawls conventionally. A portfolio meant to perform well across both needs both layers — which is the entire argument for the dual-layer approach described above, rather than picking one paradigm.

Common Failure Modes and Troubleshooting

An agent summarizes my site inaccurately even though the information is correct on the page

Check whether the relevant content is present in the raw HTML response (curl the URL, don’t just view it in a browser) or only rendered client-side after hydration. If it’s client-rendered, any agent that doesn’t execute JavaScript sees an empty shell. Move the content to a prerendered route or a static component.

My MCP server works in Claude Desktop but a different MCP client fails to connect

Verify the transport. Older MCP clients may still expect the deprecated HTTP+SSE dual-endpoint transport rather than the current Streamable HTTP single-endpoint transport. Confirm both the protocol version your server declares and the client’s supported version explicitly rather than assuming compatibility.

llms.txt and the visible site have drifted out of sync

This is a symptom of treating the discovery file as static content instead of generating it (or at least its data-bearing sections) from the same source (src/data/) that powers the rest of the site. Any manually-maintained secondary copy of your data will eventually diverge; the fix is architectural, not a reminder to “update it more often.”

Structured data validates but doesn’t seem to influence how AI answers describe me

JSON-LD is a signal, not a guarantee — answer engines weigh it alongside prose content, backlinks, and their own extraction heuristics, and none of that weighting is published or stable. No provider publishes a formula tying JSON-LD presence to inclusion in a generated answer, so treat structured data as a hygiene factor that removes ambiguity, not as a lever with a predictable, measurable effect size.

Rate limiting on /api/agent or the MCP contact tool is too aggressive and blocking legitimate agent traffic

Distinguish rate limits by endpoint risk, not a single global limit. Read-only tools (get_resume, get_projects) can tolerate higher-frequency calling from legitimate integrations; the side-effecting contact tool should stay tightly bounded regardless, since its abuse cost (spam, email quota exhaustion) is categorically different from a read being called too often.

Technical FAQ

What is the difference between MCP and a REST API?

MCP is a protocol built on JSON-RPC 2.0 that standardizes tool discovery and invocation for LLM clients — a client can call tools/list and get typed schemas for every available operation at runtime. A plain REST API has no equivalent self-description mechanism; a model or developer needs external documentation (or an OpenAPI spec plus tooling) to know what’s callable and how.

Does llms.txt improve Google search rankings?

No. llms.txt is not a recognized signal in Google’s traditional search ranking algorithm. It’s aimed at LLM-based tools that choose to read it, which is a separate channel from conventional SEO ranking. Traditional SEO fundamentals (structured data, semantic HTML, page speed, backlinks) remain what drives Google ranking.

Should an MCP server be stateful or stateless?

Default to stateless for read-only tool surfaces — it simplifies deployment (no session affinity needed), scales trivially on serverless/edge infrastructure, and matches the access pattern of most portfolio-style data (parameterized reads over a fixed dataset). Choose stateful only when a tool genuinely requires multi-step server-side context, such as a workflow that spans multiple calls and can’t be reconstructed from a single request’s parameters.

Can an AI agent call an MCP tool that has side effects without user confirmation?

That depends on the MCP client’s own confirmation policy, not the server — the server’s only responsibility is to make side-effecting tools clearly distinguishable (naming, documentation, and ideally a readOnlyHint/annotation where the protocol supports it) so a well-behaved client can gate them appropriately. Well-designed servers still minimize side-effecting surface area to reduce blast radius regardless of client behavior, since not every client enforces confirmation consistently.

Is Astro SSG or SSR better for agent-readability?

Static generation (SSG) guarantees the full page exists as HTML at request time with zero server compute — the safest baseline for any consumer that only fetches raw HTML. Server-side rendering (SSR) is necessary for genuinely dynamic content (like a live availability check) but adds a runtime dependency; if that runtime is slow or errors, agents that don’t retry gracefully may see an incomplete page. Astro’s hybrid model — static by default, SSR opted into per-route — lets each route pick the right guarantee instead of forcing a site-wide tradeoff.

What is JSON-RPC 2.0 and why does MCP use it?

JSON-RPC 2.0 is a lightweight, transport-agnostic remote procedure call specification: a request carries a method name, parameters, and an id; a response carries a matching id with either a result or a structured error. MCP adopted it because it’s simple to implement in any language, has well-defined error semantics (numeric error codes, not ad hoc HTTP status juggling), and doesn’t tie the protocol to HTTP semantics the way a purely RESTful design would — which matters since MCP also supports transports like stdio for local process communication.

Do I need both llms.txt and agents.md, or is one redundant?

They’re not redundant — llms.txt is a general discovery/summary file for any LLM reading about your site, while agents.md is an integration contract for an agent that wants to call your tools or APIs specifically. A site with only llms.txt gives an agent a description but no actionable interface; a site with only agents.md assumes the agent already knows to look for it. Shipping both costs little and serves genuinely different consumption patterns.

How do I test that my MCP server actually works before shipping it publicly?

Send raw JSON-RPC requests against the endpoint directly (curl with a JSON-RPC envelope, or a minimal script) to verify tools/list returns correct schemas and tools/call returns well-formed results for each tool, independent of any specific client. Then test against at least one real MCP client (Claude Desktop or an equivalent) to catch transport-level issues — protocol version mismatches or streaming behavior — that a raw request test won’t surface, since client implementations vary in how strictly they enforce the spec.

Does adding JSON-LD structured data guarantee my site appears in AI-generated answers?

No — structured data reduces ambiguity in what a crawler or model extracts, but no provider publishes a guarantee or a formula tying JSON-LD presence to inclusion in a generated answer. Treat it as removing a source of extraction error, not as a ranking lever with a predictable effect.

Questions or project ideas?

Reach out directly to discuss architecture, optimization, or AI agents.

Get in Touch →