Warmup — MCP and the Tool Estate, From Zero
Assumes Python, JSON and HTTP. Assumes nothing about JSON-RPC, MCP, JSON Schema, or semantic versioning.
Table of Contents
- 1. The problem MCP solves
- 2. JSON-RPC 2.0, completely
- 3. MCP's architecture
- 4. Schema enforcement
- 5. Versioning a tool estate
- 6. The estate's metadata
- 7. Authorization-aware discovery
- 8. Protocol errors versus tool errors
- 9. Lab walkthrough
- 10. Success criteria
- 11. Common mistakes
- 12. Interview Q&A
- 13. References
1. The problem MCP solves
An agent is useful only when it can do things, and doing things means calling systems. Before MCP, every combination of (agent framework, tool) was a bespoke integration: a LangChain tool class, an OpenAI function schema, a Bedrock action group, each written separately for the same underlying API.
With M frameworks and N systems that is M × N integrations. Each one is separately
written, separately reviewed, separately credentialed, and separately broken.
MCP makes it M + N. A system exposes one MCP server; any MCP-capable host can use it. The analogy the protocol's authors use is USB-C: one connector, many devices, and the device does not need to know which laptop is plugged in.
For a bank the saving is not primarily effort — it is inventory. When every tool is behind one protocol and one registry, there is a single place that knows what agents can do. That is the precondition for every governance conversation in the rest of this track, and it is why the JD names the "MCP server estate" as a thing to be engineered rather than merely adopted.
2. JSON-RPC 2.0, completely
MCP's wire format is JSON-RPC 2.0. It is a small spec and worth knowing exactly, because most implementation bugs are protocol bugs.
2.1 The three message shapes
Request — has an id, expects exactly one response:
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "payments.lookup", "arguments": {"reference": "PMT-771"}}}
Response — carries the same id, and exactly one of result or error:
{"jsonrpc": "2.0", "id": 7, "result": {"content": [{"type": "text", "text": "HELD"}], "isError": false}}
{"jsonrpc": "2.0", "id": 7, "error": {"code": -32602, "message": "arguments failed schema validation"}}
Notification — no id, and must not be answered:
{"jsonrpc": "2.0", "method": "notifications/tools/list_changed"}
The id rule is where implementations go wrong. The absence of id is not "the sender forgot" —
it is a semantic statement meaning do not reply. A server that replies to a notification breaks
clients that are not expecting a message; a client that waits for a reply to a notification hangs.
id may be a string or a number, but not a float (the spec discourages fractional parts
because of round-tripping), and not null in a request.
2.2 The reserved error codes
| Code | Name | When |
|---|---|---|
| −32700 | Parse error | invalid JSON |
| −32600 | Invalid Request | valid JSON, wrong shape (bad jsonrpc, missing method) |
| −32601 | Method not found | the method does not exist |
| −32602 | Invalid params | the method exists; the arguments are wrong |
| −32603 | Internal error | the server broke |
| −32000 … −32099 | reserved for implementation-defined server errors |
The distinction between −32601 and −32602 does real work in this lab: an undiscoverable tool returns −32601 (indistinguishable from nonexistent), while a schema violation on a tool you can see returns −32602 with the errors attached. One protects the estate from probing; the other helps the model fix itself.
The error object may carry data with anything you like. That is where the validation errors go.
2.3 Why JSON-RPC and not REST
A fair question in an interview. Three reasons:
- Bidirectional. MCP servers send messages to clients (notifications, and the two server-initiated request types in §3.4). REST is client-initiated by construction.
- Transport-agnostic. The same messages run over stdio (a local subprocess) and over HTTP. A local server that is a spawned process is a first-class case, and it has no URLs.
- Method-oriented, not resource-oriented.
tools/callis an action, not a resource manipulation. Forcing it into REST produces the usualPOST /tools/{name}/invocationsawkwardness with no benefit.
3. MCP's architecture
3.1 Host, client, server
┌──────────────────────── HOST (your agent kernel) ────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ client A │ │ client B │ │ client C │ one per server │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└────────┼───────────────────┼───────────────────┼─────────────────────────┘
│ stdio │ HTTP │ HTTP
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
│ payments │ │ crm │ │ policy │ MCP servers
│ server │ │ server │ │ server │
└──────────┘ └──────────┘ └──────────┘
- The host is the application — here, the agent kernel from Phase 01. It decides which servers to connect to and aggregates their tools.
- A client is a connection object, one per server. This one-to-one rule is deliberate: it keeps each server's capabilities, protocol version and state separate, so a misbehaving server cannot contaminate another's session.
- A server owns some tools, resources and prompts, and knows nothing about the other servers.
The consequence people miss: tool-name collisions are the host's problem. Two servers may both
export search. The host must namespace them, and the namespacing must be stable, because it ends
up in the model's prompt and in your audit records.
3.2 The initialize handshake
client → server initialize {protocolVersion, capabilities, clientInfo}
server → client {protocolVersion, capabilities, serverInfo}
client → server notifications/initialized
... normal operation ...
Three things happen at once:
Version negotiation. MCP versions are dates (2025-06-18). The client proposes; the server
either accepts or responds with a version it does support. The correct behaviour on mismatch is
offer an alternative, do not fail — a version skew between a client and one of twelve servers
should degrade that connection, not take down the host. The lab implements exactly this.
Capability declaration. Each side states what it supports: the server declares tools,
resources, prompts (each possibly with listChanged or subscribe); the client declares
sampling, elicitation, roots. This is what makes the protocol survive its own evolution — a
feature added in a later revision is simply not declared by older peers.
The initialized notification. Until the client sends it, the server should not consider the
session live. The lab enforces this: any method other than initialize before initialized
returns -32600. This matters because a server that starts pushing notifications before the
client is ready will drop them.
3.3 Tools, resources, prompts — three trust levels
The three primitives are usually explained by what they are. It is more useful to explain them by who is in control, because that is a security property:
| Primitive | Controlled by | Meaning | Risk |
|---|---|---|---|
| Tool | the model | an invocable function the model chooses to call | highest — the model decides, and it can be wrong or manipulated |
| Resource | the application | read-only context the host chooses to include, addressed by URI | medium — the content is untrusted even though the choice is not |
| Prompt | the user | a template the human explicitly selects | lowest — a human chose it |
Two design consequences:
- Anything the model can trigger needs a contract and a policy. That is why tools have schemas, scopes and side-effect classes, and resources do not.
- Resource content is still untrusted input. A document fetched by URI can contain an injected instruction. "Application-controlled" describes who chose to read it, not whether its contents are safe. This is the trust-boundary rule that Phase 11 is built on.
3.4 The two server-initiated directions
Two features invert the usual direction, and both are security-relevant enough that a bank should decide about them explicitly rather than by default:
- Sampling — a server asks the client's model for a completion. Useful (a server can do model-assisted work without its own API key); dangerous (a server can now spend your tokens, and can construct a prompt containing whatever context it likes).
- Elicitation — a server asks the user for input mid-operation. Useful (a tool can ask for a missing parameter); dangerous (a server can present arbitrary text to your user inside your trusted UI, which is a phishing surface).
Both are opt-in via client capabilities. The bank-grade default is off for third-party servers, considered individually for first-party ones, with the client mediating and logging every such request. The lab does not implement them; knowing why they need a decision is the point.
3.5 What MCP does not provide
Write this list down; it is the answer to the most likely interview question in this phase.
| Not in the protocol | Where it belongs |
|---|---|
| Authorization — who may call a tool | your registry + control plane (09) |
| Tenancy — whose tools these are | the registry's tenant visibility, this lab |
| Versioning — which contract you are calling | the registry's semver, this lab |
| Tool identity — that this server is really the payments server | workload identity (08) |
| Rate limiting and quotas | the gateway (04) |
| Idempotency and transactional safety | the action gateway (10) |
| Audit | everywhere, joined by the chain (01, 15) |
| Data classification | the registry, this lab |
The spec does define an authorization framework for HTTP transports (OAuth 2.1 resource-server
behaviour: protected-resource metadata, token audience validation, WWW-Authenticate challenges).
That is about authenticating the caller to a remote server — it is not an authorization model
for which tool a given agent may see, which is what a bank actually needs, and which is what the
lab builds.
4. Schema enforcement
4.1 JSON Schema, the useful subset
Every MCP tool declares an inputSchema in JSON Schema. You do not need the whole dialect; the
subset that earns its keep in a tool contract:
| Keyword | Purpose |
|---|---|
type | object, array, string, integer, number, boolean, null |
properties, required | object shape |
additionalProperties: false | close the object — the single most useful keyword for catching hallucinated arguments |
enum, const | closed value sets |
minimum/maximum, exclusive* | numeric ranges — where "amount must be positive" lives |
minLength/maxLength, pattern | string shape — account and reference formats |
items, minItems/maxItems, uniqueItems | arrays |
One Python-specific trap the lab tests: bool is a subclass of int. A naive
isinstance(value, int) accepts True for {"type": "integer"}, so amount: true sails through
into a payment call. The check must be isinstance(v, int) and not isinstance(v, bool).
The converse asymmetry is correct and deliberate: 5 is a valid number (JSON has one
numeric type and integers are a subset), but 5.0 is not a valid integer.
4.2 Why all errors, sorted, with paths
A validator that raises on the first error forces a serial repair loop: the model fixes one problem, resubmits, learns about the next. Three round-trips, three model calls, three chances to make something else wrong.
Returning all errors, each with a path ($.legs[1], $.amount), lets one repair turn fix
everything. And sorting them makes the repair prompt deterministic — which matters more than
it sounds, because a non-deterministic prompt defeats prefix caching and makes failures
irreproducible.
One refinement in the lab: when a value's type is wrong, further checks on that value are suppressed. Reporting "expected integer, got string" and "below minimum 1" for the same field is noise, and noise in a repair prompt lowers the repair success rate.
4.3 The repair loop
Before asking the model to try again, fix what is unambiguous:
| Situation | Repair |
|---|---|
"42" where the schema says integer | coerce to 42 |
42 where it says string | coerce to "42" |
"true" where it says boolean | coerce to True |
an extra property under additionalProperties: false | drop it |
| a missing required field with a platform default | fill it |
| a missing required field without a default | leave it — that is the model's job |
That last row is the discipline. Inventing an account number, a reference or an amount is not a repair; it is fabrication, and in a bank it is the difference between a helpful platform and an incident. The rule: repair syntax, never semantics.
The loop must also be idempotent — repairing twice gives the same result — or a retry mechanism built on it will oscillate.
4.4 The description is prompt surface
A tool's description is not documentation. It is text that goes into the model's context on
every single turn, and it is the primary input to tool selection. Consequences:
- It costs tokens on every request. Sixty tools with 200-token descriptions is 12 000 tokens before the user has said anything — which is why authorization-filtered discovery (§7) is a cost control as well as a security one.
- Its quality determines
p. From Phase 00, task success is \( p^n \), and per-steppis dominated by whether the model picks the right tool. A description that says when not to use the tool is often worth more than one that says what it does. - Platform metadata must not be in it. Scopes, classifications, owners and tenant lists are for your control plane. Putting them in the description wastes context and tells a prompt-injecting adversary the shape of your control model.
A good bank tool description names the system of record, the freshness, and the boundary:
"Return the current status, amount and counterparties of a wholesale payment by its reference. Reads the payments system of record; data is real-time. Does not cover card transactions or retail transfers — use
retail.transfers.lookupfor those."
5. Versioning a tool estate
5.1 Semantic versioning and constraints
MAJOR.MINOR.PATCH, where major means breaking, minor means backward-compatible addition, patch
means neither. Callers express what they can tolerate:
| Constraint | Matches | Meaning |
|---|---|---|
1.2.3 | exactly that | maximum pinning; you will be stuck on a retired version one day |
~1.2.3 | 1.2.x, x ≥ 3 | patch updates only |
^1.2.3 | 1.x.y, ≥ 1.2.3 | anything non-breaking — the sensible default |
* | anything | fine for a read tool in a sandbox, never for a production agent |
The reason a platform cares: a pin is what makes a deprecation window possible. Without pins, every publish is a fleet-wide change and there is no window at all.
5.2 The rule: who breaks?
The entire classification reduces to one question: does the new contract accept everything the old one accepted?
MAJOR (breaking) — the contract got stricter:
| Change | Why it breaks |
|---|---|
| add a required property | existing calls omit it |
| remove a property | existing calls send it (and with additionalProperties: false, are rejected) |
| change a type | existing values are now wrong |
| narrow an enum | a previously valid value is now invalid |
raise a minimum/minLength/minItems, or lower a maximum/maxLength/maxItems | previously valid values fall outside |
set additionalProperties: false where it was open | previously tolerated extras are now rejected |
MINOR (compatible) — the contract got looser:
| Change | Why it is safe |
|---|---|
| add an optional property | old calls still validate |
| remove a required property | old calls still validate (they sent it; it is now ignored) |
| widen an enum | everything old is still allowed |
PATCH — descriptions, titles, examples. Note that a description change is not semantically neutral for a model — it can change tool selection — which is a real argument for treating description-only changes as minor and re-running your evaluation suite. Reasonable people differ; what matters is that you decide and encode it.
The lab's registry enforces the classification at publish time. That is the point: a rule in a
wiki is a suggestion, a rule in publish() is a control.
5.3 Immutability, deprecation, retirement
Immutable versions. Once payments.lookup@1.0.0 is published, its schema never changes. If it
could, a pin would mean nothing and a reproducibility claim ("this run called version 1.0.0")
would be false. The lab refuses a republish.
Three lifecycle states, and the difference between the last two is operational:
| State | In latest() | In resolve() | In discover() | Meaning |
|---|---|---|---|---|
active | yes | yes | yes | use it |
deprecated | no | yes | only if asked | still works; migrate |
retired | no | no | no | gone |
A deprecated version remaining resolvable by an explicit pin is what makes a migration window real: existing pinned callers keep working while new callers get the new version by default.
A production deprecation has four parts, and only the first is technical: mark it, notify the known callers (which requires knowing who they are — see the registry extension), set a retirement date, and enforce it. Platforms that skip step two never actually retire anything.
5.4 The change notification
Clients cache tools/list — they must, or every agent turn pays a round-trip per server.
notifications/tools/list_changed is how the cache is invalidated. Without it a client will
happily call a tool that was retired an hour ago, and a deprecation window is theatre.
The server declares tools: {listChanged: true} at initialize so the client knows the
notification will come. The lab wires this end to end and tests that the client's list_calls
counter increments only when it should.
6. The estate's metadata
The ToolSpec fields that are not in MCP are the ones that make it a bank's estate.
6.1 Side-effect class
| Class | Retryable | Approval | Example |
|---|---|---|---|
read | yes | no | balance enquiry |
write_idempotent | yes | maybe | upsert a case note by key |
write_non_idempotent | only with an idempotency key | usually | initiate a payment |
irreversible | no | always | release past settlement finality |
The lab derives RETRYABLE from the class in a single mapping. That single mapping is the control:
retry policy stops being a decision made independently by thirty agent authors, and becomes a
property of the tool. It is also the field
Phase 10 keys its entire behaviour off.
Make it a required field with no default. A default of read is how a payment tool ends up
retryable.
6.2 Scopes, classification, tenants, owner
required_scopes— what the caller's credential must carry. Checked at discovery and again at the action gateway, because two independent gates is the Phase 00 rule.data_classification—public<internal<confidential<restricted. A principal has a clearance; a tool above it is invisible. This is how an information barrier becomes a filter rather than a policy document.tenants— empty means all; otherwise an allow-list. This is what stops a Retail agent from seeing a Wholesale tool at all.owner— the team accountable. Every tool has one, or it will be nobody's during an incident.
7. Authorization-aware discovery
The single most important design decision in this phase:
tools/listis answered relative to a principal, and the filtering happens before the list is built.
Three reasons, in increasing order of how convincing they are in a review:
- Cost. Descriptions are tokens on every turn. A filtered list of six tools instead of sixty is a 90% reduction in tool-schema context.
- Accuracy. Selection error rises with the number of choices. Fewer, relevant tools raise
per-step
p, which raises \( p^n \) superlinearly. - Security. A model that can see a tool will eventually try to call it — especially under
prompt injection, where "call
payments.release" is exactly the instruction an attacker plants. If the tool was never listed, the injected instruction has nothing to name.
And the corollary that makes it airtight: an undiscoverable tool must be indistinguishable from
a nonexistent one. If tools/call on an unentitled tool returns "forbidden" while a nonsense
name returns "unknown tool," the estate is probeable: an adversary enumerates your tool names by
diffing error messages. The lab returns -32601 with the same message shape for both.
This is the same principle as not revealing whether a username exists on a login form, applied to a tool catalogue.
8. Protocol errors versus tool errors
MCP makes a distinction that looks like a curiosity and is actually load-bearing:
| Protocol error | Tool error | |
|---|---|---|
| Shape | JSON-RPC error object | JSON-RPC result with isError: true |
| Examples | unknown method, unknown tool, schema violation, uninitialized session | downstream 503, "account not found", business rule rejection |
| Who sees it | the client (your kernel) | the model |
| What happens | the call never reached the tool | the tool ran and reported a failure |
The reason: the model can only react to what it is shown. A downstream timeout is information the agent should reason about ("core banking is unavailable; tell the user and offer to retry later"). A schema violation is not information for the model in the same way — it is a contract breach that the kernel should handle with a repair loop before spending another full turn.
Getting it backwards produces two distinct pathologies. Return everything as a protocol error and
the agent dies on a transient downstream blip. Return everything as a tool result and the model
sees -32601 unknown method, which it will cheerfully "reason" about and hallucinate around.
9. Lab walkthrough
Work Lab 01 in this order.
- JSON-RPC helpers (§2).
make_request,make_notification,validate_envelope. Tiny; get theidrules exactly right, including rejecting a floatid. validate_schemaand_validate(§4.1–4.2). Write_type_namefirst. Remember:boolis not aninteger; a type failure returns rather than continuing; sort at the end.Version,satisfies(§5.1). Strict parsing;^and~are three lines each.classify_schema_change(§5.2). Check every MAJOR condition first, then MINOR, then PATCH. The six asymmetry tests are the ones to run.RETRYABLE,classification_rank,ToolSpec.to_mcp(§6).to_mcpmust emit exactly five keys — the test asserts the set.ToolRegistry.publishand lifecycle (§5.3). The publish guard is the meat: refuse a republish, refuse a non-newer version, and demand the bump the change classification requires.ToolRegistry.discover(§7). Loop names in sorted order; for each, walk versions newest first and take the first visible one.MCPServer.handleand_dispatch(§2, §3.2). Get the notification path right before anything else: notifications returnNone, always._initialize(§3.2). Lenient negotiation — never fail on an unknown version._tools_call(§8). The order matters: params validation → discoverability → schema → handler. The handler must not be reached on a schema failure, and the test checkscall_log.- Resources, prompts,
notify_tools_changed(§3.3, §5.4). MCPClient(§3.1, §5.4). The cache and its invalidation.repair_arguments(§4.3). Coerce, drop, fill-from-defaults, and nothing else.
Then python solution.py and read the eight sections against §§2–8.
10. Success criteria
Without the guide open:
- Draw host/client/server and say why the client-per-server rule exists.
- Recite the three JSON-RPC message shapes and the
idrule for each. - Explain −32601 vs −32602 and why an unentitled tool uses the former.
- List five things MCP does not provide and where each belongs.
- Explain tools vs resources vs prompts by who controls them.
- State why resource content is untrusted even though resources are application-controlled.
- Give the who-breaks rule and classify six changes correctly.
- Explain why a deprecated version must stay resolvable by pin.
- Give three reasons discovery is authorization-aware, and the probing corollary.
- Explain protocol error vs tool error and the pathology of each mistake.
- Name the four side-effect classes and their retry policy.
11. Common mistakes
Answering a notification. Breaks clients; hangs servers.
Treating tools/list as a static catalogue. It is a query with a principal.
Filtering in the client. The model already saw the list. The filter must be server-side.
Different errors for "not allowed" and "not found." Your estate is now enumerable.
Platform metadata in the description. Wasted context, and a map of your controls for an attacker.
Publishing a breaking change as a minor bump. The single most common way to break an estate.
Mutating a published version. Every pin and every reproducibility claim becomes false.
No listChanged. Clients call retired tools; the deprecation window is theatre.
isinstance(v, int) for {"type": "integer"}. True is now a valid amount.
Repairing semantics. A fabricated account number is worse than a validation error.
Defaulting side_effect to read. A payment tool becomes retryable by omission.
Failing the handshake on an unknown protocol version. A skew becomes an outage.
12. Interview Q&A
Q: What does MCP give you, and what does it not?
A: "It gives one wire protocol between an agent host and a tool provider — JSON-RPC 2.0, a capability-negotiating handshake, and three primitives split by who controls them: tools are model-controlled, resources are application-controlled, prompts are user-controlled. That collapses M×N integrations to M+N and, more importantly for a bank, gives you one place that knows what agents can do. What it does not give you is an authorization model for which agent may see which tool, tenancy, versioning, tool identity, rate limiting, idempotency, or audit. The HTTP transport does define OAuth 2.1 resource-server behaviour, but that authenticates the caller to a server — it isn't a model for filtering a catalogue. So 'we use MCP' is a transport decision, not a tool strategy, and the estate around it is the actual engineering."
Q: How do you handle tool versioning across twelve agent teams?
A: "Immutable versions in a registry, semver, and callers pin a range — ^1.2.0 by default.
The registry classifies every schema change and enforces the bump: adding a required property,
removing a property, changing a type, narrowing an enum, or tightening a bound is major; adding an
optional property, removing a required one, or widening an enum is minor. The rule is just 'does
the new contract accept everything the old one accepted' — callers break when it gets stricter.
Then deprecation is a state, not a delete: a deprecated version stays resolvable by an explicit
pin so pinned callers keep working, while latest() returns the new one. And it's backed by
notifications/tools/list_changed, because clients cache the tool list and without the
notification they'll call something that was retired an hour ago. The part most people skip is
knowing who calls each version — without that, deprecation is a mark in a database and you can
never actually retire anything."
Q: Should every agent see every tool?
A: "No, and for three reasons that get progressively more convincing. Cost: tool descriptions are tokens on every turn, so sixty tools is maybe twelve thousand tokens before the user speaks. Accuracy: selection error rises with choice, and task success is p^n, so trimming the list raises success superlinearly. Security: a model that can see a tool will eventually call it, and under prompt injection 'call payments.release' is exactly the planted instruction — if it was never listed, there's nothing to name. So discovery is answered relative to a principal, filtered by scope, tenant and data classification before the list is built. And the corollary: calling an unentitled tool has to return the same error as calling a nonexistent one, or an adversary enumerates your estate by diffing error messages."
Q: A tool call fails. Walk me through what the agent sees.
A: "Depends which kind of failure, and the distinction is deliberate. If the arguments violate
the schema, that's a protocol error — JSON-RPC −32602 with the validation errors in data. The
tool never runs, and my kernel handles it with a deterministic repair pass first: coerce a numeric
string, drop a disallowed extra property, fill from a platform default. What it will not do is
invent a missing account number — repair syntax, never semantics. If repair leaves errors, the
model gets one turn with all of them at once, sorted, so it fixes everything in one round-trip
instead of three. If instead the tool ran and failed — core banking timed out, account not found
— that's a successful JSON-RPC response with isError: true, and the model sees it as an
observation so it can reason about it. Getting that backwards is bad both ways: everything as a
protocol error and the agent dies on a transient blip; everything as a tool result and the model
starts hallucinating around -32601 unknown method."
Q: What worries you about an MCP server you did not write?
A: "Four things. First, identity — nothing in the protocol tells me this server is really the
payments team's; I want a workload identity and a registry that records which server may serve
which tool name, or a rogue server claims payments.release. Second, the description text: it
goes into my model's context on every turn, so a hostile or careless server author has a prompt
injection channel by construction. Third, sampling and elicitation — a server can ask my model for
completions, spending my tokens with a prompt it controls, and can present arbitrary text to my
user inside my trusted UI. Both are opt-in via client capabilities and my default for third-party
servers is off. Fourth, resource content: 'application-controlled' means I chose to read it, not
that it's safe — everything it returns is untrusted input and gets the same treatment as a
retrieved document."
13. References
- Model Context Protocol — modelcontextprotocol.io: the specification (read the current dated revision and one prior to see how negotiation earns its keep), the architecture overview, and the authorization section for HTTP transports.
- JSON-RPC 2.0 — jsonrpc.org/specification. Short; read it once, completely.
- JSON Schema — json-schema.org, the Understanding JSON Schema
guide; and the
jsonschemaPython library for what a full implementation involves. - Semantic Versioning 2.0.0 — semver.org.
- Confluent Schema Registry compatibility types — backward / forward / full: the same who-breaks reasoning applied to event schemas, and the vocabulary Phase 12 uses.
- OWASP Top 10 for LLM Applications — genai.owasp.org: Excessive Agency and Supply Chain are the two entries this phase's controls address.
- Newman, Building Microservices, 2nd ed. — contract evolution and consumer-driven contracts, which is the same problem with different words.