Overview
Both products already had an API that knows exactly who is asking and what they are allowed to see. When assistants like Claude and ChatGPT started being able to call tools, the tempting shortcut was a new integration with its own idea of permissions. I did not want two.
So each product got a thin Model Context Protocol server instead. Every tool wraps one fixed query and forwards the signed-in user's own credentials, which means the API enforces exactly what it enforces for the web app. The MCP layer only curates: which fields come back, how many rows, and what gets logged.
I built the first one for Payrun, then ported it to Gain.io. The transport, the sign-in flow, the error handling and the test suites are shared; only the tools themselves differ.
How a request travels
An assistant calls a tool
The request arrives at a small serverless endpoint. The same server also runs locally over stdio, which is how I develop and inspect it.
Identity comes with it
Command-line clients bring the user's own credentials. Web assistants sign in through OAuth. Either way, the call belongs to one real person.
Input is checked, then bound
Arguments are validated, list sizes are capped, and the values are bound to the tool's one fixed query. Nothing in the input can change what runs.
The product decides
The query goes to the API with the user's credentials. Roles and organisation boundaries are enforced there, exactly as they would be in the browser.
The answer is trimmed and logged
Sensitive fields are never requested, long results are cut with a note of what was left out, and one audit line records what ran and how it went. Never the values, never the credentials.
Guardrails
One fixed query per tool
No schema introspection and no free-form queries. The server refuses to start if a tool ships anything that writes without being explicitly allowed to.
The credential is the boundary
No tool takes an organisation or user identifier as input. Scope always comes from who is signed in, so no prompt can talk the assistant into someone else's data.
One write, on purpose
A single tool can change something, and only because a person asked for it. It runs as that person and is never retried after a failed sign-in.
Honest labels
Each tool's read-only flag is derived from what it actually does, so an assistant that asks before writing asks at the right moments.
Least data by default
Field selections are chosen for the question being asked. Personal details the question does not need are not requested in the first place.
Fail fast on sign-in
An expired session is refreshed once. If that fails the server stops trying and says so clearly, and permission errors are flagged as permanent so the assistant explains instead of retrying.
What it covers
Gain.io
Read-only across the helpdesk and CRM.
- Conversations
- Contacts
- Deals and pipeline
- Offers and tasks
- Knowledge base
- Team activity
- Satisfaction and performance
Payrun
Read-only across workforce data, plus one action to file a bug report.
- Attendance
- Time and timesheets
- Leave
- Projects and tasks
- Reports
- Holidays and events
- Bug reports
Signing in from a web assistant
Command-line clients can be handed a credential. Web assistants cannot, so the endpoint also speaks OAuth 2.1 and acts as its own authorization server: standard discovery, dynamic client registration, PKCE only, and no client secrets anywhere.
The consent screen is the product's own. You sign in the way you always do and approve access. The token the assistant receives is bound to your session, so signing out of the product disconnects the assistant too, and every call still goes through the same role checks.
- The endpoint keeps no state of its own. Anything the sign-in flow needs to remember is held briefly by the product and expires quickly.
- The simpler credential flow stayed available alongside, so nobody who had already connected a command-line client had to redo anything.
Testing
Each server has a suite that runs without a live API.
- Invariants over the tool registry: every tool ships one fixed query, an honest read-only flag, and no identity override in its inputs.
- An offline check of every query against the API's real schema, so a renamed field breaks the build rather than someone's prompt.
- The full OAuth flow, from registration through consent to authenticated calls and refresh.
- An end-to-end run that starts the real server against a mock API and exercises sign-in, refresh, permission errors and limits.
- Before each release, every tool tried by hand from the assistants people actually use.
Outcome
Both products can be connected to the major assistants and editors with a single URL each, and a customer's existing roles decide what the assistant can see. The permission model stayed singular, which was the goal.
What I'd do differently
- Write the schema check first. Most early failures were drift between a hand-written query and the API, and the check finds those in seconds.
- Decide the write policy before the first tool. Bolting the one allowed write on after a dozen read-only tools meant revisiting every label.
- Treat whatever sits in front of the endpoint as part of the sign-in surface. A rewritten header cost a day that standard discovery would have saved.