SSpaarxLab Academy
Tools, MCP and Integration ContractsTM-01
All lessons ×
Technical Foundations · intermediate · 40 MIN · INTERACTIVE

Tools and MCP: Design the Integration Contract

Tuesday, 3:02 p.m. Ticket #5590 asks for one thing: downgrade one account. Scribe writes one line of SQL, matches two companies instead of one, and silently downgrades a customer who never asked for it.

Turn a tool from a loose function call into a narrow, checkable contract — schema, scope, idempotency, and typed errors — and prove it holds under three deliberate attacks.

BEFORE YOU BEGIN

What you will be able to do

  1. Turn a function into a narrow tool contract.
  2. Enforce schema, scope, permission, idempotency, and typed errors.
  3. Prove a consequential tool resists duplicate and over-broad requests.
Prerequisites
Agent Engineering: From Prompt to Reliable Work Loop
Assumed knowledge
Can read a function signature and JSON schema.
Evidence you will produce
A bounded account-plan tool with deterministic contract tests
Workload
40 min guided lesson + 45–90 min independent build
0 independent · 0 completed with support · 5 total
YOUR PLACE IS SAVED ON THIS DEVICEStart anywhere, then come back to the same beat.
01 WATCH IT FAIL

The incident

TL;DROne free-text tool, one wildcard match, two customers changed instead of one — and nothing in the run noticed.

Meet Scribe, a billing-ops agent with exactly one tool: query_database, which takes any string and runs it. Ticket #5590 asks for a single, specific downgrade — account acct_7731, Vantage Freight, Pro to Starter.

Watch what Scribe writes instead of what the ticket says. Your job is the same as a reviewer's: spot the line where 'one account' became 'every account that matches a pattern.'

SCRIBE · RUN #001 · ONE TOOL, NO SCHEMA
  1. Press play. A few seconds of real work, slowed down enough to catch the lie.
FORMATIVE CHECK

Scribe's query ran without an error and returned exactly what SQL says it should. So where did this run actually go wrong?

Step 1: choose an answer above. 0 / 20 characters. This reflection stays on your device and is not automatically scored.Stuck? Your reasoning comes first, then you can reveal the answer and continue with support.
02 UNDERSTAND WHY

The diagnosis: the tool wasn't a contract

TL;DRA tool is a promise about what can and can't happen. query_database made no promises at all.

Scribe didn't misunderstand the ticket. It picked the only tool it had, and that tool let it express a query far broader than the task needed. A permissive input schema isn't a convenience — it's an invitation to match more than you meant.

A tool contract is the fix: a name and purpose narrow enough to review at a glance, an input schema that rejects ambiguity instead of accepting it, an authorization scope enforced in the handler, an idempotency rule for repeats, and typed errors that tell the caller whether to fix, retry, ask, or stop.

Here's the contract that would have stopped ticket #5590 cold:

TOOL CONTRACT · ACCOUNT PLAN UPDATE
Schema
account_id (exact match, required), target_plan (enum: starter|pro|enterprise). No free text, no LIKE, no SQL.
Authorization
account_id must resolve inside the requesting tenant. A match outside scope returns denied, not a result.
Idempotency
request_key required. Same key twice returns the same result — never a second update.
Errors
Ambiguous or missing input returns a typed error. The handler never guesses which row you meant.
Go deeper

Why MCP matters here isn't magic — it's standardization. The value of a shared protocol is that a tool's schema, error shape, and result structure mean the same thing to every host and every model that connects to it, so a reviewer can audit the contract once instead of re-deriving it per integration. The discipline (narrow schema, explicit authority, typed errors) is what actually prevents incidents; the protocol just makes that discipline portable.

Watch for a specific smell: a tool that forwards the caller's own login token or a shared admin credential straight to the backend, instead of the handler minting its own scoped grant per call. That passthrough pattern looks convenient — one less thing to configure — and it is exactly how a tool ends up able to do more than its schema advertises. The schema only means something if the credential behind it can't exceed it.

FORMATIVE CHECK

Your team is wiring an agent to a tool that cancels subscriptions. Which line belongs in its contract?

Step 1: choose an answer above. 0 / 20 characters. This reflection stays on your device and is not automatically scored.Stuck? Your reasoning comes first, then you can reveal the answer and continue with support.
03 THE MECHANISM

The mechanism: five parts of a tool contract

TL;DRSchema narrows the ask. Scope narrows the authority. Idempotency narrows the repeat. Errors narrow the guess. Receipt narrows the doubt.

Every call through a well-built tool passes through the same five checks, in the same order, whether it's MCP, a REST wrapper, or a function call. Validate rejects malformed or overly broad input before anything else runs. Authorize confirms the caller may act on this specific record, not just on records like it.

Execute performs only the one operation the schema describes — never a broader query that happens to satisfy it. Verify checks the actual effect against what was expected: one row, not two. Record writes a receipt naming the tool, the input, the outcome, and anything denied along the way.

Most tool incidents live in the gap between two of these: a schema that's technically valid but too broad (validate without real scope), or an execute step with no verify behind it (Scribe's exact failure). Name the five parts and you can point at which one was missing.

01VALIDATEReject ambiguous or malformed input
02AUTHORIZEConfirm scope for this exact record
03EXECUTEPerform only the declared operation
04VERIFYCheck the real effect matches expectation
05RECORDReceipt: tool, input, outcome, denials
FORMATIVE CHECK

In the incident, Scribe's UPDATE ran against two rows instead of one. Which of the five parts, if it had existed, would have caught that before send_confirmation fired?

