MCP Server Security · Object Capabilities

MCP server object capabilities security — capability-based authorization for MCP tool access, unforgeable capability references, and capability attenuation

The object capability model (ocap) is an authorization principle where the right to perform an action is represented by a reference to an unforgeable capability object — not by checking identity against an ACL. In MCP servers, ambient authority means that any code in the server process can call any tool, reach any database, or invoke any external service — because it's all reachable via module imports and global state. Replacing ambient authority with object capabilities makes authorization a structural property of the code: you literally cannot call a tool you were never handed a capability for, because you don't have a reference to it.

Ambient authority vs. object capabilities in MCP tool dispatch

AspectAmbient authority (typical MCP server)Object capability model
How access is grantedImplicit — all code in the process has access to all imported modulesExplicit — code receives a capability reference; without it, access is structurally impossible
Authorization check locationRuntime ACL check deep in the call stack, often skipped by bugsAt construction time — the capability object only exists if authorization was granted
Confused deputyPossible — any trusted intermediary can be manipulated into using its ambient authority on behalf of an attackerPrevented — intermediaries only hold the capabilities they were explicitly given; they can't use authority they don't hold
AttenuationHard — requires rebuilding ACL checks in wrappersNatural — wrap the capability object to remove or constrain permissions before passing it on
RevocationComplex — must purge from ACL tables and invalidate cachesDrop the reference; any holder of a revocable forwarder automatically loses access

Implementing object capabilities in a Node.js MCP server

Object capabilities in JavaScript are simply closures that encapsulate state and expose only the operations they should permit. The key discipline: the capability object is the only reference to the underlying resource — it is never exposed globally or through module exports accessible to untrusted code.

// Bad pattern: ambient authority — any module can import and call these
import { db } from './database.js';
import { emailClient } from './email.js';

export function handleToolCall(name, args) {
  if (name === 'queryUsers') return db.query('SELECT * FROM users WHERE id = ?', [args.userId]);
  if (name === 'sendAlert') return emailClient.send({ to: args.to, body: args.body });
}

// Good pattern: capability-based — tools only receive the capabilities they need
function createQueryCapability(db, allowedTable) {
  return {
    query: (userId) => db.query(
      `SELECT * FROM ${allowedTable} WHERE id = ? AND deleted_at IS NULL`,
      [userId]
    ),
    // No insert, update, delete — not in this capability
  };
}

function createEmailCapability(emailClient, allowedDomain) {
  return {
    send: (to, subject, body) => {
      if (!to.endsWith(`@${allowedDomain}`)) throw new Error('CAP_EMAIL_DOMAIN_VIOLATION');
      return emailClient.send({ to, subject, body });
    },
  };
}

// At startup: construct capabilities with their constraints baked in
const userQueryCap = createQueryCapability(db, 'users');
const internalEmailCap = createEmailCapability(emailClient, 'internal.example.com');

// Tool handler only receives the capabilities it is explicitly passed
function createToolHandler({ queryCap, emailCap }) {
  return function handleToolCall(name, args) {
    if (name === 'queryUser')  return queryCap.query(args.userId);
    if (name === 'sendAlert')  return emailCap.send(args.to, args.subject, args.body);
    throw new Error('UNKNOWN_TOOL');
  };
}

const toolHandler = createToolHandler({
  queryCap: userQueryCap,
  emailCap: internalEmailCap,
});
// toolHandler has no direct access to db or emailClient — only the attenuated capabilities

Capability attenuation via membrane objects

A membrane is a capability wrapper that interposes on all operations, applying additional constraints before delegating to the underlying capability. In MCP servers, membranes are useful for tenant isolation: each tenant session receives a membrane over the base capability that constrains all operations to that tenant's data.

function createTenantMembrane(baseCap, tenantId) {
  return {
    query: (userId) => {
      // Attenuate: enforce tenant scope on every call
      return baseCap.queryWithTenant(userId, tenantId);
    },
    // Operations not in the membrane interface are structurally inaccessible
    // baseCap.adminQuery() cannot be reached through the membrane
  };
}

// Session setup: each session gets a membrane scoped to its tenant
function onSessionStart(tenantId, baseCap) {
  const sessionCap = createTenantMembrane(baseCap, tenantId);
  // Pass sessionCap to the tool handler; it cannot exceed tenant scope
  return createToolHandler({ queryCap: sessionCap, emailCap: internalEmailCap });
}

Revocable capabilities for session invalidation

function createRevocableCapability(target) {
  let alive = true;

  const proxy = new Proxy(target, {
    get(obj, prop) {
      if (!alive) throw new Error('CAP_REVOKED');
      return obj[prop];
    },
    apply(obj, thisArg, args) {
      if (!alive) throw new Error('CAP_REVOKED');
      return obj.apply(thisArg, args);
    },
  });

  const revoke = () => { alive = false; };

  return { cap: proxy, revoke };
}

// On session logout or token expiry:
const { cap: sessionCap, revoke: revokeSession } = createRevocableCapability(baseCap);
// Pass sessionCap to tool handler
// On logout:
revokeSession(); // All future calls through sessionCap throw immediately

JavaScript's native Proxy.revocable() provides built-in revocable references: const { proxy, revoke } = Proxy.revocable(target, handler);. Call revoke() and the proxy becomes permanently unusable, throwing a TypeError on any property access. Use this for session capabilities that must be invalidated on logout.

SkillAudit findings for object capability violations in MCP servers

CRITICAL −20Tool handler has access to full database connection without attenuation — any tool call or bug in the handler can execute arbitrary SQL, including admin operations on other tenants' data
HIGH −16No tenant isolation in tool dispatch — all sessions share the same capability objects; a prompt-injection attack in one session can invoke tools on behalf of another session's data scope
HIGH −14Capabilities not revoked on session expiry — expired sessions retain live references to database and API clients; no revocation path exists
MEDIUM −10Tool receives more capabilities than needed (e.g., read-write database access for a read-only tool) — violates principle of least authority; any bug in the tool risks writes

SkillAudit's static analysis identifies ambient authority patterns — global imports used directly in tool handlers without attenuation. Run a free audit to check your MCP server's authorization architecture.