DamienKomala.
All articles
AI & Agentic Development10 min read

Building a Claude Code Support Agent: Pydantic, Context7, and a Finished PRD

Building a Claude Code Support Agent with Pydantic and Context7 — Damien Komala

Most AI coding assistants are helpful and slightly wrong in the same breath. Ask one how to add a page to your project and it will confidently answer for a project that isn't yours — a different framework, a different test runner, a styling approach you rejected in week one. The model isn't guessing badly. It simply has no idea what you already decided.

The thing is, you almost certainly wrote those decisions down. A finished PRD is the most under-used artifact in a codebase: it states the framework, the hosting target, the testing methodology, the accessibility floor, and the constraints nobody is allowed to quietly trade away. It's a specification that happens to be sitting in a Markdown file nobody opens after kickoff.

So point the agent at it. Below is the shape of a Claude Code support agent that answers questions about your project — with a subagent that checks its work, Pydantic holding the contract, and Context7 supplying documentation the model wasn't trained on. The worked example is the PRD behind this portfolio site.

The three pieces, and what each one is for

It's worth being clear about the division of labour before any code, because each piece solves a failure the others can't:

  • Pydantic is the contract. It turns "read the PRD" from a vague instruction into a typed object with a fixed set of fields, so the extraction either succeeds or fails loudly instead of drifting.
  • Context7 is the fact-checker for anything version-specific. Model training data has a cutoff; framework APIs do not respect it.
  • The Anthropic API and Claude Code's subagents are the runtime — one agent to answer, one to verify, each with its own context window and its own narrow tool set.

Step 1: Turn the PRD into a contract, not a blob

The tempting shortcut is to paste the whole PRD into a system prompt and call it done. It half-works, which is worse than failing. A long document buried in a prompt gives the model plenty of room to weight the wrong sentence, and you get no signal at all when the PRD changes in a way that matters.

Instead, decide up front which decisions an answer must respect, and write that down as a schema:

from typing import Literal
from pydantic import BaseModel, Field


class Preferences(BaseModel):
    """The settled decisions a support agent has to respect."""

    framework: Literal["nextjs", "astro"]
    styling: Literal["tailwind", "css-modules", "styled-components"]
    hosting: Literal["vercel", "netlify", "cloudflare"]
    test_runner: Literal["playwright", "cypress", "vitest"]
    methodology: Literal["tdd", "bdd", "none"]

    min_lighthouse_a11y: int = Field(ge=0, le=100)
    min_lighthouse_perf: int = Field(ge=0, le=100)

    non_negotiables: list[str] = Field(
        description="Constraints the PRD states that an answer must never contradict."
    )

Two things are doing real work here. The Literal types mean the extraction can't invent a fifth framework, and the description on non_negotiables is not a comment — it's shipped to the model as part of the schema and is genuinely the difference between a useful list and a restatement of the document's headings. Write those descriptions for the model, not for your teammates.

One useful wrinkle: the Anthropic SDK strips constraints the API's structured-output layer doesn't support — numeric bounds like ge and le among them — and validates them on your side instead. You still get the guarantee, it just gets enforced after the response lands rather than during generation.

Step 2: Extract once, and let the model do the reading

With the shape defined, the extraction is short. The Python SDK's messages.parse() takes the Pydantic model directly and hands back a validated instance:

from pathlib import Path
import anthropic

client = anthropic.Anthropic()

parsed = client.messages.parse(
    model="claude-opus-5",
    max_tokens=2048,
    system=(
        "Extract only what the PRD states. If the document does not express "
        "a preference, say so rather than inferring one."
    ),
    messages=[{"role": "user", "content": Path("prd.md").read_text()}],
    output_format=Preferences,
)

prefs = parsed.parsed_output   # a validated Preferences instance

Run that against the PRD behind this site and most of it comes back clean: Vercel for hosting, Playwright for tests, test-driven as the methodology, Lighthouse accessibility at 100 and performance at 90 or better. None of that was inferred from the repo — it was written down before any code existed, which is precisely what makes it a trustworthy brief.

The interesting field is framework. That PRD says "Next.js or Astro", and lists Tailwind as "recommended" — preferences with a hedge, not decisions. This is exactly the case the schema exists to surface. A Literal forces the model to commit to one value, so you either accept the pick or you notice that the document never actually decided. Both outcomes are better than an agent quietly assuming one and answering from it for six months.

Treat the result as a generated artifact. Write it to a JSON file, commit it, and regenerate when the PRD changes. That gives you a diff when a decision moves, which is far more valuable than the extraction itself.

Step 3: Give the agent its brief

Now the validated object becomes a system prompt. The important move is tone: these are settled decisions, and the prompt should say so, or the agent will keep helpfully offering alternatives you already ruled out.

BRIEF = f"""You support the {prefs.framework} build described in the project PRD.

Answer within these decisions. They are settled, not open questions:
- Framework: {prefs.framework}
- Styling: {prefs.styling}
- Tests: {prefs.test_runner}, written {prefs.methodology.upper()}-style
- Accessibility floor: Lighthouse {prefs.min_lighthouse_a11y}

Non-negotiables:
{chr(10).join(f"- {item}" for item in prefs.non_negotiables)}

If a question falls outside what the PRD covers, say so plainly instead of
inventing a decision on the project's behalf.
"""

