One-line hook

Our support team’s most-used tool was a senior engineer with database access and a willingness to run UPDATE statements during a customer call. We replaced that workflow with a Model Context Protocol server, Claude Desktop, and a permissions model strict enough that we’d let an intern use it.

Why this article exists

MCP is new. Most of the public discussion is either (a) “here’s a toy filesystem server” or (b) marketing material. Almost nobody writes about what it’s like to operate an MCP server that touches a real production database in front of a real human support team. This is that article.

The setup (vague-client framing)

The problem with the obvious solutions

  1. “Just build an admin UI for it.” We had one. It handled the 60% of cases that were predictable. The other 40% — the messy ones, the “this contract was signed by the wrong entity and we need to re-parent three downstream records” cases — needed ad-hoc reasoning that no form-based UI captures.
  2. “Let support run SQL.” Auditability nightmare. Also: support reps shouldn’t have to know your foreign key topology.
  3. “Build a chatbot against your API.” We tried. The LLM-as-API-caller pattern breaks the moment the LLM needs to reason about what to do next based on what it just saw. Tool-calling helps but isn’t enough for multi-step transactional edits.
  4. “Give the LLM database access directly.” Catastrophic failure mode. One hallucinated WHERE clause and you’ve nuked a tenant.

Why MCP was the right shape for this problem

What we built (the architecture)

The Python snippets we’ll show

  1. The MCP server skeleton: mcp Python SDK, registering tools with typed input schemas
  2. A representative mutating tool with: dry-run, pydantic input validation, audit-log emission, structured diff output
  3. The permissions decorator that wraps every tool call against the current support rep’s role
  4. The audit log emitter — every call (read or write, dry-run or live, success or error) lands in an append-only store with the rep’s identity, the prompt context, and the diff
# Sketch — full version in the article
@tool(
    name="correct_signatory_on_contract",
    requires_permission="contracts.edit_signatory",
    description="Reassign the signatory on a contract. Always dry-run first."
)
def correct_signatory(
    contract_id: str,
    new_signatory_id: str,
    reason: str,
    dry_run: bool = True,
) -> SignatoryChangeResult:
    ...

The hard parts nobody warns you about

A war story (anonymized)

A rep asked the assistant to “fix the line items on contract X — the totals look wrong.” The assistant reasoned its way to a sequence of corrections, dry-ran each, produced a diff that summed to a delta of -$48,000 on the customer’s account. The rep paused, escalated to an engineer, and the engineer caught that the source data was right and the customer’s complaint was based on a misread invoice. The model wasn’t wrong to propose the change — the rep wasn’t wrong to approve dry-running it — and the dry-run + escalation path caught it before any data moved. That whole loop took 90 seconds. In the old workflow, it would have been a 30-minute engineer interrupt.

What we’d do differently

Results

The takeaway

MCP isn’t magic. It’s a sensible protocol for letting a model use tools that you control, on your terms. The hard work isn’t the protocol; it’s the tool design, the dry-run discipline, and the audit trail. Do those three things well and you can give a language model production database access without losing sleep. Do them badly and you’ve built a very expensive way to corrupt your customer data.