<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Tappert</title>
    <description>The latest articles on DEV Community by David Tappert (@david-tappert).</description>
    <link>https://dev.to/david-tappert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3893205%2F9fcab857-4519-46cf-bcc9-7390f25d8d1d.png</url>
      <title>DEV Community: David Tappert</title>
      <link>https://dev.to/david-tappert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david-tappert"/>
    <language>en</language>
    <item>
      <title>The Dangerous Bugs Are the Ones That Don't Crash: Building Input Validation for My MCP Server</title>
      <dc:creator>David Tappert</dc:creator>
      <pubDate>Sun, 03 May 2026 20:34:54 +0000</pubDate>
      <link>https://dev.to/david-tappert/the-dangerous-bugs-are-the-ones-that-dont-crash-building-input-validation-for-my-mcp-server-1521</link>
      <guid>https://dev.to/david-tappert/the-dangerous-bugs-are-the-ones-that-dont-crash-building-input-validation-for-my-mcp-server-1521</guid>
      <description>&lt;p&gt;I was building an MCP server for an event platform that automates speaker communications (confirmations, reminders, calendar invites, follow-ups). An agent created a session confirmation for "Monday March 8th." March 8th was a Sunday.&lt;/p&gt;

&lt;p&gt;I caught it. But catching it was just the beginning.&lt;/p&gt;

&lt;p&gt;The confirmation email had already been drafted with "Monday March 8th." The calendar invite had the wrong day. The follow-up survey was timestamped against a date that didn't exist on the schedule. One silent error had propagated into every downstream artifact the system touched.&lt;/p&gt;

&lt;p&gt;Now I'm not fixing one mistake. I'm chasing it through four different outputs, correcting each one, regenerating, re-checking. Every correction creates noise. Every re-check takes time. And the whole time I'm wondering: &lt;em&gt;what else did it get wrong that I didn't catch?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the thing about silent errors. They don't announce themselves. They don't crash. They just quietly spread, and the cleanup costs more than the original task saved you.&lt;/p&gt;

&lt;p&gt;I shouldn't have had to catch it. The MCP server should have rejected the input before any of those artifacts were generated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why: One Silent Error, Everywhere, All at Once
&lt;/h2&gt;

&lt;p&gt;Most validation advice focuses on the errors that blow up: wrong types, missing fields, injection attacks. Those are easy. Your server crashes, the agent gets an error, everyone knows something went wrong.&lt;/p&gt;

&lt;p&gt;The harder problem is the errors that don't blow up. The ones where your server happily does exactly what the LLM asked, and what the LLM asked was wrong. No crash. No warning. Just wrong data flowing downstream with full confidence.&lt;/p&gt;

&lt;p&gt;Here's why this is uniquely dangerous for MCP servers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't control the caller.&lt;/strong&gt; Your MCP server might be called by any number of agents, IDEs, or automation pipelines, each powered by a different model with different strengths and weaknesses. You can't rely on the caller to get it right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent runtime doesn't help.&lt;/strong&gt; It takes the LLM's tool call (name + JSON arguments) and forwards it to your server. It doesn't validate. It doesn't transform. Whatever the LLM generates, your server receives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent errors propagate.&lt;/strong&gt; This is the real cost. A wrong date in a session creation doesn't just create one bad record. It poisons every downstream artifact. The confirmation, the reminder, the calendar invite, the follow-up. Each one carries the same wrong date into a different system, a different channel, a different person's inbox. By the time a human notices, the error isn't in one place. It's everywhere. Cleaning it up means touching everything it touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM is confident.&lt;/strong&gt; It doesn't say "I'm not sure about this date." It says "Monday March 8th" with the same certainty it says "Tuesday March 10th." There's no signal that something is wrong, unless your server provides one.&lt;/p&gt;

&lt;p&gt;Your MCP server is the one constant across all callers, all models, all agents. It's not a nice-to-have. It's the only thing standing between one confident mistake and a cleanup that costs more than the automation saved you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How: Validate the Model, Not Just the Fields
&lt;/h2&gt;

&lt;p&gt;Most people stop at field validation. Is the date valid? Is the string non-empty? Is the number positive? That's necessary, but it misses the entire class of bugs I'm talking about.&lt;/p&gt;

&lt;p&gt;A session scheduled for "Monday March 8th" passes every field-level check. The date is valid. The weekday is a real weekday. The title is non-empty. The duration is positive. Every field is correct in isolation, but the model is wrong.&lt;/p&gt;

