Claude inside real systems
MCP connectors
Two ways to let Claude run a business with exactly the permissions of the person using it, and nothing more.
- Role
- Sole author
- Period
- Oct 2026
- Status
- Both in production
- Code
- Private
- Snup tools, 5 of them writes
- 17
- tests on the Snup connector
- 85
- Maquila tools
- 66
- Maquila tables under RLS
- 25 of 25
- MCP
- OAuth 2.1
- PKCE
- Elixir
- Phoenix
- Next.js
- Supabase
- Postgres RLS
01 Context
Why it exists
Running a business from Claude is useful only if the model cannot do more than the person behind it. At Snup, admin access used to be one switch that opened every admin endpoint, and day-to-day operations such as reviewing applications or investigating a charge often went straight to the database, with no permission check and no audit trail. The risk the design names first is an agent writing something to production that nobody asked for.
Maquila, an operations app for a family business, has the opposite shape: two or three trusted people, open sign-up, and agents from more than one vendor. There, being signed in is not enough, and the same rules have to hold for the web app, the desktop app, the MCP server and direct API calls.
The two connectors answer the same question in different places. Snup checks a grant in the application and makes every write a two-step operation. Maquila moves the boundary into the database, so an agent's query runs as the person.
02 Architecture
How it works
The Snup connector is embedded in the Phoenix app and calls the same domain modules as the admin panel. Snup is its own OAuth 2.1 server: Claude discovers it from a 401, registers dynamically, and the admin consents on a page where they choose what this connection may do. The diagram follows one call.
-
Connect and consent
Claude reads the protected-resource and authorization-server metadata, registers itself, and opens the consent page with PKCE. The admin signs in with their normal session and picks tools by area, whether writes are allowed, whether contact data is visible, and how long the connection lasts.
-
Gate every request
A plug checks the feature switch, the origin and the bearer token, then resolves a
Principal. It also re-checks that the person is still an admin. Personal tokens for Claude Code and OAuth for claude.ai resolve to the samePrincipal, so tools, limits and audit never need to know which one was used. -
Writes take two calls
Write tools default to
dry_run: trueand return a before-and-after preview, the side effects, and a token signed over the person, the credential, the tool and a hash of the arguments, valid for five minutes. Applying it takes the same arguments, that token and a non-empty reason. -
Audit, always
Every call writes one row: reads, denials, rate-limited calls, previews and writes, with the actor, the credential, the client, the arguments and the reason. The result itself is never stored.
-
Maquila: the query runs as the person
Supabase Auth acts as the OAuth server. Each tool builds a database client that carries the user's own token, so row-level security, column privileges and guard triggers apply to the agent exactly as they do in the app. A permission lookup only exists to return a friendly denial.
-
Maquila: the database knows it is an agent
A SQL function reads the OAuth client from the token. That powers a per-person switch that allows edits in the app but only reads through an agent, and disconnecting an agent ends its sessions immediately.
03 Decisions
The calls that shaped it
-
Decision 01
Embed the Snup connector and call the panel's own domain code
- Chose
- An MCP endpoint inside the Phoenix app, calling the same modules as the admin panel.
- Over
- A separate MCP service calling the admin API over HTTP, which needs either token forwarding (the MCP spec forbids it) or a service credential that loses the person's identity.
- Because
- One place for permissions and audit, and the panel's validations and side effects for free.
- Cost
- Logic first had to move out of controllers into domain modules, and the connector shares the app's process and database pool.
-
Decision 02
Be Snup's own OAuth server
- Chose
- PKCE, dynamic registration against a closed list of redirect URIs, one-hour access tokens and refresh tokens that rotate on every use; reusing a rotated one revokes the whole connection. Only hashes are stored.
- Over
- The existing identity provider with an audience per environment, which was tried first: adding the connector from a phone failed with it.
- Because
- The connector had to work from every Claude client, and the provider stood between the admin and the consent the design needed.
- Cost
- Security code to own: PKCE, rotation, reuse detection and revocation, covered by 29 tests.
-
Decision 03
Let each person choose a grant, and defer roles
- Chose
- A per-credential allowlist of tools plus switches for writes and contact data, intersected with still being an admin. Tools that return bank details start switched off.
- Over
- Coarse read and write scopes, and role claims in tokens, where removing a role does not affect tokens already issued.
- Because
- It shipped in days, and the grant is visible and revocable per connection.
- Cost
- No least-privilege roles yet: any admin can grant their own connection any tool.
-
Decision 04
Enforce two-step writes on the server, statelessly
- Chose
- A signed confirmation token bound to the exact call, stored nowhere. The two-step flow lives in the server, not in each tool, so a new tool cannot forget it.
- Over
- Trusting the client to show a confirmation prompt for destructive tools.
- Because
- Clients differ in what they ask the user, and a server rule holds for all of them.
- Cost
- Two round trips per write. The token binds the arguments, not the data, so domain writes are compare-and-set and report drift if the record changed in between.
-
Decision 05
Put Maquila's authorization in the database
- Chose
- Row-level security keyed by area and access level on every table, cost columns readable only through functions, guard triggers, and restrictive per-company policies.
- Over
- Per-tool checks in TypeScript, which could drift between the web app, the desktop app, the MCP server and direct API calls.
- Because
- An agent cannot see more than its human because the query runs as that human.
- Cost
- An extra lookup on every call,
select *breaks on tables with cost columns, and the rules live in SQL. Confirmation before writes is a server instruction there, not an enforced step.
04 What is interesting
Worth a closer look
-
The confirmation token is bound to the exact call
Its claims are the person, the credential, the tool and an order-independent hash of the arguments. Change any argument and it no longer matches; wait five minutes and it expires. Nothing is stored, so there is nothing to clean up.
-
Text written by users arrives marked as untrusted
Every free-text field written by a user, such as a review or a support message, reaches the model wrapped as untrusted and capped in length. Combined with the preview rule, injected text can at worst produce a preview nobody confirms.
-
Row-level security is the agent boundary
In Maquila the database itself enforces what an agent sees, and it can still tell agent traffic from app traffic. Revoking an agent ends its sessions at once, because every call validates the token again.
-
Paper an agent can read
Maquila's printed production sheets carry a plain code on every row so an agent can read a photo of the filled-in sheet and record the work. A database trigger refuses totals above the ordered quantity.
05 Built with agents
How agents fit in
Both connectors exist so agents can do real work: approve applications, chase pending payments, record production from a photo. Both were also built by agents, and all nine commits on the Snup connector and all seven on Maquila's MCP server have a Claude co-author.
Snup's connector was designed in documents first. A planning brief with no code, an ADR, and two amendments that cut scope came before the two implementation briefs, each with a checklist and a test plan. The personal-token version landed in one commit of about five thousand lines with its tests, and OAuth followed the same day.
Maquila's rules file works as the agent's operating manual: row-level security on every table, views that run with the caller's permissions, and a short plan before any change. The MCP server grew from 35 to 66 tools in six days under those rules.