DEV Community

Cover image for I Got Tired of AI Agents Breaking My System Contracts, So I Built Something to Stop It
Sa'ad Zarook
Sa'ad Zarook

Posted on

I Got Tired of AI Agents Breaking My System Contracts, So I Built Something to Stop It

Okay, story time.

If you've worked on a full stack app where the backend is Java/Spring Boot and the frontend is React, you know the drill. Someone changes something on one side of a contract and nobody tells the other side. Weeks later you're playing detective across five files trying to figure out who calls what.

And it's not just REST endpoints. It's the scheduled job that quietly writes to the same table your API touches. It's the service that calls another service, which calls another service. It's the Kafka event your controller publishes that some completely unrelated listener is consuming three modules away. All of that is "the contract" too, it's just invisible unless you go looking for it.

Now add AI coding agents into that picture. They're great at writing code in the file they're looking at. They're not great at knowing that the component they're editing calls an endpoint, which hits a controller, which calls a service, which calls a repository, which is also written to by a scheduled job at 2am, which also fires an event three other services are listening for. Agents see one file at a time. So they'll happily rename a field or change a return shape on one side and leave everything downstream of it completely unaware anything changed.

I got burned by this enough times that I decided to build the map myself. That's how Contour happened, and then, once I realized AI agents needed to query that map directly instead of just reading it off my screen, Contour MCP happened right after.

Let's get into it.

The actual problem

Working across a UI, a REST API, a service layer, a repository layer, a database, plus schedulers and events sitting on top of all of it, two things go wrong constantly.

  1. Agents (and honestly, humans too) edit one side of a flow without knowing the other side exists.
  2. People burn real time reconstructing a call chain by hand, jumping through five or six files just to make a change that should be simple.

Both come from the same root cause. Nobody, human or AI, has a live and accurate picture of how the system actually connects. Not how it's documented to connect, not how a diagram from two years ago says it connects. How it actually connects, in the code, right now.

Contour's whole job is to build that picture automatically by reading your source code.

The part I think matters most: CodeLens annotations

Before I get into the full feature list, I want to call out the thing that's genuinely not something you get out of the box in VS Code today. Contour puts inline CodeLens annotations directly above your controller methods and your frontend HTTP calls.

Above a @GetMapping or @PostMapping, it shows you how many frontend call sites actually use it. Above an axios or fetch call, it shows exactly which controller method handles it. Click it and jump straight there. If there's no known caller, or a call that resolves to nothing, it says so explicitly instead of just showing nothing and letting you assume everything's fine.

That inline, always visible, click through link between the two sides of a call is not something VS Code gives you natively, and it's not something most tools bother to surface directly in the editor gutter rather than in a separate panel. You see it exactly where you're already looking, while you're already editing the code.

What Contour actually does

Contour is a VS Code extension that indexes your codebase and builds a live map of your real call chains. It's built specifically for Java/Spring Boot backends paired with React/JS or TS frontends, whether that's a single service or a pile of microservices in one workspace.

Here's what you get once it's indexed your project.

Endpoint to consumer mapping. Covered above, this is the CodeLens piece.

Full call chain tracing. Follow a request from the controller, through the service layer, into the repository layer, all the way down to the actual database operation, including the derived query or @Query behind a Spring Data method. Multi branch chains and deep call graphs are both supported.

Drift detection, opt in, off by default. If a path, method, or DTO field changes on one side of a contract and the other side wasn't updated to match, it's flagged inline as you edit. There's an escape hatch for endpoints you're intentionally versioning.

Test coverage overlay. Right next to each endpoint you'll see whether it's covered by a real test, pulled from your existing JaCoCo and Jest/Vitest reports, not recomputed. No report found is shown honestly instead of a misleading 0%.

AI context export. One click copies a compact, accurate summary of an endpoint, its full call chain, its real consumers, its test coverage, its use case, straight to your clipboard, ready to paste into a prompt.

Use case tagging. Group related flows under a human readable label like "Order Checkout" or "User Onboarding," attach a Jira key and free form tags, and browse or export everything under that label as one combined context block.

Visual HTML report. Export a single self contained HTML file showing your whole traced system, grouped by use case, filterable by type and coverage status. Anyone can open it in a browser, no extension required.

Beyond REST. This is the part I really want to underline because it's easy to assume this is just an "API mapping tool," and it's not. Contour also traces:

  • Scheduled tasks, @Scheduled methods, what they do and how they're covered
  • Service to service calls, RestTemplate, WebClient, Feign clients, tracing outbound calls to other services in your workspace
  • Event driven flows, Spring application events, Kafka, RabbitMQ, JMS, and (clearly marked as lower confidence) Solace JCSMP, matched from publisher to consumer by event key or topic

So a "flow" in Contour isn't just "endpoint plus its callers." It's the full shape of how a piece of functionality actually moves through your system, whether that's an HTTP request, a cron job, a backend to backend call, or a message on a topic.

SOLID structural lint, opt in. A gentle nudge, not a hard error, when a controller reaches directly into a repository and skips the service layer.

It also supports project specific conventions through a committed contour.config.json, for custom annotations, hand rolled HTTP wrappers, Gradle multi module layouts, and non Spring Data repository base interfaces. There's a generator command that writes a starter config, but only when it can verify something with certainty. If it can't verify it, it writes nothing rather than guessing.

Everything runs locally through static analysis of your source files. Your code never leaves your machine.

Where the MCP part comes in

Contour on its own is great for you, the human, reading annotations in your editor. But I wanted my AI agent to be able to ask Contour questions directly, not rely on me manually copying context and pasting it in. That's Contour MCP.

