WeaveKit Docs
Guides

MCP

MCP (Model Context Protocol) is how AI agents operate your data.

MCP (Model Context Protocol) is how AI agents operate your data. The engine exposes a streamable HTTP endpoint at /mcp (JSON-RPC 2.0): Claude, Cursor, or any MCP host connects once, then discovers your objects as tools.

Two things make that safe:

  • tools/list returns a per-identity, permission-filtered tool surface.
  • tools/call re-enforces RBAC against the proxied user at call time.

That double layer is the point. Every attempt is also rate-limited, alertable, and audited.

MCP is an adapter — shared by every project type and enabled by default (adapters.mcp undeclared = on).

Enable / configure

// weavekit.config.ts
export default {
  // ...
  adapters: {
    rest: { prefix: '/api' },
    mcp: {
      // endpoint: '/mcp/agent',   // optional: mount on a different path (default /mcp)
      identities: {
        alice: { id: 'u-alice', roles: ['sales'] },
        emma: { id: 'u-emma', roles: ['finance'], teamId: 't1' },
      },
      guardrails: {
        rateLimit: { windowMs: 60_000, max: 100 },   // optional; defaults shown
        alerts: { channel: 'console' },              // console | webhook | slack
      },
    },
  },
};
  • endpoint — path to mount on (default /mcp). Change it if /mcp collides with another route in your deployment; client URLs must match.
  • identities — the static on-behalf-of directory (ref → RbacSubject). A session's proxied user must resolve here; a missing or unknown ref is rejected at session establishment.
  • guardrails.alerts — any infrastructure/alerts factory config (webhook/slack channels reuse url/webhookUrl, and so on).
  • enabled: false turns the endpoint off entirely.

How an agent connects

  1. POST /mcp with Authorization: Bearer <apiKey> and X-Weavekit-On-Behalf-Of: <ref>, carrying the initialize request. The engine creates a session bound to the resolved identity and returns an Mcp-Session-Id header for reuse.
  2. tools/list returns the tool surface compiled for that identity. RBAC decides which objects and operations appear.
  3. tools/call runs through the RBAC-decorated data-access layer against the session identity. A call-level onBehalfOf argument is an optional temporary override.

Auth failures return 401 before the transport is entered. A missing or unknown on-behalf-of ref fails the session with a clear error.

Tool surface

For every object with read permission, the engine compiles:

ToolPurposeKey args
search_<object>list (row scope applied)filter, sort, limit (≤1000), offset, fields
get_<object>fetch one by primary keythe object's primary-key field name (e.g. doc_no)
create_<object>create (writable fields only)data
update_<object>update (RBAC update whitelist only)primary-key field, changes
delete_<object>delete (row scope applied)the object's primary-key field name

The primary-key argument is named after the schema's primary: true field — never a hardcoded id. So get_repair_order takes { doc_no } when the object's primary field is doc_no.

Always present:

ToolPurpose
list_objectsobjects the identity can read ([{ name, label }])
describe_objectschema + the identity's effective permissions

Tool shaping follows RBAC exactly: an object with no listed role yields zero tools; update: [] yields no update_; fields.exclude fields are stripped from parameter schemas and from results. Argument schemas are plain JSON Schema (field types map 1:1 to string/number/integer/boolean/object/enum, relations to their target primary-key type).

Guardrails

  • Rate limiting — per-agent-key sliding window (default 100/60s). Over-limit calls return an isError result and fire a warn alert.
  • Alerts — injected AlertSink (default console; webhook/slack via config).
  • Audit — every tool attempt writes mcp.tool.<name> to the unified weavekit_audit table (action prefix ACTION_PREFIXES.MCP_TOOL), including RBAC denials and failures. actorId is the agent key; meta carries { onBehalfOf, subjectId, roles, agentLabel, tool }. Audit is best-effort — a failing sink never blocks the tool call.

Wiring

createEngine wires the endpoint for you:

const engine = await createEngine({
  databaseUrl: process.env.DATABASE_URL,
  schemaDir: '.',
  auth: { source: { 'sk-agent': { id: 'agent-1', roles: ['agent'] } } },
  adapters: { mcp: { identities: { alice: { id: 'u-alice', roles: ['sales'] } } } },
});
await engine.app.listen({ port: 3000 });
// MCP endpoint: http://localhost:3000/mcp

You inject the sinks at assembly: audit (a buffered subsystem sink, or a no-op when audit is disabled), alerts (createAlerts), and identity (mcp.identities — a static directory or your own IdentityResolver). The MCP adapter itself depends only on core contracts; adapters never import subsystems or infrastructure.

For a full end-to-end walkthrough — a customer with an existing CRM (customers / orders tables) bringing agents in through the static identity directory — see the customer integration practice.

The engine is a bridge to your database, not a DDL runner. createEngine loads the schema and never touches your tables. Build or alter tables explicitly with weave migrate, and see How migration handles existing tables for what it does and doesn't change. (Greenfield and business projects can set migrate.auto: true instead.)

Plugging in your own user store

Both auth.source and mcp.identities accept either a static map (the defaults above) or a resolver function. That lets you drive authentication and on-behalf-of identity from your own users, roles, and teams instead of hardcoded config.

  • auth.sourceRecord<string, RbacSubject> | AuthResolver, where AuthResolver = (header) => subject | null | Promise<...>. The resolver receives the full Authorization header (including the Bearer prefix) and may verify a JWT or look up the user asynchronously. Return null for unauthenticated (401).
  • mcp.identitiesRecord<string, RbacSubject> | IdentityResolver, where IdentityResolver = (ref) => subject | null | Promise<...>. The resolver may query your user table and return the subject with its roles and team. Return null for an unknown ref (the session is rejected with 400).
// weavekit.config.ts — auth + identities driven by the customer's user table
export default {
  schemaDir: '.',
  auth: {
    source: async (header) => {
      const token = header?.replace(/^Bearer\s+/i, '');   // verify your JWT here
      return token === undefined ? null : { id: `agent-${token}`, roles: ['agent'] };
    },
  },
  adapters: {
    mcp: {
      identities: async (ref) => {                          // query the customer's user table
        const row = await pool.query(
          `SELECT id, role, team_id FROM crm_users WHERE id = $1`, [ref]);
        if (row.rows.length === 0) return null;
        const { id, role, team_id } = row.rows[0];
        return { id, roles: [role], ...(team_id ? { teamId: team_id } : {}) };
      },
    },
  },
} satisfies EngineConfig;

A resolver wins over the static map when both are provided for the same field. Either way, the tool surface is compiled per identity and RBAC is re-enforced at call time — the double layer never changes.

For the full runnable case (a customer crm_users table, JWT auth source, and MCP end to end), see the user-table identity practice.

Architecture (src/adapters/mcp)

FileResponsibility
generate.tsRBAC → tool surface + JSON Schema compilation
tools.tstool execution via data-access + audit + error mapping
introspection.tslist_objects / describe_object
guardrails.tssliding-window rate limit + alert/audit injection
session.tssession model + in-memory store (TTL expiry)
http.tsfastify /mcp routes; per-session SDK transport + server wiring
index.tsregisterMcp assembly (EngineMcpConfig)

The SDK transport is stateful per session: each session owns one StreamableHTTPServerTransport and one SDK Server (the SDK server may connect to only one transport). tools/list and tools/call are served via setRequestHandler, so the surface stays dynamic per identity.

Next

On this page