Step 1: choose an answer above. 0 / 20 characters. This reflection stays on your device and is not automatically scored.Stuck? Your reasoning comes first, then you can reveal the answer and continue with support.
04 YOUR TURN

Scribe, under contract

TL;DRSame ticket, same task — now behind a schema, a scope check, and an idempotency key. Then try to break it three ways.

This is ticket #5590 again, but query_database is gone. In its place: a read-only lookup, a narrow typed update, and the old free-text tool — still present, still denied. Run it clean first, then switch to Break it and try the three moves a careless build would let through.

GOAL Downgrade account acct_7731 to starter under contract, with a receipt a reviewer can trust
lookup_account update_account_plan run_sql
SCRIBE · RUN #002 · UNDER CONTRACT
  1. Press play. A few seconds of real work, slowed down enough to catch the lie.
FORMATIVE CHECK

In the duplicate-request break, why did the retried call return the original result instead of downgrading the account a second time?

Step 1: choose an answer above. 0 / 20 characters. This reflection stays on your device and is not automatically scored.Stuck? Your reasoning comes first, then you can reveal the answer and continue with support.
05 BUILD IT FOR REAL

Build your own account-plan tool

TL;DRTake this contract to opencode and build the adapter for real. Your artifact is the receipt that proves the contract held.

Everything above ran in the page. Now build it where it counts — your machine, real fixture files, a real handler function. Open a disposable directory, start opencode, and hand it the build brief below. You're not writing the code yourself; you're holding the contract while an agent writes it, then trying to break what it built.

When you have a receipt, run the three injections from Break it against your own adapter: the ambiguous lookup, the duplicate request, and the reach for the forbidden tool. If all three are caught, you've shipped the chapter's artifact. If one gets through, you've found a real gap in a contract you wrote yourself — the best place to find it.

BUILD BRIEF · PASTE INTO OPENCODE

Build a small tool adapter in this empty directory for one capability: change an account's plan. Contract: (1) Fixtures: accounts.json with at least two accounts whose emails share a substring (e.g. vantage@vantagefreight.io and billing@vantagerobotics.com) — write this yourself first. (2) Tools: lookup_account(account_id) is read-only and exact-match only, no name or email search. update_account_plan(account_id, target_plan, request_key) is the only way to change a plan; target_plan must be one of a fixed enum. run_sql(query) must exist in the code but be denied by an allowlist before it ever reaches a handler, and the denial must be logged. (3) Validate must reject any input field not in the schema and must reject a missing or malformed account_id — never guess or pattern-match. (4) update_account_plan must be idempotent: calling it twice with the same request_key must produce exactly one row change and return the same result both times. (5) Every call — allowed, denied, or rejected — appends a structured entry to run-trace.jsonl, and a successful plan change writes receipt.json with account_id, old plan, new plan, request_key, and row count. No network calls. No real customer data. Then show me the receipt from one clean run, one run where you send an ambiguous or extra-field input, and one run where you retry the same request_key twice.

YOUR BUILD SURVIVES THE GATE WHEN
  1. Clean run: receipt.json shows exactly 1 row changed on the intended account_id
  2. Ambiguous/extra-field input: validate rejects it before any lookup or update runs
  3. Duplicate request_key: second call returns the first call's result, 0 additional rows changed
  4. run_sql attempt: denied at the tool layer and recorded in run-trace.jsonl
  5. You can explain what the artifact proves — and what it does not prove — in two sentences

What the receipt proves: on these fixtures, this adapter enforces exact-match input, one-row effect, and idempotent retries — every time you ran it. What it doesn't prove: that a real database enforces the same uniqueness guarantees, that a live MCP host wires authorization the way this mock assumes, or that the credential behind the real handler can't exceed the schema in production. Those are separate verification tasks, not conclusions you get for free from a local pass.

SUBMIT YOUR RECEIPT

Built it? Paste your receipt.json. We verify the evidence structure locally; we do not pretend that valid JSON proves the build works.

06 SHIP IT

What you're leaving with

TL;DRA tool is a contract, not a convenience — narrow schema, enforced scope, idempotent writes, typed errors, and a receipt that shows all four held.

You watched one free-text tool silently mutate the wrong customer's account, named the five checks that would have stopped it, and built a typed adapter that caught an ambiguous match, a duplicate retry, and a reach for the forbidden shortcut. The next chapter puts several tools behind one agent and asks the harder question: when two tools could both plausibly finish the job, who decides which one runs?

KEY TERMS · SAY THESE OUT LOUD IN REVIEWS
Tool contract
Name, purpose, input schema, authorization scope, idempotency rule, and error contract — written before the handler.
Idempotency key
A request identifier that guarantees a repeated call returns the original result instead of acting twice.
Least-privilege scope
A tool can only reach what its schema and handler explicitly grant — never everything the underlying system allows.
Typed error
A stable error category (invalid input, denied, not found, timeout) that tells the caller whether to fix, retry, ask, or stop.
Credential passthrough
Forwarding a caller's own token straight to a backend instead of minting a scoped grant — a smell, not a shortcut.
GO DEEPER · THE SOURCES BEHIND THIS CHAPTER
FORMATIVE CHECK

Your capstone receipt shows update_account_plan enforced schema, scope, and idempotency across three test runs. What can you honestly claim from that?

Step 1: choose an answer above. 0 / 20 characters. This reflection stays on your device and is not automatically scored.Stuck? Your reasoning comes first, then you can reveal the answer and continue with support.
CHAPTER IN PROGRESSEach check unlocks the next beat; hints never masquerade as mastery.