MCP Fusion/Security and governance/Multi-tenant connectors
Multi-tenant connectors
One connector, many tenants, no special module: per-request tenant resolution, tag-filtered capability visibility and role-based Presenters that show each tenant what it may see.
Serving several customers from one connector is not a feature you bolt on; it is a property of how context, visibility and perception are built. MCP Fusion has no tenancy module because the pieces you already use compose into one. This page is the recipe.
1. Resolve the tenant per request
Every request starts with a fresh ctx, so the tenant is decided once, before any handler:
interface AppContext {
db: PrismaClient;
tenantId: string;
role: 'viewer' | 'admin';
}
registry.attachToServer(server, {
contextFactory: async (extra) => {
const claims = await verify(extra.session?.authToken);
return {
db: pool.forTenant(claims.tenant_id),
tenantId: claims.tenant_id,
role: claims.role,
};
},
});Two rules make this safe. The factory returns a fresh object per request: no request can observe another's context. Middleware writes into that object with prototype-pollution guards, so a malicious argument name cannot reach __proto__. Handlers then use ctx.db or ctx.tenantId as the only door to data, and the tenant id never comes from tool arguments.
2. Scope capabilities by tag
Tenants rarely get the same surface. Tags plus a filter decide what exists for whom:
const filterFor = (role: string) =>
role === 'admin'
? { anyTag: ['public', 'admin'] }
: { tags: ['public'], exclude: ['internal'] };
attachToServer(server, { filter: filterFor(ctx.role) });In this shape the free deployment exposes public tools, an enterprise deployment exposes admin tools, and the handler codebase is identical. The filter is evaluated where the tool list is built, under the request's own context. See Tool exposition.
3. Shape perception by role
Visibility decides which tools a role sees; perception decides what those tools show. The verified pattern is two tools over one handler, selected by the filter from step 2:
const listPeople = async (input, ctx) => ctx.db.people.findMany();
f.query('people.list_public')
.describe('List people in the tenant, without contact details')
.tags('public')
.returns(ViewerPresenter) // schema without email or phone
.handle(listPeople);
f.query('people.list_admin')
.describe('List people in the tenant with full records')
.tags('admin')
.returns(AdminPresenter)
.handle(listPeople); // same handler functionA viewer-role agent receives only people.list_public and never sees contact fields; an admin agent receives both, with the full Presenter. The field rules and redaction live in one place per audience, and the contract digest records both surfaces. Contextual rules, written as (data, ctx) => string[], add role-dependent guidance on top. See Models and Presenters.
4. Isolate credentials per tenant
Vendor keys should not be shared across tenants either. Declare them with defineCredentials and read them per request: on Vinkius Cloud the runtime injects each buyer's secrets per request, and locally the same accessor reads your environment. See Credentials.
5. Attribute cost and traffic
Every call can be attributed: pass ctx.tenantId as the rate-limit key so one tenant cannot exhaust another's budget, and emit the tenant id in audit events and telemetry. In the Vinkius console the same connector surfaces per-server cost and reliability, documented in AI spend and Tool reliability.
The pattern in one table
| Concern | Mechanism | Where it lives |
|---|---|---|
| Identity | JWT, API key or OAuth token | contextFactory + auth middleware |
| Data isolation | per-tenant client from the context | contextFactory |
| Capability visibility | tags + filter | tool list construction |
| Field isolation | role-based Presenters | Presenter per audience |
| Secret isolation | BYOC credentials | runtime injection |
| Fair use | rate-limit key from ctx | rate limiter middleware |
| Proof | contract digest + audit events | lockfile and audit sink |
There is no runtime switch to forget and no cross-tenant cache to misconfigure, because nothing is shared unless you explicitly share it. That is the difference between a multi-tenant connector and a single-tenant connector with a tenant column.
Next steps
- Authentication: where the identity comes from
- Middleware and context: derivation and isolation mechanics
- Security pipeline: the layers each request crosses
