MCP support went from a niche spec to a standard buyer question faster than most of us expected. If you ship developer-facing software, someone has probably already asked whether Claude, ChatGPT, Cursor or an n8n agent can talk to it, and for most self-hosted products the honest answer is "not safely." We recently shipped a native MCP server for CRM AI agents inside Perfex CRM, a self-hosted PHP application, and this post walks through the design decisions: where the JSON-RPC 2.0 endpoint lives, why we permission-filter 148 tools per API token instead of exposing everything, and why read-only is our default posture. Full disclosure: we build and sell the module discussed here, but the trade-offs apply to anyone embedding MCP in an existing product.
Some context so the constraints make sense. Our REST API module for Perfex CRM (currently v3.0.3) exposed CRM resources - customers, leads, invoices, projects, tasks, tickets - over plain REST for many releases before v3, with per-token permissions deciding what each integration may touch. Version 3 added the MCP layer on top of that existing surface, and that turned out to be the most important architectural fact of the whole project: we did not design an agent interface from scratch, we projected an already permissioned API into the MCP tool model.
Why the MCP server lives inside the PHP app
The path of least resistance would have been a bridge: a small Node or Python process that speaks MCP on one side and calls the REST API on the other. Many MCP servers for existing products are built this way, and for a hosted SaaS with a single deployment it can be a fine answer.
For self-hosted software it falls apart at distribution. Perfex installs run in environments their owners control and we do not - often shared hosting or a small VPS - and asking an administrator to operate a second long-lived process next to a PHP app is a support burden at best and impossible at worst. A bridge also duplicates state: the API token lives in the CRM, a copy lives in the bridge config, and the permission model has to be re-implemented there or, more commonly, skipped.
So the MCP server is native. The endpoint is POST /api/mcp, speaking JSON-RPC 2.0 over Streamable HTTP, and it ships inside the same module as the REST API, enabled by a toggle in the module's platform settings. This matches PHP's execution model surprisingly well: with Streamable HTTP each JSON-RPC message arrives as an ordinary HTTP POST, so there is no resident process to babysit - just the request-per-execution model PHP has always had. Authentication reuses the existing API tokens (sent in the authtoken header, created in the API Management screen under Setup in the admin area), so an agent presents exactly the credential any integration would, and the same middleware evaluates it.
An MCP server for CRM AI agents should not expose everything
The module defines 148 tools. A naive MCP integration would return all of them from tools/list and enforce permissions only when a tool is called. We rejected that, and it is the decision we would defend hardest.
Instead, tools/list is permission-filtered: a tool appears only if the token behind the request holds the matching permission. Grants are per resource and per verb - Get, Create, Update, Delete - so a token limited to reading invoices produces a short tool list that can read invoices and do nothing else. Granting a capability in the permission editor is the same act that makes its tools visible to an agent.
Three reasons drove this. First, context economics: every tool definition you return is prompt space the client model has to carry, and tool-selection accuracy degrades as the list grows. An agent doing invoice lookups gains nothing from the schemas of tools it can never call. Second, security shape: a capability that is never listed cannot be called, planned around or hallucinated into a multi-step workflow; failing at list time is a stronger invariant than failing at call time. Third, honesty in errors: the expose-everything approach produces agents that confidently attempt forbidden operations, burn a round trip, then improvise around a 403. Filtering the list keeps the model's picture of the world consistent with its actual authority.
Read-only agents as the safety default
LLM agents are probabilistic. A wrong search is a wasted call; a wrong delete is an incident. So our recommended starting posture is a read-only token per agent. The permission editor has a one-click Read-only preset for exactly this reason, alongside per-token request limits, quotas and expiry dates.
The upgrade path is deliberate: run the agent read-only until the workflow proves out, then grant Create or Update on the specific resources it needs, one at a time. For write paths, the API supports an Idempotency-Key header on POST, so a retried create replays the stored response instead of duplicating a record - a property that matters more with agents than with humans, because agents retry enthusiastically. There is also an optional staff-level visibility mode that ties a token to a staff member, scoping data exactly the way the admin panel would for that user.
What agents are actually good at against a CRM
The pattern from our own testing is consistent: agents shine at judgment over small result sets and are a poor fit for repetition.
Good agent tasks look like "find this customer, summarize their open tickets and unpaid invoices, and draft the follow-up", cross-resource questions that would otherwise mean four admin screens, and first-draft record creation where a human reviews before anything is sent. The query itself is fuzzy and the value is synthesis.
Repetitive, fully specified work belongs on the deterministic side of the same module. The OpenAPI spec at GET /api/openapi documents 72 paths and 139 operations for coded integrations, with a reference copy in our examples repository; POST /api/batch executes up to 50 operations in one request; 124 webhook events across 22 event groups push changes out, HMAC-signed and delivered asynchronously with retries; and the n8n community node @themesic/n8n-nodes-perfex-crm covers 19 resources with 108 operations plus a polling trigger, next to native Zapier and Make polling endpoints. The rule of thumb we give customers: if you can write the steps down exactly, use REST, batch or a workflow tool; if the steps require reading and deciding, use the MCP server.
Where this approach is the wrong tool
An honest limits section, because MCP is having a hype moment. Do not point an agent at bulk data migration or high-volume sync - per-step LLM latency and token cost make it strictly worse than a scripted REST client, which is what the batch endpoint is for. Do not give an unattended agent write access to financial records; keep a human in the loop for anything that emails a customer or touches money. And if your needs are purely Zapier-style automation with no natural-language component, the connectors will serve you better than MCP will. The MCP server is a complement to the API, not a replacement for engineering.
Key facts
| Item | Detail |
|---|---|
| Module version | 3.0.3 (updated 2026-08-03) |
| MCP endpoint | POST /api/mcp, JSON-RPC 2.0, Streamable HTTP |
| Tools | 148, permission-filtered per API token |
| REST surface | 72 paths, 139 operations (OpenAPI at GET /api/openapi) |
| Webhooks | 124 events in 22 groups, HMAC-signed, async retries |
| Batch | POST /api/batch, up to 50 operations |
| Track record | 2,941 sales on CodeCanyon, rated 4.91/5 from 44 verified CodeCanyon reviews |
Links
- REST API module for Perfex CRM - product page
- All Perfex CRM modules
- Examples repository
- MCP setup guide
If you are embedding MCP in your own product and made different calls on tool filtering or write access, we would genuinely like to hear how it went - questions and pushback welcome in the comments.
Top comments (1)
CRM is a good test case for MCP because the tool boundary has to respect business state. An agent should not just call "update customer" because it can. It needs clear scopes, audit trails, validation, and human approval around actions that change relationship history.