&lt;p&gt;The question isn't "is each field valid?" It's &lt;strong&gt;"does the whole input make sense together?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three principles make this work:&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate cross-field coherence
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;model_validator&lt;/code&gt; runs after all fields are parsed and checks relationships between them. The weekday matches the date. The end time is after the start time. The duration fits the time window. The reminder comes before the event. No single field is wrong, but together, they might be nonsense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collect all errors in one pass
&lt;/h3&gt;

&lt;p&gt;If the agent sends a request with three problems, report all three so it can fix them in one retry. Don't play whack-a-mole. That wastes round-trips and burns tokens. Pydantic does this naturally; it collects all field validation errors before raising a single &lt;code&gt;ValidationError&lt;/code&gt;. Your &lt;code&gt;model_validator&lt;/code&gt; can do the same: accumulate errors in a list and raise once at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write error messages for machines
&lt;/h3&gt;

&lt;p&gt;The error message isn't for a human reading a log. It's for an LLM that needs to fix its own output. "2026-03-08 is a Sunday, not a Monday" is actionable. "Invalid date" is not. The more specific the message, the faster the self-correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What: The Implementation
&lt;/h2&gt;

&lt;p&gt;Here's the concrete code. I built this with &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; using spec-driven development: requirements with acceptance criteria, a design with Pydantic models, and a task breakdown. The implementation follows the spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting point: MCP tools without validation
&lt;/h3&gt;

&lt;p&gt;My MCP server had tools for creating sessions, searching documents, managing speakers. Each tool accepted arguments directly from the LLM and passed them through. The models looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateSessionInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;str&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
    &lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
    &lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;duration_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;speaker_aliases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;reminder_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Typed. Totally unvalidated beyond basic types. &lt;code&gt;day_of_week&lt;/code&gt; is never checked against &lt;code&gt;session_date&lt;/code&gt;. &lt;code&gt;end_time&lt;/code&gt; could be before &lt;code&gt;start_time&lt;/code&gt;. &lt;code&gt;reminder_date&lt;/code&gt; could be after the event. &lt;code&gt;duration_minutes&lt;/code&gt; could contradict the time window. Every field is correct in isolation, and the model is wrong.&lt;/p&gt;

&lt;p&gt;This is the bug from the opening. "Monday March 8th" passes every type check. The date is valid. The weekday is a real weekday. Pydantic says it's fine. The server creates the session. Four downstream artifacts, all wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding validation to an existing spec
&lt;/h3&gt;

&lt;p&gt;The spec was already built. Some tasks were done, some were in progress. Doesn't matter.&lt;/p&gt;

&lt;p&gt;When I realized the gap, I added a new validation design standard to my spec workflow (validation patterns, error message quality, boundary checks) that Kiro applies as a review pass before (or sometimes after) implementation starts. Think of it as a standards review, but for the spec. I review the gaps it finds and use them as the input for the next iteration of the spec.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Remediation Report — Round 2: Pydantic Validators&lt;/span&gt;

&lt;span class="gu"&gt;## Summary&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Gaps:**&lt;/span&gt; 3
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Overall:**&lt;/span&gt; The models use plain BaseModel with type hints only.
  No @field_validator or @model_validator. Validation is handled
  procedurally or not at all.

&lt;span class="gu"&gt;## Gaps&lt;/span&gt;

&lt;span class="gu"&gt;### GAP-1: Tool accepts any day_of_week without checking the date&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; The model accepts any string for day_of_week, including weekdays
  that don't match session_date. This is the "Monday March 8th" bug.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Suggested action:**&lt;/span&gt; Add @model_validator that checks day_of_week
  against session_date.weekday()

&lt;span class="gu"&gt;### GAP-2: No cross-field time validation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; end_time can be before start_time. duration_minutes can contradict
  the time window. reminder_date can be after the event.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Suggested action:**&lt;/span&gt; Add @model_validator that checks all time
  relationships in one pass, collects all errors.

&lt;span class="gu"&gt;### GAP-3: No protection against empty speaker lists&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; speaker_aliases accepts an empty list. A session with no speakers
  generates confirmations addressed to nobody.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Suggested action:**&lt;/span&gt; Add min_length=1 on the Field, or a
  @field_validator that rejects empty lists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I told Kiro: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnazxq8twtx6iu1yw18di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnazxq8twtx6iu1yw18di.png" alt=" " width="800" height="254"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;"I have a gap on the spec. We need to apply better validation. Take a look at my new remediation file and update the spec please."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Kiro updated all three layers (requirements, design, and tasks) and the new tasks showed up unchecked alongside the completed ones. That's the point: the spec isn't a planning document you write once and forget. It stays in sync with your code, and you can update it at any point: mid-build, after shipping, during a review.&lt;/p&gt;

