MCP Fusion/Security and governance/Multi-tenant connectors

Multi-tenant connectors

Ask AI about Vinkius

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:

typescript
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:

typescript
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:

typescript
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 function

A 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

ConcernMechanismWhere it lives
IdentityJWT, API key or OAuth tokencontextFactory + auth middleware
Data isolationper-tenant client from the contextcontextFactory
Capability visibilitytags + filtertool list construction
Field isolationrole-based PresentersPresenter per audience
Secret isolationBYOC credentialsruntime injection
Fair userate-limit key from ctxrate limiter middleware
Proofcontract digest + audit eventslockfile 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