MCP Adapter
@vengtoo/mcp-adapter wraps every tool registered on a Model Context Protocol server with a Vengtoo authorization check. Before a tool runs, the adapter asks Vengtoo "is this caller allowed to invoke this tool with these arguments?" and short-circuits the call on a deny.
One line of integration. Tool arguments flow into resource.attributes, so you can write ABAC conditions like "support_rep can invoke issue_refund only if amount < 100" without touching adapter code.
Install
npm install @vengtoo/mcp-adapter @modelcontextprotocol/sdk
Requires Node 18+.
Quick start
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { VengtooAdapter } from "@vengtoo/mcp-adapter";
import { z } from "zod";
const server = new McpServer({ name: "acme-support", version: "1.0.0" });
server.tool(
"issue_refund",
{ customer_id: z.string(), amount: z.number() },
async ({ customer_id, amount }) => {
return await payments.refund(customer_id, amount);
},
);
const vengtoo = new VengtooAdapter({
apiKey: process.env.VENGTOO_API_KEY,
agentUrl: "http://localhost:8181/access/v1/evaluation",
subjectType: "user",
subjectResolver: (ctx) => (ctx.mcpContext as any)?.session?.userId,
});
vengtoo.wrapAllTools(server); // wraps every registered tool in place
const transport = new StdioServerTransport();
await server.connect(transport);
After wrapAllTools, every tool invocation is intercepted. If Vengtoo denies, the original handler does not run — the MCP client gets an error back with a deny reason.
How it maps to Vengtoo
For each tool call, the adapter POSTs:
{
"subject": { "id": "<from subjectResolver>", "type": "<subjectType>" },
"resource": {
"type": "mcp_tool",
"name": "<tool name>",
"properties": <tool arguments>
},
"action": { "name": "invoke" }
}
| Vengtoo field | Source in MCP |
|---|---|
subject.id | Returned by subjectResolver(ctx) |
subject.type | subjectType config field (required) |
resource.type | Fixed string "mcp_tool" |
resource.name | The registered tool name |
resource.properties | The tool arguments |
action.name | Fixed string "invoke" |
Resources are matched by type + name, so you don't have to pre-register every tool as a Vengtoo resource UUID. Write policies against the tool name and you're done.
Configuration
new VengtooAdapter({
// Auth — pick one
apiKey: "azx_...",
// or OAuth Client Credentials:
clientId: "client_...",
clientSecret: "azx_cs_...",
// Endpoints — pick one or both.
// If both are set, the agent is tried first and the cloud is the fallback.
agentUrl: "http://localhost:8181/access/v1/evaluation",
cloudUrl: "https://api.vengtoo.com/access/v1/evaluation", // default
// Resolve the subject id from the MCP request context.
subjectResolver: (ctx) => ctx.mcpContext.session.userId,
// Required. Must match the subject type used when creating subjects in Vengtoo
// (e.g. "user", "ai_agent", "service").
subjectType: "ai_agent",
// Optional. Suppresses the Vengtoo reason string in the deny response
// returned to the MCP client.
denyMessage: "forbidden",
timeoutMs: 10_000,
});
Subject resolution
There is no user identity in the MCP tool-call payload by default — the protocol carries only the tool name and arguments. You decide what identity becomes the Vengtoo subject.
// End-user identity (default pattern for v1).
// Alice signs in to the MCP server; her id travels in session/headers.
subjectResolver: (ctx) => (ctx.mcpContext as any)?.session?.userId;
// Agent-as-subject. The agent itself has a Vengtoo identity.
// Useful for autonomous / scheduled agents.
subjectResolver: () => "agent_abc";
Delegation patterns (Alice asked Claude, which is invoking issue_refund — both must be allowed) are on the roadmap. The resolver is shaped to support them forward-compatibly.
Endpoints
- Local agent — fastest. Set
agentUrlto the agent's/access/v1/evaluation. Typical sidecar:http://localhost:8181/access/v1/evaluation. - Cloud — set
cloudUrlalone if you don't run an agent, or leave both set for automatic fallback when the agent is unreachable.
See Vengtoo Agent for running the agent locally.
Usage patterns
Wrap all tools after registration
server.tool("lookup_customer", schema, handler);
server.tool("issue_refund", schema, handler);
server.tool("delete_account", schema, handler);
vengtoo.wrapAllTools(server);
Recommended default — covers every tool including ones added by third-party extensions.
Wrap per tool at registration
const vengtoo = new VengtooAdapter({ /* ... */ });
server.tool(
"issue_refund",
schema,
vengtoo.wrapTool(
async ({ customer_id, amount }) => payments.refund(customer_id, amount),
"issue_refund",
),
);
Use when you want some tools gated and others not.
Manual check
const result = await vengtoo.check(
{ toolName: "issue_refund", args, mcpContext: extra },
"issue_refund",
args,
);
if (!result.allowed) throw new Error(result.reason);
Use inside custom handlers that need the decision result before deciding how to respond.
Writing policies against tool calls
The adapter's mapping makes tool-call authorization work like any other Vengtoo policy. With the application and mcp_tool resource type in place, everything else is a standard RBAC/ABAC policy.
Example — support_rep can refund up to $100; support_manager bypasses the limit:
resource "vengtoo_resource_type" "mcp_tool" {
application_id = vengtoo_application.support.id
name = "mcp_tool"
actions = ["invoke"]
}
resource "vengtoo_resource" "issue_refund" {
application_id = vengtoo_application.support.id
name = "issue_refund"
type = vengtoo_resource_type.mcp_tool.id
}
resource "vengtoo_policy" "rep_small_refunds" {
application_id = vengtoo_application.support.id
name = "rep-can-refund-under-100"
effect = "ALLOW"
priority = 50
resources = [{
resource_id = vengtoo_resource.issue_refund.id
actions = ["invoke"]
}]
condition = "input.resource.attributes.amount < 100"
}
resource "vengtoo_policy_assignment" "rep_refund_policy" {
policy_id = vengtoo_policy.rep_small_refunds.id
entity_type = "role"
entity_id = vengtoo_role.support_rep.id
}
Because tool arguments arrive in resource.attributes, your policy conditions reference them directly — no adapter-side filtering, no wrapping per tool.
Deny behavior
The adapter fails closed. A deny means:
- The original tool handler does not run.
- The MCP client receives an error response. If
denyMessageis set, it's used verbatim; otherwise the adapter surfaces thereasonreturned by Vengtoo.
Network errors, 401s, and 5xx responses from Vengtoo are treated as denies. When both agentUrl and cloudUrl are configured, a network or 5xx failure against the agent triggers a fallback to cloud before the adapter denies.
Reference demo
vengtoo-ai-agent-demo is a runnable end-to-end example — MCP server, three tools, two users, RBAC + ABAC policies seeded via Terraform. Clone, terraform apply, npm run demo, read the output.