Contract Testing with MCP¶
Self-describing, self-testing APIs on the IDP — powered by contract-mcp-server and the contract-assistant KAgent agent.
The Problem¶
Static OpenAPI specs drift from reality. Manually maintained Pact suites break silently. Most teams only discover API incompatibilities in production, after a consumer has already deployed against a provider that changed an endpoint.
The Solution¶
Every service exposes its live contract via GET /openapi.json. The contract-mcp-server collects those contracts, generates Pact consumer tests automatically (with realistic schema matchers, not just status codes), detects breaking changes between versions, and validates compatibility across the whole platform. The contract-assistant AI agent wraps all of this as natural-language workflows.
Using Contract Testing from Backstage¶
Everything below is available directly from the Backstage UI — no CLI, no curl, no local setup required.
1. Chat with the AI Assistant¶
Open http://backstage.idp.local/ai-assistant and select the contract-assistant agent. Use plain English:
| Goal | What to type |
|---|---|
| Onboard a service | "Fetch and register the contract for payments-api in services-prod" |
| Discover all services in a namespace | "Discover contracts for all services in services-dev" |
| Generate Pact consumer tests | "Generate Pact tests for checkout-service consuming payments-api v1.2.0" |
| Check if a consumer is compatible | "Is checkout-service compatible with the latest payments-api?" |
| Get a full compatibility matrix | "Show the compatibility report for payments-api" |
| Detect breaking changes | "Did anything break between payments-api v1.0.0 and v2.0.0?" |
| Inspect a registered contract | "Show me the contract for inventory-service" |
| List all registered services | "List all registered contracts" |
The agent automatically calls the right underlying tool and returns a human-readable summary. For generate_contract_tests, it returns both the TypeScript test file and the Pact JSON inline — paste them into your repo.
2. Scaffold a Contract Test Project¶
Open http://backstage.idp.local/create and choose one of two templates:
Contract Testing Suite — for new consumer test projects:
1. Search Contract Testing Suite → click Choose
2. Fill in consumer name, provider name, provider base URL, Pact broker URL
3. Choose new-repository (creates a standalone repo) or add-to-existing (opens a PR against your service repo)
4. Click Create — Backstage scaffolds:
- tests/<name>.pact.spec.ts — Pact V3 tests with MatchersV3 matchers
- contract/openapi.yaml — editable consumer contract
- .github/workflows/contract.yml — CI: run → publish → verify
- catalog-info.yaml — registers the test suite in the Backstage catalog
Enable Contract Testing — one-click onboarding for an existing service:
1. Search Enable Contract Testing → click Choose
2. Enter your service name and namespace
3. Click Create — Backstage auto-discovers the service's live /openapi.json, registers it, and wires an ArgoCD hook so every future deploy re-registers automatically
3. View Contract Status in the Catalog¶
After onboarding, your service's contract status is visible in the Backstage Catalog:
- catalog-info.yaml entities of type test-suite link back to their provider
- The contract-assistant agent can be asked about any catalog entity by name
Architecture¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
Components¶
| Component | Location | Purpose |
|---|---|---|
contract-mcp-server |
services/contract-mcp-server/ |
Central contract registry + MCP server |
contract-assistant KAgent |
kubernetes/kagent/contract-agent.yaml |
AI agent for natural-language workflows |
ai-gateway |
kubernetes/kagent/ai-gateway-toolserver.yaml |
The single RemoteMCPServer CRD. The contract tools reach agents through the AI Gateway's contract target, configured in kubernetes/ml-platform/ai-gateway.yaml |
Backstage template — contract-testing-suite |
backstage/catalog/templates/contract-testing-suite/ |
Scaffold a full Pact test project |
Backstage template — enable-contract-testing |
backstage/catalog/templates/enable-contract-testing/ |
One-click onboarding for existing services |
| GitHub Actions workflow | .github/workflows/contract-check.yml |
Per-PR breaking change gate |
| Helm values | services/contract-mcp-server/helm-values-*.yaml |
Kubernetes deployment config |
Access Points¶
| Interface | URL | What you can do |
|---|---|---|
| KAgent UI | http://kagent.idp.local | Chat with contract-assistant |
| Backstage AI Assistant | http://backstage.idp.local/ai-assistant | Same agent, inside Backstage |
| Backstage Templates | http://backstage.idp.local/create | Scaffold a Pact test project |
| MCP endpoint | http://contract-mcp-server.idp.local/mcp | Direct JSON-RPC tool calls |
| REST API | http://contract-mcp-server.idp.local/api | Team-friendly HTTP API |
| Health | http://contract-mcp-server.idp.local/healthz | Storage type + discovery mode |
| Metrics | http://contract-mcp-server.idp.local/metrics | Prometheus counters + histograms |
End-to-End Flows¶
Flow 1 — Service onboards (self-describing)¶
1 2 3 4 5 6 7 | |
Flow 2 — PR breaks a consumer (CI gate)¶
1 2 3 4 5 6 7 8 9 10 | |
Flow 3 — Consumer generates their Pact tests¶
1 2 3 4 5 6 7 8 9 10 | |
Flow 4 — AI agent workflow (natural language)¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Making a Service Self-Describing¶
Add GET /openapi.json to your service. The response must be a valid OpenAPI 3.x document.
Go (reference: hello-service)¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
Node.js / Express¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
The richer your schemas (requestBody, response schemas, required fields, format, enum), the more useful the generated Pact tests will be. The generator extracts these to produce like(), integer(), regex(), and eachLike() matchers automatically.
Generated Pact Tests — What They Look Like¶
Given a provider spec with a POST /pets endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
The test code is a production-ready starting point. You should:
- Replace like('string') placeholders with realistic domain values
- Add provider state setup if your provider needs seeded data
- Publish the resulting ./pacts/ directory to your Pact broker
MCP Tool Reference¶
All tools are callable via POST /mcp (JSON-RPC 2.0, Accept: application/json, text/event-stream).
fetch_service_contract¶
Pull /openapi.json from a running service and auto-register it as a contract.
1 2 3 4 5 6 7 | |
Response includes: paths[], schemas{} (per-operation parameters + requestBody + responses + examples), source, version, title.
auto_discover_contracts¶
Scan every service in a namespace and register all that expose /openapi.json.
1 2 3 4 5 6 7 | |
Response: { scanned, discovered, skipped, errors, services: [{serviceName, status, paths}] }
register_contract¶
Manually push an OpenAPI spec (JSON or YAML string).
1 2 3 4 5 6 7 8 | |
Fires the breaking-change webhook automatically if a previous version exists and breaking changes are detected.
get_contract¶
Retrieve a stored contract. Returns the full spec plus a schemas field with per-operation context (parameters, requestBody, response examples) for AI agent consumption.
1 2 3 4 5 6 7 | |
Response shape:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
list_contracts¶
List all registered services and their available versions.
1 2 3 4 5 | |
generate_contract_tests¶
Generate Pact consumer tests from a registered provider spec.
1 2 3 4 5 6 7 | |
Returns:
- pactJson — Pact v3 interaction JSON with real request/response bodies (drop into ./pacts/)
- testCode — TypeScript using MatchersV3: like(), integer(), decimal(), regex(), eachLike()
- instructions — where to save the files and how to run them
validate_compatibility¶
Check if a provider's current spec satisfies all paths expected by a consumer (both must be registered).
1 2 3 4 5 6 7 | |
Returns: compatible: true/false, missingPaths, and a human-readable verdict.
detect_breaking_changes¶
Compare two versions of a service spec. Detects: path_removed, method_removed, required_param_added.
1 2 3 4 5 6 7 | |
Returns: { breaking: [{type, path, method, detail}], nonBreaking: [...], summary }.
get_compatibility_report¶
Full consumer/provider compatibility matrix: every registered service checked as a potential consumer.
1 2 3 4 5 6 7 | |
Returns: { totalConsumers, compatible: N, incompatible: N, consumers: [{consumer, compatible, missingPaths}] }.
REST API Reference¶
Useful for CI pipelines that don't need the full MCP protocol.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/contracts/:service/:version |
X-Api-Key |
Register contract (body: raw OpenAPI JSON/YAML) |
GET |
/api/contracts/:service |
none | Get latest contract (?version= for specific) |
GET |
/api/contracts |
none | List all registered services |
GET |
/api/compatibility/:provider/:consumer |
none | 200 = compatible, 409 = broken |
POST |
/api/breaking-changes |
none | Body: {service_name, from_version, to_version} |
CI example using the REST API:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Claude Desktop / Custom MCP Client Setup¶
To use the contract testing tools directly from Claude Desktop or any MCP-compatible client:
1 2 3 4 5 6 7 8 | |
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS). After restarting Claude Desktop, the 9 tools appear automatically and you can use them in any conversation: "use register_contract to register this OpenAPI spec for payments-api v2.0.0: ..."
Note: Always set Accept: application/json, text/event-stream for raw MCP calls. Without it the StreamableHTTP transport returns 406.
Backstage Templates¶
contract-testing-suite¶
Generates a full Pact test project for a consumer/provider pair.
- Open Backstage → Create → search "Contract Testing Suite"
- Fill in: Consumer name, Provider name, Provider base URL
- Choose new-repository or add-to-existing
- Click Create
Generated files:
- contract/openapi.yaml — consumer's expected contract (edit to match your actual API calls)
- tests/<name>.pact.spec.ts — Pact V3 consumer tests with MatchersV3
- .github/workflows/contract.yml — CI: run tests → publish to Pact broker → provider verify
- catalog-info.yaml — registers in Backstage catalog
enable-contract-testing¶
One-click setup for an existing service:
1. Deploys/wires contract-mcp-server if not already running
2. Registers contract-assistant KAgent
3. Auto-discovers the service's OpenAPI contract via fetch_service_contract
4. Scaffolds a consumer test skeleton
5. Applies ArgoCD Helm hook for automatic re-registration on every sync
ArgoCD Self-Testing Hooks¶
Any service using helm/service-template can opt into automatic contract checking on every ArgoCD sync.
1 2 3 4 | |
PostSync (after every deploy): calls fetch_service_contract + get_compatibility_report — registers the running spec and logs consumer compatibility status.
PreSync (block breaking deploys):
1 2 3 4 5 | |
Before syncing, a Job calls detect_breaking_changes. If any breaking changes are found, the Job exits 1 and ArgoCD blocks the sync.
CI pattern to wire versions into Helm:
1 2 3 4 5 6 7 | |
contract-assistant — Quick Reference¶
Open http://kagent.idp.local → select contract-assistant, or use Backstage AI Assistant.
| Goal | Prompt |
|---|---|
| Make all services self-describing | "Discover contracts for all services in services-dev" |
| Pull one service's spec | "Fetch the contract for hello-service" |
| See schemas and examples | "Get the contract for payments-api" |
| Generate Pact tests | "Generate contract tests for hello-service consumer frontend-app" |
| Check compatibility | "Is frontend-app compatible with hello-service?" |
| Detect breaking changes | "Breaking changes between my-service v1.0.0 and v2.0.0?" |
| Full compatibility audit | "Show the compatibility report for hello-service" |
| List registered services | "List all registered contracts" |
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
PORT |
3003 |
HTTP listen port |
STORAGE_TYPE |
memory |
memory / postgres / dynamodb |
DATABASE_URL |
— | PostgreSQL connection string (required if STORAGE_TYPE=postgres) |
AWS_REGION |
us-east-1 |
AWS region (required if STORAGE_TYPE=dynamodb) |
DYNAMO_TABLE |
contract-registry |
DynamoDB table name |
DISCOVERY_MODE |
kubernetes |
kubernetes / http / docker |
K8S_API |
https://kubernetes.default.svc |
Kubernetes API server URL |
K8S_TOKEN |
— | Service account bearer token (for out-of-cluster use) |
SERVICES_REGISTRY |
— | name1=http://url1,name2=http://url2 (http discovery mode) |
DOCKER_HOST |
/var/run/docker.sock |
Docker socket path (docker discovery mode) |
DISCOVER_PROBE_TIMEOUT_MS |
2500 |
Timeout per service when probing for /openapi.json |
DISCOVER_CONCURRENCY |
10 |
Max concurrent probes during auto_discover_contracts |
HTTP_TIMEOUT_MS |
8000 |
General HTTP call timeout |
BREAKING_CHANGE_WEBHOOK_URL |
— | URL to POST breaking-change events (Slack, PD, etc.) |
API_KEY |
— | If set, write operations require X-Api-Key header |
Storage Backends¶
STORAGE_TYPE |
Persistence | When to use |
|---|---|---|
memory |
Lost on restart | Local dev, ephemeral CI |
postgres |
Persistent | Self-hosted, local Docker Compose |
dynamodb |
Persistent, managed | AWS-native deployments |
DynamoDB table setup:
1 2 3 4 5 6 7 8 9 10 | |
Breaking Change Webhook Payload¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Wire to Slack: set BREAKING_CHANGE_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK.
Redeploy After a Code Change¶
1 2 3 4 | |
Trade-offs, Risks & Governance¶
"AI-driven testing" covers two genuinely different code paths here, and the risk profile of each is different. Knowing which one you're relying on for a given decision matters.
Deterministic path (no LLM in the loop). generate_contract_tests, detect_breaking_changes, and validate_compatibility are plain schema diffing over the registered OpenAPI documents (see generator.ts / store.ts) — same input, same output, every time. This is the part that's safe to gate CI on: contract-check.yml and the ArgoCD PreSync hook call these tools directly over HTTP, never through a model. There's no hallucination risk here because there's no model in this path.
Conversational path (LLM in the loop). Asking contract-assistant "Can I deploy payments-api 2.0.0-pr-4?" goes through an LLM that calls can_i_deploy and then paraphrases the result. The tool call and its JSON result are deterministic; the sentence wrapped around it is not. Don't treat the chat answer as the source of truth — treat the underlying tool result (visible in the audit log, and in the raw CI check output) as the source of truth, and the chat answer as a convenience layer on top of it.
Failure modes to plan for:
- Misreading a "safe" answer. If a consumer's contract is stale or was never registered, can_i_deploy has nothing to check against and returns safe-by-omission, not safe-by-verification. Treat "no registered consumers" as "unknown," not "clear." (get_stale_contracts exists precisely to surface this before it bites you.)
- Non-determinism at the chat layer. Two people asking the same question in different phrasing can get answers with different emphasis or wording, even though the underlying tool call is identical. For anything gating a merge or a deploy, the CI check / can_i_deploy tool result is authoritative — the chat transcript is not audit evidence.
- Incomplete or malformed specs. generate_contract_tests degrades gracefully on missing schema fields (falls back to like(null) matchers), which means a sparse OpenAPI doc silently produces a weak test rather than an error. Weak tests pass more often than they should.
Governance today:
- Audit trail. Every register / breaking-change-detected / compatibility-check event is logged and queryable via get_audit_log — this is the mechanism for "who registered what, when," and it's what you'd pull for a post-incident review.
- Write authorization. Setting API_KEY requires X-Api-Key on all write operations (register_contract, etc.), so anyone can read contracts but only holders of the key can register or overwrite one. In the current demo/POC deployment this is unset — fine for a conference cluster, not fine for production, where it should be scoped per-CI-pipeline via distinct keys or replaced with the AWS_ROLE_ARN OIDC path already wired into contract-check.yml.
- What's not governed yet. There's no approval workflow for overwriting an existing registered version (last write wins), and no role distinction between "can register a contract" and "can override a breaking-change block." For a platform beyond POC scope, both would need to move from "technically possible" to "explicitly designed."
Troubleshooting¶
fetch_service_contract returns "No OpenAPI spec found"
The service does not expose /openapi.json. Add the endpoint and redeploy. The probe checks: /openapi.json, /openapi.yaml, /api-docs, /swagger.json.
auto_discover_contracts shows all services as no_spec
Services do not expose /openapi.json, or the port is wrong. Most K8s Services listen on port 80. Try "port": 80. Check with: curl http://<service>.<namespace>.svc.cluster.local/openapi.json.
detect_breaking_changes says "version not found"
The version must be registered with register_contract before you can diff it. Register the new version in CI before deploying.
Generated tests compile but all assertions are trivial
Your OpenAPI spec has no requestBody or response schema fields — only path/method definitions. Enrich your spec with schemas, required fields, and format hints (see "Making a Service Self-Describing" above). The generator extracts these to build matchers.
KAgent contract-assistant shows READY=False
1 2 | |
MCP call returns empty or no data: lines
Ensure Accept: application/json, text/event-stream is set. Without it, the StreamableHTTP transport may return 406.
generate_contract_tests returns "provider not found"
The provider must be registered before generating tests. Call fetch_service_contract or register_contract for the provider first, then generate.
can-i-deploy returns 404
The service is not registered in the contract registry. Register it first: run fetch_service_contract (AI Assistant: "Register the contract for <service>") or POST directly to /api/contracts/<service>/<version>.
Breaking changes not detected between versions
Both registered versions must have different content. If you registered the same spec twice under different version strings, the diff will be empty. Always register the new (changed) spec before calling detect_breaking_changes.
ArgoCD PreSync hook fails immediately with "fromVersion not set"
The PreSync hook requires contractCheck.fromVersion and contractCheck.toVersion to be passed via Helm values in your CI pipeline. Add --set contractCheck.fromVersion=<old-version> --set contractCheck.toVersion=<new-version> to your helm upgrade command.
Audit log is empty after a pod restart
The audit log is stored in-memory (circular buffer, 500 events). It is lost when the pod restarts. For a persistent audit trail, switch to STORAGE_TYPE=postgres — the audit events will then survive restarts.
Template not visible in Backstage Create page
The contract templates may be commented out in backstage/catalog/all-templates.yaml. Uncomment the contract-testing-suite and enable-contract-testing entries, then restart Backstage (docker compose restart backstage for local, or sync the ArgoCD app in-cluster).
contract-assistant not responding in AI Assistant
The KAgent agent may not be deployed. Check: kubectl get agents -n kagent contract-assistant. If it's missing, run scripts/bootstrap-ai.sh. If it exists but shows READY=False, restart the controller: kubectl rollout restart deployment/kagent-controller -n kagent.
auto_discover_contracts returns 0 services
In local/Docker mode, set DISCOVERY_MODE=http and SERVICES_REGISTRY=payments-api=http://payments-api:8000,hello-service=http://hello-service:8080. The default kubernetes mode requires in-cluster API access. In standalone Docker Compose, use the http mode.
Common Pitfalls¶
- Don't call
/mcpdirectly for scripts — the REST API at/apiis easier to use fromcurl/ CI pipelines./mcpis for MCP-native clients (Claude Desktop, KAgent). - Don't use
STORAGE_TYPE=memoryin production — contracts are lost on every restart. Usepostgresordynamodb. - Don't skip
fetch_service_contractbeforegenerate_contract_tests— the provider must be registered first. - Don't omit
requiredfields and response schemas from your OpenAPI spec — the test generator uses these to build meaningful matchers. Without them, generated tests only check HTTP status codes.