Skip to main content

MCP Boundary: Workers Call Tools, Not the Database

You have built a lot of discipline. Migrations are reviewed (Lesson 1). The journal records every run (Lesson 2). Idempotency keys stop duplicate writes (Lesson 3). Claims stop duplicate work (Lesson 4). The outbox stops lost side effects (Lesson 5). Events make the history append-only (Lesson 6). RLS makes the tenant wall live in the database (Lesson 7).

So why not just give the Reconciler a DATABASE_URL and let it write any SQL it wants? You have the discipline. You can read the SQL the agent produces. You can refuse a bad migration.

Because the next Worker that joins the workforce will not. And the model the Reconciler calls today might write a slightly different SQL tomorrow. And the on-call engineer who patches the Reconciler at 2am will not remember every discipline you wrote down. And a single hallucinated DELETE FROM expenses from a model upgrade is enough to put a whole department on a press release.

The point of the SoR is that the database does not depend on Worker discipline. It depends on shape. MCP is how that shape is enforced.

A Worker should not have a DATABASE_URL. It should have a short list of named tools. Each tool wraps a parameterised query, applies the right role, sets the tenant context, and records the call. The Worker calls the tool. The tool talks to the database. The Worker never sees raw SQL.

This lesson teaches the smallest set of tools the Reconciler needs and the boundary that keeps it from doing anything else.

Key Terms for This Lesson
  • MCP: Model Context Protocol. A standard way for an agent to call named, parameterised tools instead of running arbitrary code or SQL.
  • MCP server: The process that exposes the tools. It owns the database connection. The Worker connects to the MCP server, not to the database.
  • MCP tool: A named operation with a fixed input shape and a fixed output shape. Behind it is a parameterised query plus policy and audit.
  • Parameterised query: A SQL statement whose values are bound at execution time, not concatenated into a string. The shape of the statement is fixed.
  • Tool surface: The full list of tools a Worker can call. Everything outside the list is impossible by construction.
  • Principle of least surface area: Expose the smallest tool surface that lets the Worker do its job. Every tool you add is a tool you must defend.

Two Boundaries Compared

The two ways to wire a Worker to a database are radically different in what they allow.

BoundaryWorker hasSurface areaFailure mode
Raw DATABASE_URLAll of SQLInfiniteAnything from a typo to a DROP TABLE expenses to a missing WHERE clause
Bounded MCP toolsA short list of named toolsSmall, named, parameterisedTool returns an error; database state is safe

The raw DATABASE_URL boundary trusts the Worker to never write a bad SQL statement. The bounded MCP boundary makes the database refuse to run statements the Worker did not pre-approve. The first is policy in the head. The second is policy in the code.

You can have every discipline in this chapter and still be one bad model output away from a disaster. The MCP boundary removes that whole class of disaster.

The Reconciler's Minimum Tool Surface

Six tools cover the Reconciler's whole job. Three categories.

Read (one call returns rows; no writes):

  • get_run_status(run_id): returns one row from the runs table.
  • list_my_runs(): returns the recent runs this Worker owns.
  • peek_pending_transactions(limit): returns pending bank rows without claiming them.

