Engineering
Engineering

MCP as the CRM's tool surface: one tool set, four kinds of callers

Zac Sheffer · 2026.9.22 · 6 min read

We ship an MCP server alongside our REST API. Same operations, different transport. This post covers why we added it, how the tool surface is designed, and what we got wrong the first time.

Why MCP when we already have REST

The REST API works fine for programs. MCP adds three things for agents:

  1. Tool discovery is programmatic. An agent connects and gets a typed catalog of every tool, its input schema, and its required auth level. No docs to read, no tool descriptions to hand-write. The catalog updates when we deploy. This is a core MCP protocol capability.
  2. Sessions carry auth state. REST authenticates per request. MCP sessions carry an assurance level, and different tools in the same session can require different levels. A voice agent connects as "identified," reads contacts, gets denied on revenue tools. In REST you would model this as separate API keys with different scopes. In MCP it is built in. The authorization section of the spec describes how this works.
  3. Agents see what they can call. Role-gated tools are enumerable. A finance-role session sees revenue tools. A member-role session does not. The agent plans based on what is available, not by trying things and hitting 403s.

The tradeoff: MCP is a second surface to maintain. The two must agree. We accepted that because agent-facing use is where the product is going.

Four callers, one contract

The same tools serve four transports:

  • OAuth clients — IDEs and chat assistants where a human authenticated interactively
  • API keys — first-party automation acting for an org
  • Voice agents — a calling platform hands us a resolved identity; we look it up, we do not establish it
  • In-process — Kai's intake agent imports the same handlers directly, no HTTP

Every path builds the same context: user, tenant, assurance level. The in-process path is the important one. Because Kai's intake agent calls the same handlers external clients do, CRM semantics are single-sourced. No internal API to drift.

Assurance levels

Not all authenticated callers are equally trusted. A voice session is "identified" — the calling platform verified the number, but nobody typed a password. An OAuth session carries higher assurance.

Every tool declares a minimum assurance level. The dispatcher checks it. Pass or fail, no negotiation. This borrows from NIST SP 800-63 (AAL1 through AAL3).

Concrete example: a voice agent can search_contacts and get_deal_status. It cannot call report_deal_revenue. The server decides, not the model.

Role gates layer on top. They check the role in the target org, not in any org the user belongs to. "Finance somewhere" is not "finance here." Denials distinguish not-a-member from insufficient-role because those mean different things when debugging.

Tool design

snake_case names. The protocol is JSON-RPC 2.0. CamelCase tool names caused parsing failures in two agent frameworks we tested. snake_case worked everywhere. We went with it.

Fifteen tools, not fifty. Each tool is one verb on one resource. search_contacts searches contacts. create_referral creates a referral. No manage_contacts with a mode parameter. The MCP tools spec says tools should have clear descriptions and input schemas. A tool that does multiple things based on a parameter is a tool the agent will misuse.

Fifteen is also a context window constraint. An agent that enumerates fifty tools fills its context with descriptions before doing any work. Fifteen fits comfortably in a system prompt.

We have rejected convenience tools like search_and_create_contact. The two-step pattern is what the agent should learn. Collapsing steps into one tool hides logic the agent needs to see.

Response format

MCP responses serve two consumers: a model that pays per token and a UI that wants the full payload.

Every tool returns both: a one-line content summary for the model ("found 3 contacts at Novaform, most recent added in June") and the complete structuredContent for the UI. The model reasons on the summary. The UI renders the records.

Errors stay out of structuredContent. It is success-only. Failures travel as typed errors the model can narrate.

Two other contract decisions: read tools carry the protocol's read-only and idempotent annotations so clients can retry and parallelize them. Create tools return isNew: false on duplicates instead of erroring — "already exists" is information, not failure.

What we got wrong

Tools too coarse. First version had a sync_crm tool that triggered a full sync for a given CRM. An agent that wanted to sync one contact had to trigger the whole thing. We broke it into sync_company, sync_contact, sync_deal. Agent behavior improved immediately because the tool granularity matched the decision granularity.

Auth too permissive. First version checked for a valid session but not assurance level per tool. Every authenticated caller could call every tool. We caught it in testing when a voice-agent session called report_deal_revenue. That should have been denied. The assurance-level gate fixed it. The test is permanent.

Error messages too helpful. Denial messages originally explained which role was required and in which org. Useful for debugging, bad for production — an adversarial agent can enumerate the permission model from denials. Errors now say "not authorized" or "not found" and nothing more. Detail goes to logs.

Schema generation

Output schemas are generated from the same Zod schemas that validate our data. A Zod version conflict between our schemas and the MCP SDK forced us below the SDK's convenience layer, which also cost us automatic response validation.

The replacement: a test suite that asserts schema-payload agreement for every tool by exercising each tool's declared output schema against real handler responses. SDK auto-validation checks shapes at runtime when a client is already connected. The test suite checks them in CI before a client exists. Given the choice, take the gate over the safety net.

Where this sits

The REST API answers what a program can do. The MCP server answers what an agent can do, given a specific identity, in a specific org, with a specific role, at a specific assurance level. That question is harder because the caller is fluent and has no memory of your security model.

The same rigor is what lets an agent gate its own recommendations and lets us ship an AI layer that acts like staff. One tool set, four transports, no special cases.

We ship in public. Come build with us.

Introzy is free to start. No card required.