<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged agentic-evaluation</title>
<link>https://coderlegion.com/tag/agentic-evaluation</link>
<description>Powered by Question2Answer</description>
<item>
<title># Evaluating Pydantic AI agents that call MCP tools</title>
<link>https://coderlegion.com/24261/evaluating-pydantic-ai-agents-that-call-mcp-tools</link>
<description>&lt;p&gt;A Pydantic AI agent returns &lt;code&gt;BookingResult(confirmed=True, flight_id='AA127', total_usd=412.50)&lt;/code&gt;. Every Pydantic check passes. The &lt;code&gt;output_type&lt;/code&gt; matches. The tool-call arguments validated against the Pydantic model on the way in. The retry counter is at zero. The user asked to book the Sunday flight, and AA127 is a Friday flight. The agent confirmed the wrong booking with full type safety.&lt;/p&gt;
&lt;p&gt;That is the gap I want to talk about. Pydantic AI's typed contract gives you free schema validation. It does not give you free quality evaluation. The shape is guaranteed, the retry-on-validation-failure loop is wired, and the &lt;code&gt;output_type&lt;/code&gt; parses. That is the floor. The ceiling is whether the agent picked the right tool, whether the arguments were semantically correct, whether the typed output reflects the user's goal, whether the dependency invariants held, and whether an MCP tool description carried an injection payload that bypassed every type check. Four layers cover that gap, and there is a thorough &lt;a rel=&quot;nofollow&quot; href=&quot;https://futureagi.com/blog/evaluating-pydantic-ai-mcp-agents-2026/?utm_source=fagiCoderlegion&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution&quot;&gt;walkthrough of the full methodology&lt;/a&gt; with the rubric code and the instrumentation if you want to lift it directly; this is the shorter argument for each layer.&lt;/p&gt;
&lt;h3 id=&quot;why-the-typed-contract-is-the-floor-not-the-ceiling&quot;&gt;Why the typed contract is the floor, not the ceiling&lt;/h3&gt;
&lt;p&gt;Pydantic AI is the agent framework from the Pydantic team. You declare an agent with a model, an &lt;code&gt;output_type&lt;/code&gt; (a Pydantic model), a typed &lt;code&gt;deps_type&lt;/code&gt; for runtime dependencies, and tools whose arguments are Pydantic models. The framework dispatches the model call, parses the tool selection, validates arguments against the schema, calls the tool, and re-prompts on a validation failure. It also has first-class MCP client support.&lt;/p&gt;
&lt;p&gt;That contract handles four bug classes for free: tool-argument shape mismatch, output-schema mismatch, a non-existent tool name, and &lt;code&gt;deps_type&lt;/code&gt; enforcement at call time. Every one of those is a question about syntax. The questions that break in production are about meaning, and the two do not overlap:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Failure mode &lt;/th&gt;&lt;th&gt;Caught by the type contract &lt;/th&gt;&lt;th&gt;Needs evaluation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Tool argument shape mismatch &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Output shape mismatch &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Wrong tool for the user intent &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Right tool, semantically wrong arguments &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Right tool, wrong MCP server &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Typed values do not reflect the user goal &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Trajectory loops, stalls, or stops short &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Refusal drifts under retry pressure &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MCP tool description carries an injection payload &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Runtime guardrail&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MCP result tampers with the next planning turn &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Runtime guardrail&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A planned call violates a dependency invariant &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Schema validation is one assertion per field. The evaluation that matters is the assertion across fields, across calls, across servers, and across the dependencies that anchor the run.&lt;/p&gt;
&lt;h3 id=&quot;layer-1-score-every-typed-output-field-on-its-own&quot;&gt;Layer 1: score every typed output field on its own&lt;/h3&gt;
&lt;p&gt;Structured output is the part most teams over-trust. A &lt;code&gt;BookingResult(confirmed: bool, flight_id: str, total_usd: float)&lt;/code&gt; constrains the shape. It says nothing about whether &lt;code&gt;confirmed&lt;/code&gt; is correctly &lt;code&gt;True&lt;/code&gt;, whether &lt;code&gt;flight_id&lt;/code&gt; matches the flight the user asked about, or whether &lt;code&gt;total_usd&lt;/code&gt; is plausible for the route.&lt;/p&gt;
&lt;p&gt;The rule I use: any field whose correctness depends on the input gets its own judge. Pure-structure fields, a UUID or a fixed-format timestamp, do not. One judge per non-trivial field, each rubric narrow enough that the score is unambiguous. A &lt;code&gt;flight_id&lt;/code&gt; judge scores whether the flight matches the user's stated day, route, and time window, 5 for a match and 1 for a different day or route. Add the same for &lt;code&gt;confirmed&lt;/code&gt; (did the booking actually succeed in the trace) and &lt;code&gt;total_usd&lt;/code&gt; (is the value plausible for the route).&lt;/p&gt;
&lt;p&gt;Per-field judges produce per-field signal, which is the thing that names the fix. A single &quot;is this object correct&quot; judge produces an average you cannot act on. Gate CI per axis (&lt;code&gt;FlightIdFidelity &amp;gt;= 4.5&lt;/code&gt;, &lt;code&gt;ConfirmationFidelity == 5&lt;/code&gt;) and fail the build on the axis that broke.&lt;/p&gt;
&lt;h3 id=&quot;layer-2-check-tool-call-argument-fidelity&quot;&gt;Layer 2: check tool-call argument fidelity&lt;/h3&gt;
&lt;p&gt;The type contract confirms that &lt;code&gt;book_flight(flight_id='AA127', date='2026-08-15')&lt;/code&gt; parses against the tool's argument model. It says nothing about whether &lt;code&gt;AA127&lt;/code&gt; is the flight the user asked about or whether the date matches what they said. Argument fidelity sits between &quot;the arguments parsed&quot; and &quot;the agent did what I asked.&quot;&lt;/p&gt;
&lt;p&gt;Run a function-calling rubric on every tool-call span, scoring the tool name and arguments against the user prompt. Add three more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Task completion&lt;/strong&gt; scores the end state against the user goal across the whole trajectory, on the parent run span.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refusal calibration&lt;/strong&gt; catches both over-refusal and under-refusal, which matters because the retry-on-validation-failure loop can push the agent into a refusal posture when validation keeps failing on edge cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool-server attribution&lt;/strong&gt; carries the case the others miss. When two MCP servers expose similarly named tools, a docs &lt;code&gt;search&lt;/code&gt; and a CRM &lt;code&gt;search&lt;/code&gt;, the agent can call the wrong one and still produce a plausible answer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gate per run: function-calling above 0.90, task completion above 0.85, refusal calibrated, server attribution passing. The build fails on the axis that broke.&lt;/p&gt;
&lt;h3 id=&quot;layer-3-treat-the-mcp-integration-as-a-security-surface&quot;&gt;Layer 3: treat the MCP integration as a security surface&lt;/h3&gt;
&lt;p&gt;Three properties of MCP change the threat model. The tool catalog is part of the prompt, since &lt;code&gt;tools/list&lt;/code&gt; returns each tool's name, description, and schema straight into the model's context. The supply chain is &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt;. And the trust boundary moves per session, because the active toolset is the union of every registered server at run time.&lt;/p&gt;
&lt;p&gt;Four checks run at the gateway and one runs in CI. At the completion boundary, scan every tool definition before it enters the model's context, so a poisoned description is rejected at registration. At the per-call hook, scan arguments before dispatch and results before they enter the next planning turn, so an injection hidden in a &lt;code&gt;read_file&lt;/code&gt; return never reaches the model. Score sandbox and permission-escape attempts against a fixed payload catalog and the tool's declared scope. Get cross-tenant isolation from per-key allowed and denied tool lists plus trace-ID audit propagation. The CI piece is a 200 to 500 case regression suite of attack tool definitions and tampered results, run on every PR that touches MCP policy.&lt;/p&gt;
&lt;h3 id=&quot;layer-4-verify-the-agent-s-dependency-invariants&quot;&gt;Layer 4: verify the agent's dependency invariants&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;deps_type&lt;/code&gt; is the most underused contract surface. You declare a deps type, the agent injects it into every tool through the run context, and the framework enforces the type at parse. What it cannot enforce is whether the model's plan respects the invariants those deps imply. A support agent with &lt;code&gt;deps=AccountContext(tenant_id='acme', role='viewer')&lt;/code&gt; can plan a &lt;code&gt;delete_record&lt;/code&gt; call. Every type check passes. The action is a policy violation that should never have been planned.&lt;/p&gt;
&lt;p&gt;The fix is a deterministic invariant assertion per tool call: a pre-call check that takes the tool name, the arguments, and the run context, and returns allow or deny against the deps. If a viewer role reaches for a write tool, raise a structured error. Surface the verdict as a span attribute. The framework's retry loop treats the error as a structured tool failure, so the model sees the message, plans an alternative, and the run proceeds without escalating to a wrong action. Gate CI on zero tool-call spans where a deps-invariant violation flipped to an error on the golden set, and enforce the same rule at the gateway in production. Two layers, one invariant, no silent policy drift.&lt;/p&gt;
&lt;h3 id=&quot;running-the-four-layers-in-production&quot;&gt;Running the four layers in production&lt;/h3&gt;
&lt;p&gt;Run the gates cheapest-first. The &lt;code&gt;output_type&lt;/code&gt; parse and &lt;code&gt;deps_type&lt;/code&gt; enforcement run inside the framework at no token cost. The deps-invariant assertion is the next layer, still pure Python. The MCP runtime scanners are single-digit to tens of milliseconds. The judge-based rubrics (function calling, task completion, per-field judges) run last, only on the rows the deterministic gates let through.&lt;/p&gt;
&lt;p&gt;Build the golden set from production retries. Take around 300 prompts that produced two or more attempts in the last 30 days of live traffic; the retry count is the natural filter, because the prompts that retry are the ones closest to the framework's limits. Score offline and live with the same rubrics: offline over the golden set with per-axis CI thresholds, live by applying the same templates to sampled production spans, so a regression that escapes CI is caught within hours.&lt;/p&gt;
&lt;p&gt;Watch retries as a cost signal. The retry default is good for uptime and bad for the bill. A prompt whose mean retry count drifts from 1.1 to 2.4 has doubled in cost without changing in correctness. Chart the distribution per output type and alert on shifts.&lt;/p&gt;
&lt;h3 id=&quot;the-anti-patterns-i-keep-seeing&quot;&gt;The anti-patterns I keep seeing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema-pass-rate as the metric.&lt;/strong&gt; Parse success is a floor. Treating it as the ceiling ships wrong values confidently, because every type check is green. Add at least one per-field judge per output type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monolithic &quot;is this object correct&quot; judges.&lt;/strong&gt; A single score over the whole object is uninterpretable. Did &lt;code&gt;flight_id&lt;/code&gt; break or &lt;code&gt;confirmed&lt;/code&gt;? Per-field judges produce per-field signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No deps-invariant assertions.&lt;/strong&gt; The type contract enforces the type, not the policy. Every write tool reachable from a viewer-role run is a silent escalation path until the assertion is in place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusting MCP descriptions blindly.&lt;/strong&gt; A registered MCP tool is not a vetted one. The description scan and the result scan are not optional once the agent consumes any third-party server.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Instrument the agent so the run, tool-call, and model-request spans show up with the tool arguments and the output type populated. You cannot evaluate what you cannot see.&lt;/li&gt;
&lt;li&gt;Add one judge per non-trivial field of your output type, score it over a 50-row golden set, and wire per-axis CI thresholds.&lt;/li&gt;
&lt;li&gt;Add deterministic role checks on every write tool. Surface the verdict on the span and fail CI on golden-set runs that planned a denied call.&lt;/li&gt;
&lt;li&gt;Front the agent with an MCP scanner that checks tool definitions and results, and set allowed servers and per-key tool lists from day one.&lt;/li&gt;
&lt;li&gt;Cluster the first week of production failures, promote representative rows into the regression set, and re-run.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The teams shipping reliable Pydantic AI MCP agents stopped reporting schema-pass-rate and started reporting per-field judge scores, argument fidelity, MCP scanner verdicts, and deps-invariant pass rates. The type contract gives you the type-safe agent. The evaluation stack gives you the correct one.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24261/evaluating-pydantic-ai-agents-that-call-mcp-tools</guid>
<pubDate>Sat, 08 Aug 2026 13:18:00 +0000</pubDate>
</item>
</channel>
</rss>