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 manage to be helpful and slightly wrong at the same time. Ask one how to add a page to your project and it'll cheerfully answer for a project that isn't yours: a different framework, a different test runner, a styling approach you threw out in week one. The model isn't bad at guessing. It just has no idea what you already decided.

And you almost certainly wrote those decisions down. A finished PRD is the most under-used file in a codebase. It names the framework, the hosting target, the testing approach, the accessibility floor, and the constraints nobody's allowed to quietly trade away. Then it sits in a Markdown file that nobody opens again after kickoff, like a gym membership.

So point the agent at it. What follows is the shape of a Claude Code support agent that answers questions about your project, with a second agent that checks its work, Pydantic holding the contract, and Context7 supplying documentation the model was never trained on. My worked example is the PRD behind this very portfolio site.

The three pieces, and what each one is for

Before any code, it helps to know who does what, because each piece fixes a failure the other two 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 works or fails loudly instead of slowly drifting. Context7 is the fact-checker for anything that depends on a version. Model training data has a cutoff, and framework APIs couldn't care less about it. And the Anthropic API plus Claude Code's subagents are the runtime: one agent answers, another verifies, and each gets its own context window and its own short list of tools.

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 a day. It half works, which is honestly worse than failing outright. A long document buried in a prompt gives the model plenty of room to latch onto the wrong sentence, and when the PRD changes in a way that matters, nothing tells you.

Instead, decide up front which decisions an answer has to respect, and write them 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 here are pulling real weight. The Literal types mean the extraction can't invent a fifth framework. And the description on non_negotiables isn't a comment. It gets sent to the model as part of the schema, and it's the difference between a useful list and a lazy restatement of the document's headings. Write those descriptions for the model, not for your teammates. (Your teammates weren't going to read them anyway.)

One handy wrinkle: the Anthropic SDK strips out constraints the API's structured-output layer doesn't support, numeric bounds like ge and le among them, and checks them on your side instead. You still get the guarantee. It's just enforced after the response arrives rather than while it's being generated.

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

Once the shape is defined, the extraction is short. The Python SDK's messages.parse() takes the Pydantic model directly and hands you 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 guessed from the repo. It was all written down before any code existed, which is exactly why it makes a trustworthy brief.

The interesting field is framework. My PRD says "Next.js or Astro" and calls Tailwind "recommended." Those are preferences with an escape hatch, not decisions, and past me was clearly keeping my options open. This is exactly the situation the schema is there to catch. A Literal forces the model to commit to one value, so either you accept its pick or you notice the document never actually decided. Both beat an agent quietly assuming one and answering from it for six months.

Treat the result as a generated file. Write it to JSON, commit it, and regenerate it whenever the PRD changes. That way a moved decision shows up as a diff, and the diff turns out to be more valuable than the extraction itself.

Step 3: Give the agent its brief

Now the validated object becomes a system prompt. The important part is the tone. These are settled decisions, and the prompt has to say so, or the agent will keep helpfully suggesting alternatives you ruled out ages ago.

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. It's a Markdown file in .claude/agents/, with frontmatter that says 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...)

That tools line matters more than it looks. This agent can read and search, but it can't write files or run commands. An agent that can only answer questions can't accidentally refactor your codebase while it's answering one, which is a sentence I wish I'd never had a reason to write.

Step 4: Add the subagent that checks the work

An agent that drafts an answer and then reviews its own answer isn't really reviewing anything. Same context, same assumptions, and a strong urge to agree with itself, like grading your own homework. Splitting the job between two agents fixes that cheaply, because a fresh subagent starts with an empty context window and no emotional 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.

A few details make this actually work instead of just doubling your token bill. The checker gets a smaller tool set than the agent it's checking. Its brief is adversarial on purpose: find the evidence or flag the claim, with no option to smooth over something shaky. And it returns findings, not a rewrite, so the main agent stays responsible for the final answer instead of laundering an unverified claim through a second model.

You don't need this for every question. "Where does the nav live?" is a job for grep. Send it to the checker when an answer names a version-specific API, which is exactly where the last 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 way this fails is very recognizable once you've been bitten: confident, correct-looking code that uses a config key that got renamed, a hook whose signature changed, 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" }
    }
  }
}

Using it is a deliberate two-step. First you resolve the library name to an ID, then you ask that ID a specific question, like "how to configure static generation with generateStaticParams" rather than "routing". One-word queries get you shallow results about everything. One concept per query gets you something you can actually use.

The rule I'd put in the brief is to check the docs even when the agent thinks it knows the answer. An agent that only looks things up when it feels unsure will never look up the confidently wrong cases, and those are the only ones that hurt.

What this actually buys you

The obvious win is fewer wrong answers. The sneakier one is that the PRD stops dying at kickoff. Once an agent's behavior comes from it, a stale PRD produces visibly stale answers, and suddenly there's a real reason to keep it up to date. The extraction step doubles as a linter for your own spec, too. If the model can't find a testing methodology in the document, that's not an extraction bug. That's a note to self.

It also fails in ways you can see. Schema validation complains loudly instead of drifting quietly. An agent told to say "the PRD doesn't cover this" leaves a visible gap instead of a confident invention. And the checking subagent marks claims as unverified instead of deleting them, so every failure shows up somewhere you'll notice it.

Where it breaks

A few things worth knowing before you build it. PRDs drift, and nothing warns you, so regenerate the preferences file as part of your build or on a schedule and commit it. Then a changed decision shows up in a diff instead of in a wrong answer three weeks later.

A schema that's too tight forces bad fits. If your Literal lists every option except the one the project actually picked, the model will choose the closest wrong answer rather than refuse. Give ambiguous fields some room, or an explicit escape value.

The agent also 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 isn't a preference. It's a gap, and it stays a gap until someone writes something.

Verification isn't free either. A checking subagent roughly doubles the cost of an answer, so save it for claims that name a specific API and let the cheap questions stay cheap. And 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 it and helpfully repeat it back to you.

Final thoughts

None of this is really about Claude, Pydantic or Context7 in particular. Swap any one of them out and the shape still holds. What makes the difference is refusing to let the agent run on vibes in three places. The project's decisions come from a written spec instead of guesswork, the extraction is held to a typed contract instead of trusted prose, and version-specific claims get checked against current docs instead of recalled from memory.

An assistant that knows what you already decided is a very different tool from one that guesses well. Getting there doesn't take a better model. It takes about twenty lines of schema and the willingness to open the PRD again.