GTM Engineering
MCP for RevOps: Giving Claude Governed Access to Your CRM
An MCP server is a door into your CRM for an AI. Build the door with a lock, a log, and a scope before you hand out the key. Here is the permission model.
· 13 min read
The pitch for an MCP server is that you can ask Claude “which enterprise deals slipped last quarter and why” and it queries your CRM and answers, no SOQL, no export, no analyst. That works. What the pitch skips is that you just gave a language model a connection to the system that runs your revenue, and the model will call whatever tool you expose with whatever arguments it decides on. If one of those tools is “update records” and the scope is “all objects,” a bad prompt or a confused chain rewrites production. The demo and the disaster are the same feature.
MCP, the Model Context Protocol, is a standard way to hand an AI a set of tools it can call: query this, read that, update the other. For RevOps that is enormous, because it turns the CRM from a thing you export into a thing you converse with. But a tool the model can call is a door into your data, and you do not hang a door without a lock, a log, and a limit on which rooms it opens. The teams that ship MCP safely design the permission model before they expose the first tool. That model is the whole job, and it is what this piece builds.
Here is the build I run in place of “connect Claude to Salesforce.” Five rungs, bottom to top, each one a layer of governance that has to hold before the one above it lights up. This is the framework. Scroll it, then I will show you the tool schemas, the query guard, and the approval queue that make each rung real.
- L5Dangerous capabilities off the menuomit
Bulk updates, deletes, and exports never become MCP tools. They stay in governed, human-triggered runs. The safest tool is the one you chose not to expose, and deciding what to leave off is as much of the job as scoping what is on.
- L4Writes as proposals to a queuequeue
A write tool inserts to an approval queue and holds no update permission on real records. A human or a rule approves the batch before anything applies. The model proposes, the queue disposes, and the blast radius is a review list.
- L3Audit log on every callevery call
Record tool, arguments, result size, and conversation for every call from day one. Live in read-only for weeks and read the log. It tells you what people ask, which write tools would matter, and whether any argument pattern looks wrong.
- L2Read tools with tight schemasread-only
Every tool is a typed function: ID patterns, enums, limit caps, additionalProperties false. The model can only pass arguments you anticipated. Queries are parameterized and run WITH SECURITY_ENFORCED, never string-built from model output.
- L1Scoped integration user1 user
The server acts as a dedicated integration user, never a human login or a system admin. A read-only profile with field-level security limited to the exact objects the tools touch. This identity, below the tool and below the prompt, is your real permission boundary, and it fails closed.
Locate your rollout on that ladder. Most teams that get burned skipped straight to L4 with an admin login and no log. Every rung below it is a wall a hostile or confused prompt hits before it reaches production. Build them in order and the demo and the disaster stop being the same feature.
The model calls the tools, you own the tools
The mental model that keeps you safe: you are not giving Claude your CRM. You are giving it a set of functions you wrote, each with a fixed shape, and the model can only do what those functions allow. The model does not have a Salesforce login. Your MCP server has the login, and the server decides what the model is allowed to ask for. Every guarantee lives in the server, not in the prompt, because the prompt is the one thing you do not control.
So the design question is never “what should Claude be able to do.” The real question is what tools you expose, what each one can touch, and who the server becomes when it makes the call. Get those three right and a hostile prompt hits a wall. Get them wrong and a helpful prompt does damage.
Start read-only, and stay there longer than you want to
The single highest-leverage decision is to launch with zero write tools. A read-only MCP server delivers most of the value, “answer questions about my CRM,” at a fraction of the risk, because the worst a read tool can do is show data to someone who should not see it, which your field-level security already governs. Writes are where the irreversible damage lives, and you do not need them to prove the thing works. Ship read-only, live in it for weeks, watch the audit log, and only then decide whether any write is worth building.
The rush to hand AI a write is worth resisting on the evidence too. Jason Lemkin’s 2025 SaaStr survey of AI agents in revenue teams found 83 percent of companies got nothing usable and only 3 percent got real revenue, and the pattern behind the 3 percent was a narrow, governed scope rather than a broad autonomous one. A read-only MCP server that answers pipeline questions reliably beats a write-enabled one that corrupts a forecast in month two. Start where the failure mode is “shows the wrong chart,” not “rewrites the quarter.”
The integration user is the second decision. The server acts as a dedicated integration user, never a human’s login and never a system admin. That user gets a read-only profile with field-level security scoped to the objects and fields the tools need, and nothing else. If a tool never reads Social Security numbers, the integration user cannot see them, so no prompt can extract them. Permission is enforced at the identity, below the tool, below the prompt, where the model has no reach.
The artifact: a tool definition with a scope
An MCP tool is a named function with a typed input schema. The scope lives in the schema and in the code behind it. Here is a read tool that answers “show me open opportunities for an account,” written so it physically cannot do anything else.
{
"name": "get_open_opportunities",
"description": "Return open opportunities for one account. Read-only.",
"inputSchema": {
"type": "object",
"properties": {
"account_id": { "type": "string", "pattern": "^001[A-Za-z0-9]{15}$" },
"limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
},
"required": ["account_id"],
"additionalProperties": false
}
}
Three defenses are baked into that schema. The account_id pattern rejects anything that is not a real 18-character account ID, so the model cannot smuggle a SOQL fragment into the argument. The limit cap means no single call can pull the whole object. And additionalProperties: false means the model cannot pass a field you did not anticipate. The tool is a narrow slot, not an open query box.
Behind the schema, the query is parameterized and bounded, never string-built from model output:
// The tool builds the query. The model only supplies bound arguments.
List<Opportunity> getOpenOpps(Id accountId, Integer lim) {
return [
SELECT Id, Name, StageName, Amount, CloseDate
FROM Opportunity
WHERE AccountId = :accountId // bound, never concatenated
AND IsClosed = false
WITH SECURITY_ENFORCED // FLS honored, no bypass
ORDER BY Amount DESC
LIMIT :lim
];
}
WITH SECURITY_ENFORCED is the line that makes the integration user’s field-level security binding at query time: a field the user cannot see is not returned even if the SELECT names it. The model asks; the CRM decides what it is allowed to answer.
When you do add writes, add a queue, not a key
Eventually someone wants a write, “update the stage,” “log the call.” Do not give the write tool direct update permission. Route it through a proposal queue, the same seam I build for CRM agents with write access. The write tool does not update the record. It writes a proposed change to a queue, and a human or a rule approves the batch before anything applies. The model proposes, the queue disposes.
{
"name": "propose_stage_update",
"description": "Propose a stage change. Does NOT write. Queues for approval.",
"inputSchema": {
"type": "object",
"properties": {
"opportunity_id": { "type": "string", "pattern": "^006[A-Za-z0-9]{15}$" },
"new_stage": { "type": "string", "enum": ["Discovery","Qualified","Proposal","Negotiation","Closed Won","Closed Lost"] },
"reason": { "type": "string", "minLength": 20 }
},
"required": ["opportunity_id", "new_stage", "reason"],
"additionalProperties": false
}
}
The enum on new_stage means the model cannot invent a stage. The minLength on reason forces an explanation into the audit trail. And the word “propose” is not decoration: this tool has no update permission at all, so even if the model calls it a thousand times, it fills a review queue, it does not touch a live record. That is the difference between a door with a lock and a door propped open.
The permission matrix
Every tool gets classified before it ships. This is the matrix I fill in for a rollout.
| Tool | Access | Integration user can | Approval needed |
|---|---|---|---|
| get_open_opportunities | Read | SELECT on Opp, FLS-scoped | None |
| search_accounts | Read | SELECT on Account, FLS-scoped | None |
| get_activity_history | Read | SELECT on Task/Event | None |
| propose_stage_update | Write-proposal | INSERT to queue object only | Human approves batch |
| propose_field_update | Write-proposal | INSERT to queue object only | Human approves batch |
| bulk_update | Not exposed | Nothing | Never build this as a tool |
The bottom row is the point. Some capabilities do not get an MCP tool at all. A bulk update belongs in a governed data-loader run a human triggers, not in a function a model can call. Deciding what to leave off the menu is as important as scoping what is on it.
Read-only launch vs write-enabled launch
The two rollouts carry different risk, and the risk should decide the order.
| Read-only launch | Write-enabled launch | |
|---|---|---|
| Worst-case damage | Shows data FLS already governs | Rewrites production records |
| Reversibility | Nothing to reverse | Only if you captured prior values |
| Value delivered | Ask questions, get answers | Same, plus queued actions |
| Integration user profile | Read-only, FLS-scoped | Read plus insert to a queue object only |
| Time to ship safely | Days | Weeks, after read-only proves out |
| Approval gate | None needed | Mandatory before any apply |
What the audit log is for
Every tool call gets logged: which tool, what arguments, what the integration user returned, and which conversation asked. The log earns its keep the day someone asks “why did the model return this account’s data” or “did any tool get called with an argument I did not expect,” and you can answer from a record instead of a shrug. Watch the log for weeks during the read-only phase and it tells you which write tools are even worth building, because it shows you what people ask the CRM to do.
View as table
| Item | Value |
|---|---|
| Pipeline questions | 44% |
| Account lookups | 29% |
| Activity history | 17% |
| Forecast queries | 10% |
Ship the first rung this week
The ladder is the shape of the finished system. You do not build all five rungs at once. You stand up L1 and L2 end to end, prove it on one question, and let the audit log earn the rungs above it. Here is the week-one cut.
- 1
Create the integration user before any code
A dedicated user, read-only profile, field-level security limited to the two or three objects your first tool reads and nothing else. Test that it fails closed: log in as that user and confirm it cannot see a field you did not scope. This is L1, and it is the boundary the prompt can never reach past.
- 2
Ship exactly one read tool with a tight schema
One tool, "get open opportunities for an account," with an ID pattern, a limit cap, and additionalProperties false. The query is parameterized and runs WITH SECURITY_ENFORCED. Resist the second tool until the first proves out.
- 3
Turn on the audit log on call one
Record tool, arguments, result size, and conversation for every call. You want the log running before the first real question, because the log from week one is what tells you which write tools, if any, are worth building later.
- 4
Live in read-only and read the log for a month
Ask questions, watch what the team asks, and watch for any argument pattern you did not expect. A month of clean logs is the evidence that earns L4, a write proposal queue. No log, no write.
- 5
Write down what stays off the menu
Before anyone asks, name the capabilities that will never be MCP tools: bulk update, delete, export. Put them in a governed, human-triggered run instead. Deciding the omissions up front is L5, and it is cheaper to write now than to walk back later.
MCP turns your CRM into something your team can talk to, and that is worth building. But you are handing an AI a door into the system that runs your revenue, and the door needs a lock, a log, and a limit before the key goes out. Start read-only, act as a scoped integration user, log everything, and route every write through a proposal queue. This week, stand up one integration user with a read-only profile and one read tool with a tight schema, and let a month of clean audit logs decide whether you ever build a write. Build the door before you hand out the key.
Keep reading
One email. Every week.
One email a week: a system I built or broke, with the config, the numbers, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.
The newsletter opens soon.
Connect a provider in src/config.ts