In Claude Code, that brief becomes a subagent definition — a Markdown file in .claude/agents/ with frontmatter describing when to use it and which tools it gets:

---
name: build-support
description: Answers implementation questions about this project. Use when
  someone asks how to add a component, wire a route, or write a test here.
tools: Read, Grep, Glob, mcp__context7__resolve-library-id, mcp__context7__query-docs
model: opus
---

You support the Next.js build described in the project PRD.
(...the generated brief goes here...)

The tools line matters more than it looks. This agent reads and searches; it does not write files or run commands. An agent that can only answer questions cannot accidentally refactor your codebase while answering one.

Step 4: Add the subagent that checks the work

A single agent that drafts an answer and then reviews its own answer is not really reviewing anything — it's the same context, the same assumptions, and a strong pull toward agreeing with itself. Splitting the job across two agents fixes that cheaply, because a fresh subagent starts with an empty context window and no attachment to the draft.

---
name: answer-check
description: Verifies a drafted answer against current library docs and the
  repo before it reaches the user. Use after drafting any answer that names
  an API, parameter, flag, or config key.
tools: Read, Grep, mcp__context7__query-docs
model: sonnet
---

You are checking someone else's draft, not writing your own.

For every claim that names an API, parameter, or config key: find the
evidence in the docs or in this repository, or mark the claim unverified.
Report findings only. Do not rewrite the answer.

Three details make this work rather than just doubling your token bill. The checker gets a narrower tool set than the agent it checks. Its brief is adversarial by construction — find evidence or flag it, with no option to smooth over an uncertain claim. And it returns findings rather than a rewrite, so the primary agent stays responsible for the final answer instead of laundering an unverified claim through a second model.

You don't need this on every question. "Where does the nav live?" is answered by a grep. Route it to the checker when an answer names a version-specific API — which is exactly where the next piece comes in.

Step 5: Stop the agent coding from memory

Every model has a training cutoff, and framework APIs move faster than cutoffs do. The failure mode is specific and recognisable: confidently correct-looking code using a config key that was renamed, a hook that changed signature, or a CLI flag that no longer exists. It looks right because it was right, eighteen months ago.

Context7 closes that gap by serving current documentation over MCP. It's an HTTP server, so setup is a few lines of config:

{
  "mcpServers": {
    "context7": {
      "type": "http",
      "url": "https://mcp.context7.com/mcp",
      "headers": { "Authorization": "Bearer ctx7sk-YOUR-KEY-HERE" }
    }
  }
}

Usage is deliberately two-step. First resolve the library name to an ID, then query that ID with a specific question — "how to configure static generation with generateStaticParams", not "routing". Broad one-word queries return shallow results for everything; one concept per query returns something you can actually act on.

The rule worth encoding in the brief is to consult the docs even when the agent thinks it knows the answer. An agent that only checks documentation when it feels uncertain will never check the confidently-wrong cases, which are the only ones that hurt.

What this actually buys you

The obvious win is fewer wrong answers. The less obvious one is that the PRD stops being a document that dies at kickoff. Once an agent's behaviour is derived from it, a stale PRD produces visibly stale answers, and there's finally a reason to keep it current. The extraction step doubles as a linter for your own specification — if the model can't find a testing methodology in the document, that's not an extraction bug.

The structure also degrades gracefully. Schema validation fails loudly rather than silently drifting. An agent told to say "the PRD doesn't cover this" produces a visible gap instead of a confident fabrication. The verifying subagent marks claims unverified rather than deleting them. Every failure surfaces as something you can see.

Where it breaks

Worth knowing before you build it:

  • PRDs drift, and nothing warns you. Regenerate the preferences file as part of your build or a scheduled job, and commit it, so a changed decision shows up in a diff rather than in a wrong answer three weeks later.
  • An over-tight schema forces bad fits. If Literal lists every option except the one the project actually chose, the model picks the nearest wrong answer rather than refusing. Give ambiguous fields room, or an explicit escape value.
  • The agent inherits the PRD's blind spots. A document that never mentions error states produces an agent with no opinion on error states. Silence in the source is not a preference — it's a gap, and it stays a gap.
  • Verification is not free. A checking subagent roughly doubles the cost of an answer. Reserve it for claims that name a specific API, and let the cheap questions stay cheap.
  • Keep credentials out of the agent's reach. The Context7 key belongs in MCP configuration, not in a prompt, an agent file, or anywhere the agent can read and repeat it back.

Final thoughts

None of this is really about Claude, Pydantic, or Context7 specifically — swap any one of them out and the shape holds. What makes the difference is refusing to let the agent operate on vibes in three separate places: the project's decisions come from a written specification rather than inference, the extraction is constrained by a typed contract rather than trusted prose, and version-specific claims are checked against current documentation rather than recalled.

An assistant that knows what you already decided is a fundamentally different tool from one that guesses well. The gap between them isn't a better model. It's about twenty lines of schema and the willingness to open the PRD again.