« Phase 02 · Warmup · Track Overview

Lab 01 — MCP Server, Client & the Bank's Tool Estate

The problem

MCP gives you one wire protocol between an agent and its tools. That solves the M×N integration problem and nothing else. It does not tell you who may call a tool, which version of it, what happens when its schema changes, whether a retry is safe, or how a Retail agent is prevented from discovering a Wholesale payment tool.

Those are the platform's problems, and they are where the JD's phrase "tool packaging, versioning, capability advertisement, schema enforcement, and runtime tool discovery across Wholesale, Retail, and Group functions" actually lives. So you build both halves: a faithful MCP server and client, and the estate that governs what they may see.

What you build

#ComponentWhat it does
1make_request, make_notification, validate_envelope, JsonRpcErrorJSON-RPC 2.0 with the standard error codes; a notification has no id and is never answered
2validate_schemaa real JSON Schema subset returning all errors with paths, sorted — so a repair loop fixes everything in one turn
3Version, classify_schema_changesemver with ^/~ constraints, and the rule that decides whether a schema edit is patch, minor or major
4ToolSpec, ToolRegistryan immutable, versioned estate that refuses a breaking change published as a minor bump
5SideEffect, RETRYABLEthe classification that derives retry policy at the platform level, not the agent's
6ToolRegistry.discoverauthorization-aware discovery: scope, tenant, classification and lifecycle filtering, newest visible version per tool
7MCPServerinitialize/capability negotiation, tools/list, tools/call, resources, prompts, notifications/tools/list_changed
8MCPClientone client per connection, tool-list caching, cache invalidation on the change notification
9repair_argumentsdeterministic repair of unambiguous argument errors — and a refusal to invent values

Key concepts

ConceptWhereWhy it matters
Protocol error vs tool error_tools_calla schema violation is JSON-RPC -32602 and never reaches the tool; a tool failure is a successful response with isError: true, so the model can react
Authorization-aware discoverydiscoveran undiscoverable tool is indistinguishable from a nonexistent one — otherwise the estate is probeable
Description is prompt surfaceToolSpec.descriptionthe model reads it; it is not documentation, and it is not where platform metadata goes
Who breaks?classify_schema_changeadding a required field breaks callers; removing one does not. Narrowing an enum breaks; widening does not
Immutable versionspublishrepublishing a version makes every pin meaningless
Side-effect classRETRYABLEretry policy is derived by the platform from a required field, never chosen per call site
listChangednotify_tools_changedthe only thing that makes a deprecation window work against a caching client
Repair, not fabricationrepair_argumentscoerce "42" to 42; never invent a missing account number
Version negotiation is lenient_initializean unsupported version offers a fallback rather than failing — skew must not become an outage

Files

FileRole
lab.pyyour implementation
solution.pyreference; python solution.py runs an eight-part worked session
test_lab.py84 tests
requirements.txtpytest

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py

Success criteria

  • All 84 tests green against your lab.py.
  • validate_schema rejects True for {"type": "integer"} and accepts 5 for {"type": "number"}.
  • A type mismatch produces one error for that path, not a cascade.
  • Errors come back sorted, so two runs produce identical repair prompts.
  • classify_schema_change gets all six asymmetries right (add/remove required, add/remove property, narrow/widen enum).
  • The registry refuses 1.1.0 for a breaking change and demands 2.0.0.
  • A deprecated version is not latest() but is still resolve()-able by an explicit pin.
  • A tool the principal cannot see returns -32601 with the same shape as a tool that does not exist.
  • tools/list output contains exactly name, title, description, inputSchema, _meta — no scopes, no classification.
  • A schema violation leaves call_log empty.
  • repair_arguments is idempotent and never fills a required field without a default.

How this maps to the real stack

This labThe real thingWhat we simplified
MCPServer / MCPClient over a direct callthe official MCP SDKs over stdio or streamable HTTP, with sessions, resumability and SSEno transport, no framing, no auth headers; the message shapes are faithful
Capability negotiationthe real initialize handshake, with dated protocol revisionswe negotiate three capabilities; the spec has more, plus experimental blocks
tools/list filteringdone by your server implementation — the spec has no authorization model at allthis is the point of the lab: the spec leaves it to you, and most implementations skip it
validate_schemajsonschema / pydantic / provider-side structured outputswe cover a subset; no $ref, oneOf, allOf, formats
ToolRegistryan internal service backed by Postgres, or an API-catalogue product; Azure APIM as the north-south enforcement pointno storage, no API, no approval workflow
classify_schema_changecontract-testing tools (Pact), schema registries (Confluent compatibility modes), OpenAPI diff toolsours is a small rule set over one schema dialect
notifications/tools/list_changedthe same notification, over a live transportwe deliver it by calling a method on the client
repair_argumentsa validate-and-retry loop around the model, plus constrained decodingours does the deterministic half only, which is the half worth automating

Honest limits. No transport, no concurrency, no authentication of the server to the client (in production, an MCP server is a workload with its own identity — Phase 08), no sampling or elicitation (the server-initiated directions, both security-relevant), and no rate limiting.

Extensions

  1. A real transport. Wrap the server in stdio framing (Content-Length headers) or an HTTP endpoint, and make the client speak it. Then add request cancellation and see what it does to your call_log.
  2. Server identity. Give each MCP server a workload identity and require the client to verify it; then have the registry record which server serves which tool, so a rogue server cannot claim payments.release.
  3. Sampling and elicitation. Implement the server→client directions. Then write the threat model: a server that can ask the client's model for a completion can exfiltrate context.
  4. Pagination. Add cursor/nextCursor to tools/list and resources/list and make the client's cache correct across pages.
  5. Deprecation enforcement. Track which agents called which tool version, and turn "deprecate" into an automated impact report plus a scheduled retirement.
  6. Schema evolution tests. Given a stored corpus of past calls, assert that a proposed new schema still validates all of them — the contract test that catches a "minor" bump that is not.

Interview / resume bullets

  • "Built the bank's MCP tool estate: an immutable versioned registry that classifies every schema change as patch/minor/major and refuses a breaking change published as a minor bump, with deprecation windows enforced by notifications/tools/list_changed."
  • "Made tool discovery authorization-aware — filtered by scope, tenant and data classification before the list is built — so an unentitled tool is indistinguishable from a nonexistent one and the estate cannot be probed by an agent."
  • "Separated protocol errors from tool errors: a schema violation is rejected at the boundary and never reaches the tool, while a downstream failure is returned to the model as a normal result so it can adapt rather than the run failing."
  • "Made every tool declare a side-effect class, and derived retry policy from it at the platform level — which removed a class of double-execution bugs from thirty agent teams at once."