&lt;p&gt;Here's what the updated spec looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements&lt;/strong&gt;: a new requirement with acceptance criteria, each one specific and testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Requirement: Model-Level Input Validation&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; WHEN a create_session tool call includes a day_of_week that does
   not match the session_date, THEN the model SHALL raise a
   ValidationError with the actual weekday and the provided one
&lt;span class="p"&gt;2.&lt;/span&gt; WHEN end_time is before or equal to start_time, THEN the model
   SHALL raise a ValidationError
&lt;span class="p"&gt;3.&lt;/span&gt; WHEN duration_minutes does not match the time window between
   start_time and end_time, THEN the model SHALL raise a
   ValidationError
&lt;span class="p"&gt;4.&lt;/span&gt; WHEN reminder_date is on or after session_date, THEN the model
   SHALL raise a ValidationError
&lt;span class="p"&gt;5.&lt;/span&gt; WHEN speaker_aliases is empty, THEN the model SHALL raise a
   ValidationError
&lt;span class="p"&gt;6.&lt;/span&gt; ALL validation errors SHALL be collected and returned in a single
   response with messages specific enough for the LLM to self-correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design&lt;/strong&gt;: validators added directly to the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_validator&lt;/span&gt;

&lt;span class="n"&gt;WEEKDAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Monday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tuesday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wednesday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thursday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Friday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saturday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sunday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateSessionInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;str = Field(min_length=1, max_length=200)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
    &lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
    &lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;duration_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;speaker_aliases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reminder_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@model_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_model_coherence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="c1"&gt;# Weekday must match the date
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WEEKDAYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is a &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not a &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# End time must be after start time
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_time (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) must be after &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_time (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Duration must match the time window
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start_dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;end_dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;actual_minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_dt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_dt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_minutes&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_minutes (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_minutes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match the time window (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; minutes)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Reminder must be before the event
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reminder_date&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reminder_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder_date (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reminder_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) must be before &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_date (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tasks&lt;/strong&gt;: new tasks with traceability, plus updates to existing ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [ ] Add model_validator to CreateSessionInput
&lt;span class="p"&gt;    -&lt;/span&gt; Weekday vs date check, end_time vs start_time, duration vs
      time window, reminder vs session date
&lt;span class="p"&gt;    -&lt;/span&gt; Collect all errors in one pass, raise once
&lt;span class="p"&gt;    -&lt;/span&gt; Requirements: 1–6
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] Write tests for model-level validation
&lt;span class="p"&gt;    -&lt;/span&gt; Valid input passes, each invalid combination raises
      ValidationError with specific message
&lt;span class="p"&gt;    -&lt;/span&gt; Validates: Requirements 1–6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiro also updated existing tasks that were affected. The session creation task now validates before processing instead of assuming clean input.&lt;/p&gt;

&lt;h3&gt;
  
  
  From spec to code
&lt;/h3&gt;

&lt;p&gt;Once the spec was updated, Kiro implemented the tasks. The validator lives in the model. Wiring it into the MCP tool is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;str, session_date: str, day_of_week: str = None, ...) -&amp;gt; str:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateSessionInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Every field is now coherent as a unit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The validation runs before any downstream logic. If the input is incoherent, the tool returns the errors immediately. No confirmation drafted, no calendar invite created, no follow-up queued.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cascade that didn't happen
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The bad input:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;CreateSessionInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kiro Deep Dive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Monday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;duration_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Without model validation:&lt;/strong&gt; the server creates the session. The confirmation email says "Monday March 8th." The calendar invite is scheduled for Sunday March 8th. The follow-up survey is timestamped against a date that doesn't match the agenda. Three downstream artifacts, all wrong, zero errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With model validation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValidationError: 2026-03-08 is a Sunday, not a Monday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One error, caught at the boundary, before anything is created. The agent self-corrects and retries with the right date.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Pydantic catches that type checking doesn't
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the LLM sends&lt;/th&gt;
&lt;th&gt;Why it's wrong&lt;/th&gt;
&lt;th&gt;Pydantic catches it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;date: "2026-03-08"&lt;/code&gt; with &lt;code&gt;day: "Monday"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;March 8 is Sunday&lt;/td&gt;
&lt;td&gt;✅ model_validator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;start: "14:00"&lt;/code&gt; with &lt;code&gt;end: "13:30"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;End before start&lt;/td&gt;
&lt;td&gt;✅ model_validator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;duration: 20&lt;/code&gt; with a 30-min window&lt;/td&gt;
&lt;td&gt;Duration mismatch&lt;/td&gt;
&lt;td&gt;✅ model_validator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;reminder: "2026-03-10"&lt;/code&gt; for a March 8 event&lt;/td&gt;
&lt;td&gt;Reminder after event&lt;/td&gt;
&lt;td&gt;✅ model_validator&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Beyond This Example
&lt;/h2&gt;

&lt;p&gt;The date-weekday check is one example of validation against silent errors. Others that might make sense for your MCP server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session overlap detection&lt;/strong&gt;: is the speaker already presenting in another session at the same time? This is the kind of cross-record validation that no single model can catch on its own, but your server has the context to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business hours sanity check&lt;/strong&gt;: a session at 3 AM is technically valid but probably wrong for a community event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate detection&lt;/strong&gt;: flag if a session with the same title and date already exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How much validation is the right amount? That depends on the use case. You still want to be engaged and thinking about this. Not every field needs a custom validator. The goal is to catch the errors that propagate silently, not to validate everything the LLM could theoretically get wrong.&lt;/p&gt;

&lt;p&gt;One thing that helps more than validation: &lt;strong&gt;good data models.&lt;/strong&gt; Think about which fields are independent inputs and which are derived. If you accept &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;end_time&lt;/code&gt;, &lt;em&gt;and&lt;/em&gt; &lt;code&gt;duration_minutes&lt;/code&gt;, you have three interrelated values, and now you need a validator just to check they agree. You could accept two and compute the third. Or you could keep all three and treat the redundancy as a checksum, the same way the weekday validates the date. The right call depends on whether the LLM is computing the value (checksum it) or the user is providing it directly (don't duplicate it).&lt;/p&gt;

&lt;p&gt;The date-weekday case is a good example. Users say things like "schedule me a session for next Monday" and the LLM resolves that to a date. The weekday is the user's intent; the date is the LLM's computation. Accepting both and validating that they agree is how you catch the LLM's reasoning errors. That's not redundancy, it's a checksum.&lt;/p&gt;

&lt;p&gt;Each new check is a new acceptance criterion in the spec. Add it, run the tasks, review the output. Same workflow, same patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Get started with Kiro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No spec yet?&lt;/strong&gt; If you already have an MCP server but no spec, you can vibe in the validation directly, or you can ask Kiro to build a spec from your existing code first. Building the spec from your hand-crafted or vibe-coded MCP will document your design and help you, Kiro, and others understand how to add to it. It makes it easier to see what's validated and what isn't, and to add new checks systematically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have a spec?&lt;/strong&gt; Describe the validation you want to add, and the new tasks show up alongside your existing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec has drifted from the code?&lt;/strong&gt; Maybe you wrote the spec, executed the tasks, and then kept building without updating it. Ask Kiro to update the spec based on the current code. It isn't foolproof, but it's a good way to get back in sync, and you might understand your own codebase better once you see what it's actually doing described as requirements and design decisions.&lt;/p&gt;

&lt;p&gt;Field validation is table stakes. Model validation is where you stop the cascade.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm David, a Technical Account Manager at AWS. My background is in software development, business systems, product development, and program management. I started building MCP servers and agent tooling not as a side project, but because I needed them. I quickly realized that making LLMs work &lt;em&gt;with&lt;/em&gt; my existing workflows, rather than rebuilding everything around them, is the harder and more interesting problem. This article is a first example of that: a real bug, a real fix, and a workflow that scales.&lt;/p&gt;

&lt;p&gt;I'm planning more articles in this space. A few topics I'm exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standards reviews for specs&lt;/strong&gt;: encoding your best practices (validation patterns, error message quality, security checks) so they're applied automatically to every spec, not just the ones you remember to check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A spec workflow driven by "Start with Why"&lt;/strong&gt;: how I structure every spec around Why → How → What, so the problem shapes the requirements instead of the other way around&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working backwards from code to spec&lt;/strong&gt;: how to reverse-engineer a spec from an existing codebase and use it to regain control of a project that's drifted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would be most useful to you? Drop a comment. I'd love to hear what problems you're running into with your MCP servers.&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>validation</category>
      <category>pydantic</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
