tenant-scoped-crm
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tenant-scoped-crmshow my open opportunities"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-tenant-scoped-crm
A reference MCP server demonstrating safe agent access to multi-tenant CRM data: tenant scoping enforced in the data layer rather than the prompt, tools that execute under the calling user's permissions rather than a service account, human confirmation on writes, and an audit record for every tool call. This is a reference implementation, clearly labelled as such. It is not client work and not a production system. It exists to make one architectural argument concrete and runnable.
The problem
An AI agent over a CRM is three security problems wearing one trench coat.
Tenant-aware retrieval. In a multi-tenant system the agent must never return one customer's data to another. The tempting shortcut is to fetch broadly and filter afterwards, or to instruct the model in its system prompt to stay within a tenant. Both are wrong. A prompt is not a security control. If tenant scoping is a post-filter or a system-prompt instruction, you have a data-leakage bug that will not show up in testing. It will show up in a customer's screenshot. Scoping belongs in the retrieval layer, where there is no code path that can express a query without it.
Permission-aware tools. The agent's tools must run under the calling user's role, not a service account with full access. A sales rep should see only their own opportunities, a manager the team's, an admin the tenant's. If the tools run under god-mode, you have built a privilege escalation path with a chat interface on it.
Actions requiring approval. Reads within scope can be unrestricted. Writes cannot. A write tool must show what it would do and require an explicit human confirmation before it does it. And the confirmation flag must not be mistaken for permission: confirmation is not authorisation. The write has to be scoped to what the caller may change regardless of what they confirm.
Related MCP server: production-grade-mcp-agentic-system
The three security properties
Each property is enforced at a specific point in the code and proved by a specific test. The enforcement is not spread across the codebase or asserted in prose; it is one place, with one test pointed at it.
1. Tenant isolation, enforced in the data layer
Every query runs through a Repo that is constructed against a Context
(tenant, user, role) and binds the tenant into the SQL itself. The tenant
predicate is not a caller-supplied argument, and there is no method that omits
it. A valid id from another tenant returns nothing, because AND tenant_id = @tenantId sits on the row being returned, not on a join that could be dropped.
Enforcement point:
src/repo.ts.Proof:
tests/tenant-isolation.test.tshands a user in one tenant a real, existing id from another tenant and asserts that every read returns empty. The only thing standing between the caller and the other tenant's data is the tenant predicate, so the test feeds it genuine cross-tenant ids.
2. Permission-aware tools
Opportunity visibility is narrowed by role on top of tenant scoping: a rep sees their own, a manager sees their reports' plus their own, an admin sees the whole tenant. The role-to-visibility rule lives in one file and is called by the repository, never by a tool, so a tool cannot widen its own scope.
Enforcement point:
src/roles.ts(the visibility predicate), applied insrc/repo.ts.Proof:
tests/permissions.test.tsasserts the full matrix: rep sees only their own, manager sees the team's, admin sees the tenant's, and no role sees another tenant's rows.
3. Read-only by default, human confirmation on writes
There is exactly one write tool, update_opportunity_stage. Without
confirmed: true it writes nothing and returns a preview of the change it would
make. With confirmed: true it applies the change. Both the preview read and
the write go through the same tenant-and-owner-scoped repository queries, so a
preview cannot reveal, and a confirmed call cannot mutate, an opportunity outside
the caller's scope.
Enforcement point:
src/tools/update_opportunity_stage.ts, backed by the scopedgetOpportunityandupdateOpportunityStageinsrc/repo.ts.Proof:
tests/write-confirmation.test.ts. The case that matters most is "a CONFIRMED write against a same-tenant peer's opportunity changes nothing": one rep passesconfirmed: trueagainst another rep's opportunity in the same tenant, and nothing changes. Tenant scoping alone would allow it; owner scoping refuses it. That test is what shows confirmation alone is insufficient. The cross-tenant confirmed write is proved in the same file.
Every tool call, allowed or denied, writes an audit record (src/audit.ts,
via the dispatch middleware in src/dispatch.ts). The record holds the
timestamp, tenant, user, tool, validated arguments, result count, and decision.
It stores arguments and a count, never returned row contents: an audit log that
copies protected records into itself becomes a second, unscoped store of the data
it exists to protect. The get_audit_log tool is itself scoped, an admin sees
the tenant's trail, everyone else sees only their own rows.
The headline: isolation holds through the real server, not just in unit tests
Unit tests exercise the repository directly. The claim that matters is that isolation holds through the actual MCP server over its actual transport. It does.
Running the built server over stdio as rep-carol@globex, calling get_account
with acc-acme-1, a real and valid Acme account id:
serving as rep-carol@globex (role: rep)
get_account(acc-acme-1 = a REAL acme id) as carol/globex => null
list_opportunities as carol/globex => [ { "id": "opp-globex-1", ... "tenantId": "globex" ... } ]Carol asks for a genuine Acme row, by its real id, through the real server, and
gets null. Her opportunity list contains only her own tenant's row. The
isolation is a property of the data layer, so it does not matter that the request
arrived over MCP rather than from a test harness.
Known limitations, stated plainly
Identity is asserted, not authenticated. Under the stdio transport the caller's tenant and user come from environment variables (
CRM_TENANT,CRM_USER). What the caller asserts is only their tenant and user id; their role is resolved from the database, never trusted from the environment. But nothing authenticates the assertion itself. In production this is where real auth would sit.TypeScript cannot enforce "no code path can run an unscoped query." A tool could, in principle, import the database connection and bypass the repository. The type system will not stop it. The connection is therefore module-private in
src/db.tsand never exported, andtests/no-raw-db-access.test.tsasserts the contract structurally: no file undersrc/toolsimports the connection orbetter-sqlite3. Encapsulation plus that test is the guarantee, not the compiler.The SDK transport validation gap was closed, not left open. The MCP SDK's high-level server validates tool arguments before the tool callback runs and, on failure, returns an error without invoking our code, which would leave a schema-rejected call unaudited. Rather than weaken the published schemas to a passthrough (which would trade away the tool-design property to buy the auditability one), the server registers tools with the SDK for schema publication and then supersedes the low-level
CallToolhandler insrc/server.ts, so validation and auditing both happen in one place. Every call, valid or invalid or unknown-tool, produces an allow or deny record.tests/server-audit.test.tsproves this by driving a real MCP client over an in-memory transport and asserting both that the published schema is still strict and that a validation failure at the transport boundary is recorded as a deny.
What I would do differently in production
Postgres row-level security instead of an application-layer repository. The repository pattern here is a faithful demonstration, but the strongest place to enforce tenant isolation is the database itself, where even a compromised application cannot escape the policy.
Real authentication instead of environment variables, so identity is proved rather than asserted.
pgvector for the unstructured side of the CRM, so semantic retrieval is subject to the same tenant policy as structured queries.
Auditing at or in front of the transport, so that malformed and rejected calls are recorded by infrastructure that no application bug can bypass. This server closes the equivalent gap in application code; in production the audit boundary would sit lower.
Run it in 30 seconds
Requires Node 20. The database is in-memory and seeded on start, so there is no setup step.
npm i
npm test
CRM_TENANT=acme CRM_USER=rep-alice npm startnpm test runs the full suite, including the tenant-isolation and
write-confirmation tests described above. npm start builds and serves the MCP
server over stdio as the identity in the environment.
Seeded identities: tenant acme has rep-alice, rep-erin, mgr-bob,
admin-dan; tenant globex has rep-carol.
To attach it to Claude Desktop, build it (npm run build) and add this to
claude_desktop_config.json, using an absolute path:
{
"mcpServers": {
"tenant-scoped-crm": {
"command": "node",
"args": ["/absolute/path/to/mcp-tenant-scoped-crm/dist/index.js"],
"env": {
"CRM_TENANT": "acme",
"CRM_USER": "rep-alice"
}
}
}
}Change CRM_USER to rep-carol (with CRM_TENANT set to globex) to see the
same tools return a different, correctly scoped view of the data.
Note
Built as a portfolio piece to demonstrate architecture and discipline. The production multi-tenant automation work behind it is under NDA.
Licensed MIT. See LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/gvprime/mcp-tenant-scoped-crm'
If you have feedback or need assistance with the MCP directory API, please join our Discord server