Write (one call mutates rows; idempotency and claims are baked in):

  • record_tool_call(turn_id, tool_name, input, output): appends a row to tool_calls (callback to Lesson 2's journal).
  • claim_next_outbox_message(): runs the FOR UPDATE SKIP LOCKED claim from Lesson 5 and returns the next pending outbox row.
  • mark_outbox_delivered(message_id): flips the row to dispatched and stamps dispatched_at.

Append-only (one call adds an immutable event):

  • append_event(run_id, event_type, payload): inserts into agent_events (callback to Lesson 6).

And just as important, the tools that are NOT on this list.

  • No execute_sql(query).
  • No run_migration(path).
  • No truncate_table(name).
  • No delete_row(table, id).

If a tool is not on the surface, the Worker cannot reach the behaviour. There is no "the model just wrote a SQL string". There is no SQL string in scope.

What an MCP Tool Wraps Under the Hood

You do not have to write the tool implementation. You have to recognise what one looks like.

A tool is three things stacked together.

  1. A parameterised query: the SQL is fixed; only the values change.
  2. A policy check: the MCP server uses the right role, sets the tenant context (callback to Lesson 7), and verifies the inputs.
  3. An audit log entry: the MCP server records that the tool was called, by whom, with what inputs, with what result.

The Worker calling claim_next_outbox_message() sees a JSON response. Inside the MCP server, behind that call, this happens:

  1. The server starts a transaction as worker_write.
  2. The server sets app.current_user_id to the Worker's current tenant.
  3. The server runs the parameterised SELECT ... FOR UPDATE SKIP LOCKED against outbox_messages.
  4. The server inserts a row into agent_events recording the claim.
  5. The server commits.
  6. The server returns the row data to the Worker as JSON.

Six prior lessons of discipline collapse into one tool call. The Worker did not write any SQL. The Worker did not pick a role. The Worker did not set a tenant context. The Worker did not remember to emit an event. The tool did all of it.

That is the whole point.

Dev MCP vs Prod MCP

In development, agents sometimes get a wide tool surface so they can prototype. The thing you cannot ship is a Worker that talks to production with a development-grade MCP.

AspectDev MCPProd MCP
DB accessFull DATABASE_URL behind one execute_sql toolParameterised, role-scoped tools per operation
LoggingOptionalMandatory audit per call
RLSOften disabledEnforced
IdempotencyManual (the agent passes a key sometimes)Wrapped by the tool (the tool builds the key)
Tenant contextTest fixtures, hardcodedSET LOCAL on every call, derived from the request
Allowed verbsAnything the role can doThe minimum set per tool

The Reconciler in production should not see a single execute_sql. The Reconciler in development might, but only against a Neon branch with synthetic data and a one-Worker scope.

The rule: never wire the production database to a dev MCP. Never wire a Worker to a tool surface wider than its job. A tool surface that grew "for convenience" is a future incident.

What a Tool Definition Looks Like

You read these. You do not write them. Recognition only.

- name: append_event
description: Append an immutable event row to agent_events.
params:
run_id: { type: uuid, required: true }
event_type: { type: string, required: true }
payload: { type: object, required: true }
returns:
event_id: bigint
occurred_at: timestamptz
side_effects:
- inserts one row into agent_events
- inserts one row into tool_audit_log
role: worker_write
raises:
- InvalidRunId
- PolicyViolation

Four things to read.

  1. The input shape is fixed. Three named parameters, two of them required, one a typed object.
  2. The return shape is fixed. The Worker gets back exactly two fields.
  3. The side effects are declared. Two writes happen for every successful call. Reading this is how you know an append_event call leaves both an event row and an audit entry.
  4. The role is named. The tool runs as worker_write, not as the database owner.

A tool whose definition lists "side_effects: any" or "params: any" is not a tool. It is execute_sql in a costume.

PRIMM-AI+ Practice: Design the Reconciler's Tool Surface

Predict [AI-FREE]

Before reading further or asking the agent, list the smallest set of tools the Reconciler needs. Use the three categories: Read, Write, Append-Only. For each tool, write one sentence describing what raw SQL it wraps.

Then write down:

  • The shortest tool surface you could design that still lets the Reconciler do its full job.
  • The first tool you would consider adding next, and the specific Worker behaviour that would require it.
  • The first tool that looks tempting but you should refuse to add. Why.
  • Your confidence score from 1 to 5.

You should be able to answer all three before reading the agent's proposal.

Run

Ask Claude Code to propose the Reconciler's tool surface as a short YAML or JSON list. Constrain the agent: no execute_sql, no run_migration, no truncate_table. After it produces the list, ask it to demonstrate the Reconciler calling claim_next_outbox_message in a session that has no DATABASE_URL in scope.

What you should see: a tool surface of roughly six tools, each with a fixed input shape, a fixed return shape, and a declared role. The demonstration runs through the MCP server. The Worker never holds a database connection.

Investigate

Write your own one-paragraph explanation:

  1. The six tools cover read, write, and append-only without exposing arbitrary SQL.
  2. Removing peek_pending_transactions would force the Reconciler to use claim_next_outbox_message instead, which marks the row claimed instead of just looking at it. Less peeking, more committing.
  3. The MCP server applies the role and the tenant context on every call. The Worker does not pick either.
  4. Every write tool inserts both the business row and the audit row. The audit trail is automatic.

Then ask the agent:

  1. "Which of these six tools could a malicious or buggy Worker still use to leak data, and how would the MCP server stop it?"
  2. "If I removed peek_pending_transactions entirely, what Worker behaviour would change? Walk through the Reconciler's run."
  3. "Name three tools that look useful but should NOT be added to the Reconciler's surface, and the specific failure each one would let in."

The third question is where the principle of least surface area earns its name. update_run_status(run_id, new_status) looks helpful for ops cleanups. It would also let a Worker silently change a failed run to completed. That is not a tool the Reconciler needs.

Modify

Add one new tool to the surface for ops use only: force_complete_run(run_id).

Predict:

  • Which role should be allowed to call this tool.
  • What audit it must produce when called.
  • Why a Worker should never be able to call it, even by accident.
  • What the on-call engineer's process should look like before invoking it.

What you should see in the agent's response: force_complete_run runs as worker_admin, not worker_write. Every call writes an agent_events row with event_type = 'run_force_completed' and a payload naming the operator and the reason. The Reconciler's tool list does not include it. The MCP server refuses the call if the requesting Worker is not authenticated as an operator.

Adding an ops tool to the surface is fine. Adding it without the audit and the role separation is how an ops tool becomes a backdoor.

Make [Mastery Gate]

The brief in business English:

"Design the minimal MCP tool surface for the Reconciler so that: (1) the Worker cannot run arbitrary SQL, (2) the Worker cannot read another user's transactions, (3) every write the Worker performs produces an event in agent_events, and (4) every claim the Worker performs uses FOR UPDATE SKIP LOCKED. Anything outside the tool list must be impossible."

Hand this brief to the agent. Ask for:

  1. The full tool list (name, params, returns, role).
  2. A one-line note for each tool naming the lesson it depends on (Lesson 3 for idempotency, Lesson 4 for the claim, Lesson 5 for the outbox, Lesson 6 for the event, Lesson 7 for the tenant context).
  3. A short paragraph naming what was excluded from the surface and why.

You read each piece. The gate passes when you can:

  1. Point at the tool list and name what each tool refuses to let the Worker do.
  2. Read the exclusion paragraph and confirm execute_sql, run_migration, and any unscoped delete are absent.
  3. Explain in business terms why removing one tool would still let the Worker do its job, and how that supports the principle of least surface area.

If the agent ships execute_sql "for flexibility", you reject. If the agent ships a write tool that does not emit an event, you reject. If the agent ships a tool with params: any, you reject.

Try With AI

Prompt 1: Tool Surface Design

Help me design the smallest MCP tool surface for the Expense Reconciler
Worker. The Worker must be able to read pending bank transactions,
claim and dispatch outbox messages, record its tool calls, and append
events. It must NOT be able to run arbitrary SQL, drop tables, or
read another user's data.

List the tools you would expose. For each one give me:
- name
- params (named and typed)
- returns
- role
- one sentence on what raw SQL it wraps and what it prevents

What you're learning: Designing surfaces is a discipline of subtraction. The right answer is short. Every tool you add is a tool you must defend. The exercise is to start with the job and stop adding the moment the job is covered.

Prompt 2: Dev vs Prod MCP Comparison

I have a dev MCP that exposes one tool called execute_sql which takes
a SQL string. I want to wire a Worker against production. Walk me
through every specific way this is unsafe. For each one, name the
prior lesson (idempotency, claims, outbox, events, RLS) whose
discipline is bypassed by execute_sql.

Then propose the smallest set of prod-grade tools that would replace
execute_sql for the Reconciler's actual job.

What you're learning: The dev MCP is convenient because it does nothing. The prod MCP is small because it does everything for you. The exercise is mapping each prior discipline to a tool that wraps it, so the Worker cannot accidentally skip any of them.

Prompt 3: Principle of Least Surface Area Drill

I will paste a proposed tool surface of nine tools for the Reconciler.
For each tool, tell me whether it belongs on the surface or should be
removed. If you would remove it, name the failure mode it would let in
or the smaller tool that already covers the same job.

1. get_run_status(run_id)
2. list_my_runs()
3. peek_pending_transactions(limit)
4. execute_sql(query)
5. claim_next_outbox_message()
6. mark_outbox_delivered(message_id)
7. append_event(run_id, event_type, payload)
8. record_tool_call(turn_id, tool_name, input, output)
9. update_expense_amount(expense_id, new_amount)

What you're learning: Reviewing a surface is the most common MCP boundary work in real teams. Tool 4 is the deal-breaker; it reintroduces every failure mode the chapter spent eight lessons closing. Tool 9 is the subtle one; it lets a Worker quietly rewrite a committed business fact, which is exactly what idempotency and events were supposed to prevent.

Checkpoint

  • I can explain why even a well-disciplined Worker should not hold a raw DATABASE_URL.
  • I can read a list of MCP tools and categorise each one as Read, Write, or Append-Only.
  • I can name at least three tools that should NOT be on the Reconciler's surface and the failure each one would let in.
  • I can compare a dev MCP and a prod MCP and explain why the dev boundary is unsafe to ship.
  • I can read an MCP tool definition (params, returns, role, side effects) and confirm whether it preserves the prior lessons' disciplines.
  • I rejected at least one agent-proposed tool surface during practice for including execute_sql or a write tool that did not emit an event.

Flashcards Study Aid