Contour MCP
Everything Contour
does endpoint↔consumer mapping, full chain tracing (UI → API → service → database),
drift detection, SOLID structural lint, test coverage overlay, scheduled-task /
service-to-service / event-driven flow tracking, use-case tagging with Jira linkage, and
the shareable visual report plus a bundled local, read-only MCP server that exposes
the same trace index to Claude Code, Claude Desktop, and other MCP clients.
This extension installs side by side with plain Contour you don't need to choose one. If you don't
need the MCP server, install
Contour
instead; this is a superset of it.
How this differs from vanilla Contour
- The same trace engine, packaged for this extension. The indexing, matching, drift
detection, coverage overlay, and use-case tagging live in
@contour/trace-engine-mcp, a workspace package inside this repo.
- Plus an MCP server. On first activation this extension 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 each
client's own trust prompt still applies (this extension never auto-approves itself).
- The trace index becomes a cache file, not just in-memory extension state. Every
time Contour finishes a reindex (full or incremental never a separate timer), it
atomically rewrites
.contour-mcp/cache.json in your workspace. The MCP server reads
that file fresh on every tool call, so an agent's answer always reflects your latest
edit, or clearly tells you to wait for Contour to finish reindexing rather than
guessing.
Install
Install from the Marketplace (search "Contour MCP"), or build and install a .vsix
locally:
npm install
npm run compile
npx vsce package
code --install-extension api-contract-tracer-mcp-*.vsix
Open a workspace with at least one backend (Java/Spring) and one frontend (React/JS)
folder. Contour MCP indexes automatically on activation; watch the "Contour MCP" output
channel for progress. Once the first index completes, the bundled MCP server is
registered automatically Claude Code or VS Code will prompt you to trust it the first
time it's discovered, same as adding any other MCP server by hand.
All read-only. None writes a file, runs a command, or calls any external API including
Jira: if a flow is tagged with a Jira key, only what's already stored locally is
surfaced, never a live lookup. get_coupling and list_hotspots are only registered
while Coupling Map is enabled — genuinely absent from the tool list
while it's off, not just hidden.
| Tool |
Input |
Returns |
list_flows |
entity |
Flow summaries (endpoints, scheduled tasks, service calls, events) touching a file, service, or table |
get_trace |
file, symbol? |
The scoped call chain for a file UI/consumer → API → service → 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 real code have diverged, optionally scoped |
export_context |
scope, task |
A minimal, packaged context bundle sized for a specific task |
get_coupling |
target |
Whether a DTO, service method, or entry point is shared across unrelated parts of the system, and how risky (good/warning/critical/excluded/unknown) |
list_hotspots |
minSeverity?, limit? |
The ranked list of shared-contract/high-fan-in coupling findings, worst first |
Example prompts, once the server is 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."
get_coupling "Is OrderSummaryDto safely shared, or is it hidden coupling?"
list_hotspots "What are the worst coupling hotspots in this codebase right
now?"
The chain tracer understands real Spring DI now
Interfaces with more than one real implementation used to be invisible to the tracer.
Now every hop in a chain resolves through a priority chain — an explicit
chains.beanOverrides config hint, then @Primary, then @Qualifier/bean-name
matching, then (the common case) the single remaining candidate — and when none of
those disambiguate it, the chain says so explicitly instead of guessing:
-> JdbcOwnerRepositoryImpl.findById() [repository, ambiguous]
[note: 2 candidate implementations, none disambiguated — see chains.beanOverrides | @Profile("jdbc")]
Every hop also carries a confidence (certain / resolved / ambiguous) and, where
relevant, a note disclosing an @Transactional/@Async/@Cacheable AOP annotation, a
@Profile/@ConditionalOnProperty conditional-wiring guard, or a
@PreAuthorize/@Secured/custom authorization annotation on that hop — verbatim,
including arguments, never evaluated or assumed. DDD/hexagonal (ports-and-adapters)
layouts are supported too: a domain-layer repository interface with no Spring Data
ancestry, implemented by an @Repository-annotated infrastructure adapter, resolves
correctly — a record or enum in that adapter role (not just a class) resolves the
same way. All of this is configurable per project under chains in
contour.config.json — see
docs/indexing-engine-upgrades.md for the full
config reference and the evidence behind each of these claims (real bugs found on real
open-source codebases, not just synthetic fixtures).
Controllers built contract-first from an OpenAPI spec are indexed too
A Controller that implements an OpenAPI-codegen-generated interface (the
interfaceOnly: true pattern common with openapi-generator-maven-plugin) puts its real
@RequestMapping/@GetMapping/etc. annotations on the generated interface's methods, not
on the @Overrides in your own source the generated interface itself lives under
target/, which the indexer correctly never treats as project source. The endpoint
indexer now resolves those @Override methods back to their generated interface's own
mapping annotation, scoped per backend service so it can't cross-resolve across unrelated
services in a monorepo. Confirmed against real spring-petclinic-reactjs: previously 7 of
its 8 controllers indexed to zero endpoints; now all 8 resolve correctly. Works out of the
box for the common Maven default output path
(target/generated-sources/openapi/**); add more locations via backend.generatedApiGlobs
in contour.config.json for a Gradle build or a customized <output>.
SOLID structural lint
Advisory-only Problems-panel warnings, off by default (contourMCP.solidLint.enabled).
Three checks today, each independently toggleable via the solidLint namespace in
contour.config.json:
- Controller → Repository layering a Controller calling a Repository directly,
skipping the Service layer.
- Field injection
@Autowired on a field instead of a constructor. Applies
everywhere, not just Controllers; never fires inside src/test/**, where field-level
@Autowired/@MockBean is normal and still recommended by Spring itself.
- Entity leak a
@Entity-annotated class returned from, or accepted via
@RequestBody into, a Controller endpoint coupling the API shape directly to the
database schema.
All three default on once the setting is enabled; set solidLint.fieldInjection and/or
solidLint.entityLeak to false in contour.config.json to turn a specific one off
without disabling the rest. Every check was validated against real Spring Boot source
before shipping, not just synthetic fixtures the field-injection detector, for example,
was proven against a real 2-true/9-false-if-naive split in production code, where a
naive "flag any @Autowired" regex would have wrongly caught 9 real constructor/method
injections alongside the 2 real field-injection violations.
Coupling Map
Off by default (contourMCP.couplingMap.enabled). A different question than the SOLID
lint above: instead of "is this one class well-formed," it asks "is this DTO or service
method being reused by two or more unrelated parts of the system in a way that
creates hidden coupling" — built on the same traced call graph, plus two lightweight
signals (cognitive complexity, integration fan-out). A bare-minimum, defensible signal,
not a full architecture-governance suite.
Tuning lives under a couplingMap namespace in contour.config.json: exclude
(built-in defaults plus your own package/name-pattern exclusions), markedShared
(targets a human has reviewed and accepted as intentionally shared), relatedness (how
flows get clustered), and theme (webview colors). Marking, unmarking, or including an
excluded target back into analysis is always a human decision made in the editor —
never something an MCP client can do; get_coupling and list_hotspots (see the tools
table above) are read-only, same guarantee as the rest of the server.
Two commands:
Contour MCP: Open Coupling Map — a webview with three views: Overview (every
cluster, sized by scope, ringed by risk), Detail (one flow's full internal chain plus
any bridge into another flow), and Matrix (every pair of flows at once). Click
through to mark/unmark a target as intentionally shared, or include an excluded
target back into analysis — both write straight to contour.config.json.
Contour MCP: Export Coupling Map — the same map as a single, self-contained HTML
file for sharing with someone who doesn't have the extension installed. Pan/zoom,
search, and hover all still work standalone; marking and click-to-navigate need the
live webview.
Staleness
The MCP server never trusts an in-memory copy it re-reads .contour-mcp/cache.json
from disk on every call. If the cache is missing, or a file your query touches was
edited after the cache was last built, or the cache itself has passed its backstop max
age (contourMCP.cacheStalenessThresholdMinutes, default 360 minutes), you get a clear
message to let Contour finish reindexing never stale or fabricated data.
Further reading
docs/indexing-engine-upgrades.md DI resolution,
DDD/hexagonal support, the confidence/disclosure model, targeted invalidation, and the
optional tree-sitter AST parsing path, each claim backed by a test or a real-repo
validation run.
docs/mcp-technical-writeup.md why handing an
already-computed trace to an AI agent beats letting it re-derive the same call graph
by reading files, with token-count evidence from a real trace.