It's a separate extension that installs side by side with plain Contour, so you don't have to pick one. It bundles a local, read only MCP server that exposes the same trace index to Claude Code, Claude Desktop, or any other MCP client. Same indexing engine underneath, consumed as a versioned package synced from the same source rather than a hand maintained fork, so upstream fixes land on both sides.

On first activation it writes a local, git ignored MCP server registration, .mcp.json for Claude Code, .vscode/mcp.json for VS Code's own MCP support, pointing at a bundled Node script. No separate install step. And it never auto approves itself. Your MCP client's own trust prompt still applies, same as adding any other MCP server by hand.

Why this actually reduces wasted work

This is the part I'd push on if you're wondering whether it's worth adding. Without something like this, an agent working on a task has to rediscover the shape of your system every single time it's asked to touch something, by grepping around, opening files, following imports, guessing at what calls what. That's slow, it burns context window on exploration instead of on the actual task, and it has to be redone from scratch on the next task because the agent has no memory of what it found last time.

With Contour MCP, that exploration has already been done once, ahead of time, by the indexer, and it's kept current. The agent asks a direct question and gets back the answer instead of re-deriving it by wandering through five files again. Less exploring, less guessing, more of the agent's effort actually going toward the change you asked for.

The five tools

All five are read only. None of them writes a file, runs a command, or calls an external API, not even Jira. If a flow has a Jira key tagged on it, you only get what's stored locally, never a live lookup.

Tool Input What you get back
list_flows entity Flow summaries (endpoints, scheduled tasks, service calls, events) touching a given file, service, or table
get_trace file, symbol? The scoped call chain, UI or consumer through API through service to database
get_flow jira_key The flow tagged to that ticket, description, tags, test coverage, as stored locally
get_drift scope? Where the traced architecture and the actual code have diverged, optionally scoped
export_context scope, task A minimal, packaged context bundle sized for a specific task

What that looks like in practice once the server's connected:

list_flows: "What flows touch OrderController?"

get_trace: "Trace OrderController.java end to end, from the API down to the database."

get_flow: "What's the flow tagged to ORD-123, and is it tested?"

get_drift: "Has anything drifted between the frontend and backend in the orders service?"

export_context: "I need to add a cancel order endpoint, give me the minimal context for that."

That last one is my favorite. Instead of an agent reading through six files to piece together what adding a cancel order endpoint even touches, it asks once and gets back exactly the shaped context it needs, nothing more, nothing less.

Staleness is handled, not just flagged

I was pretty paranoid about this part while building it. Nothing kills trust in a tool like this faster than an agent confidently telling you something that used to be true. So this was solved directly rather than left as a caveat.

The MCP server never trusts an in memory copy. Every time Contour finishes a reindex, full or incremental, never on a background timer, it atomically rewrites a cache file in your workspace, .contour-mcp/cache.json. The MCP server reads that file fresh from disk on every single tool call, so an agent's answer reflects your latest edit.

If the cache is missing, or a file your query touches was edited after the cache was last built, or the cache has passed its configurable max age (contourMCP.cacheStalenessThresholdMinutes, 360 minutes by default), you don't get a best guess answer. You get a clear message telling you to let Contour finish reindexing. The tool would rather say it doesn't know yet than make something up.

Who this is and isn't for

I'll be straight about this. It's not a general purpose tool. It's built specifically for Spring MVC style controllers on the backend and axios/fetch style calls on the frontend. If your stack looks different, results will be limited or empty, by design, not as a bug I haven't gotten to yet.

It works best with backend and frontend folders opened together in one multi root VS Code workspace. Backend only or frontend only still works, you just lose the cross linking that makes this genuinely useful.

A few honest limitations worth knowing upfront:

  • This is static analysis, not a compiler and not a runtime tracer. It reads source text and annotations. Highly dynamic or reflection heavy code may not be traced properly.
  • Solace JCSMP event detection specifically is built from documented reference patterns rather than validated against a wide range of real production codebases, and it's clearly marked with a lower confidence badge wherever it appears.
  • Raw JDBC access outside a repository, or non JPA data layers, are a known limitation, not silently ignored.

Getting started, the short version

I'm not turning this into a full setup tutorial, the extension pages have the full config reference if you need custom annotations or a non standard project layout. Broadly though:

  1. Install Contour if you want the in editor experience, inline CodeLens annotations, drift detection, visual reports, AI context copy, with no MCP required.
  2. Install Contour MCP instead if you want that same trace data queryable by Claude Code, Claude Desktop, or another MCP client. It's a superset, you lose nothing by picking it, and both can coexist if you're not sure yet.
  3. Open your backend and frontend folders together as one workspace, let it index automatically, and look for the inline annotations plus the Contour view in your sidebar.

If your project has a slightly unconventional setup, there's a contour.config.json you can drop in with full editor autocomplete once it's named correctly, and a generator command for the cases it can detect automatically.

Why I think this actually matters

I don't think the answer to "AI agents don't understand my architecture" is "write better prompts" or "paste more code into context." That's a manual, lossy translation of something that should just be queryable directly. The real fix is giving the agent, and honestly yourself, a live and accurate source of truth for how the system is actually wired together, across REST, service calls, schedulers, and events, not how it was wired together when someone last updated an architecture doc.

That's the bet behind Contour and Contour MCP. Traced, not guessed. Local, not phoned home. Read only, so it can never make things worse. Honest about what it doesn't know yet, and current about what it does.

If you're living in a Spring Boot plus React codebase and you've lost an afternoon reconstructing a call chain by hand, or watched an agent confidently break a contract it didn't know existed, give it a shot and let me know what breaks. If you find a pattern it doesn't recognize, open an issue, that feedback is genuinely how this gets better.


Links if you want to look at it yourself:

Top comments (0)