The Hidden Security Gap in Multi-Tenant MCP Servers
When you build a multi-tenant SaaS application or an MCP (Model Context Protocol) server that serves multiple organizations, cross-tenant data leakage is one of the most dangerous vulnerabilities you can ship. A single missing organizationId filter in a database query can expose one tenant's data to another — and traditional security scanners like Snyk, Semgrep, and CodeQL don't catch these patterns.
That's why I built mcp-tenant-isolation — a static analysis scanner with 57 deterministic rules specifically designed to catch tenant isolation failures in multi-tenant codebases.
What Is Tenant Isolation?
Tenant isolation ensures that data belonging to one organization (tenant) is never accessible to another. In a multi-tenant SaaS app, every database query, cache read, and file access must be scoped to the current tenant's organizationId.
The most common failure looks like this:
// VULNERABLE: No organizationId filter
const users = await prisma.user.findMany({
where: { role: 'admin' }
});
// SECURE: Tenant-scoped query
const users = await prisma.user.findMany({
where: { role: 'admin', organizationId: ctx.orgId }
});
It looks obvious in isolation. But in a codebase with 100+ API routes, dozens of lib functions, and complex middleware chains, missing tenant filters are easy to miss in code review and impossible for traditional SAST tools to detect.
Why Traditional Scanners Miss This
Tools like Snyk and Semgrep are excellent at detecting:
- SQL injection
- XSS
- Dependency vulnerabilities
- Secret leakage
But they don't understand tenant context. They don't know that organizationId is the tenant boundary. They don't track which functions require tenant guards. They can't tell you that prisma.user.findMany({ where: { role: 'admin' } }) is missing a critical tenant filter.
mcp-tenant-isolation fills this gap with 57 rules across 7 categories:
Rule Categories
| Category | Rules | What It Detects |
|---|---|---|
| Database Queries (DBQ) | 12 | Missing organizationId in findMany, findUnique, updateMany, deleteMany, groupBy, aggregate
|
| Schema (SCH) | 6 | Missing tenant columns, missing composite indexes, missing RLS policies |
| File Storage Isolation (FSI) | 4 | Unscoped S3/Blob keys, missing tenant prefix in file paths |
| Cache Key Scoping (CKS) | 5 | Redis/memoization keys without tenant prefix |
| IDOR | 8 | Insecure direct object references — accessing resources by ID without ownership check |
| Logging (LOG) | 6 | Missing tenant context in structured logs |
| MCP-Specific (MCP) | 15 | Tool visibility, cache prefix, session binding, credential vault isolation, prompt injection surface |
Quick Start
Install
npm install -g mcp-tenant-isolation
Scan your codebase
mti scan ./src
Output
The scanner produces a clear pass/fail verdict with detailed findings:
╔══════════════════════════════════════════╗
║ MCP Tenant Isolation Scanner v1.6.2 ║
║ Verdict: FAIL ║
╚══════════════════════════════════════════╝
Findings: 47 active, 12 suppressed, 3 baseline
DBQ-001 HIGH app/api/users/route.ts:23
Missing organizationId in prisma.user.findMany()
Remediation: Add organizationId to the where clause:
where: { ..., organizationId: ctx.orgId }
IDOR-003 HIGH app/api/documents/[id]/route.ts:45
No ownership check after findUnique by ID
Remediation: Verify result.organizationId === ctx.orgId
CI/CD Integration
GitHub Actions (Pre-built Action)
- uses: subodhkc/mcp-tenant-isolation@v1
with:
path: ./src
format: sarif
Manual npx
- name: Run tenant isolation scan
run: npx mcp-tenant-isolation scan ./src --format sarif --output scan.sarif
- name: Upload to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scan.sarif
SARIF output integrates directly with GitHub Advanced Security — findings appear in your repo's Security tab alongside CodeQL results.
MCP Server Integration
The package includes an MCP server with 4 tools that AI agents (Claude Desktop, Cursor, Cline) can use:
{
"mcpServers": {
"mcp-tenant-isolation": {
"command": "npx",
"args": ["-y", "mcp-tenant-isolation", "mcp"]
}
}
}
Tools available:
-
scan— Run tenant isolation scan on a codebase -
rules— List all 57 rules with descriptions and remediation hints -
config— Get/set scanner configuration -
version— Get scanner version info
This means you can ask Claude: "Scan my src directory for tenant isolation issues" and get structured findings with remediation hints — directly in your chat.
Supported Frameworks
- ORMs: Prisma, Drizzle, raw SQL (Knex, pg, mysql2)
- Web frameworks: Next.js (App Router & Pages), Express, Fastify
- Cache: Redis (ioredis, redis), in-memory memoization
- Storage: S3, Vercel Blob, local filesystem
- Auth: NextAuth, custom JWT, session-based
Output Formats
| Format | Use Case |
|---|---|
| Terminal | Local development with pass/fail verdict |
| JSON | Programmatic consumption, custom dashboards |
| SARIF 2.1.0 | GitHub Code Scanning, Azure DevOps |
| AI JSON | AI agent consumption with remediation hints |
| Markdown | Shareable reports for PRs and team review |
Suppression and Baselines
Not every finding is actionable immediately. The scanner supports:
-
Suppressions —
.mtirc.jsonconfig to suppress specific findings with reasons - Baselines — Mark existing findings as known debt, only fail CI on new issues
- Rule packs — Enable/disable rule categories per project
{
"suppress": [
{
"rule": "DBQ-001",
"file": "app/api/public/badges/route.ts",
"reason": "Public route, no tenant context needed"
}
]
}
Real-World Results
I ran the scanner against a production Next.js SaaS codebase with 200+ API routes:
- 47 active findings — including 23 missing tenant filters, 8 IDOR risks, 5 unscoped cache keys
- 12 suppressed — public routes with legitimate no-tenant-context access
- 3 baselined — known debt scheduled for next sprint
- Scan time: 4.2 seconds for 50,000+ lines of TypeScript
Why This Matters for MCP Server Developers
If you're building MCP servers that serve multiple tenants (organizations, teams, users), tenant isolation is the security boundary. The MCP protocol doesn't enforce tenant isolation — it's up to your implementation.
The 15 MCP-specific rules check for:
- Tool visibility — Are all tools visible to all tenants? Should some be org-scoped?
- Cache prefix — Is the MCP response cache keyed by tenant?
- Session binding — Is the MCP session bound to a specific tenant?
- Credential vault — Are per-tenant credentials isolated in the vault?
- Prompt injection surface — Does the server expose tenant context to prompt injection?
Links
- GitHub: subodhkc/mcp-tenant-isolation
- npm: mcp-tenant-isolation
- Landing page: haiec.com/mcp-tenant-isolation
-
MCP Registry:
io.github.subodhkc/mcp-tenant-isolation - License: MIT
If you found this useful, star the repo on GitHub and share with your team. Every missing tenant filter is a potential data breach waiting to happen.
Top comments (0)