Veto provides two integration patterns for Model Context Protocol servers. Both check authorization before your tool handler runs — they differ in how they handle denials.
| createVetoGuard | vetoMiddleware |
|---|
| Denial behavior | Returns MCP error response (isError: true) | Throws VetoError |
| Boilerplate | Less — wraps the handler | More — called inside the handler |
| Recommended | Yes | When you need custom denial logic |
Option 1: createVetoGuard (recommended)
createVetoGuard returns a protect function that wraps your tool handlers. When authorization is denied, the tool handler never executes — instead, protect returns an MCP-compatible error response with isError: true.
import { VetoClient, createVetoGuard } from "@useveto/node";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const veto = new VetoClient({ apiKey: process.env.VETO_API_KEY! });
const protect = createVetoGuard(veto, { agentId: "support-bot" });
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.tool(
"send-email",
{ to: z.string(), subject: z.string(), body: z.string() },
protect("send-email", async (params) => {
await sendEmail(params);
return { content: [{ type: "text", text: "Email sent!" }] };
}),
);
When authorization is denied, the response looks like this:
{
"content": [{ "type": "text", "text": "Authorization denied: ..." }],
"isError": true
}
Options
The agent ID to check authorization against. This must match the agent registered in Veto.
onError
"deny" | "allow"
default:"\"deny\""
What to do when the Veto API is unreachable (network error, timeout, or 5xx response).
"deny" — fail closed: block the tool call (default, recommended for production)
"allow" — fail open: let the tool call proceed
onDenied
(toolName: string, reason: string) => void
Optional callback invoked whenever authorization is denied. Use it for logging, metrics, or alerting.
Option 2: vetoMiddleware
vetoMiddleware returns a guard function you call at the start of each tool handler. If authorization is denied, it throws a VetoError. Use this pattern when you need to control the error flow yourself.
import { VetoClient, vetoMiddleware } from "@useveto/node";
const veto = new VetoClient({ apiKey: process.env.VETO_API_KEY! });
const guard = vetoMiddleware(veto, { agentId: "support-bot" });
server.tool("send-email", schema, async (params) => {
await guard("send-email", params); // throws VetoError if denied
await sendEmail(params);
return { content: [{ type: "text", text: "Sent!" }] };
});
vetoMiddleware accepts the same options as createVetoGuard.
Fail-closed behavior
By default, if the Veto API is unreachable — due to a network error, timeout, or server error — tool calls are blocked. This is the safe default: your agent does nothing rather than acting without authorization.
// Default: fail closed (recommended)
const protect = createVetoGuard(veto, {
agentId: "support-bot",
// onError defaults to "deny"
});
You can override this to fail open, but this is not recommended for production:
const protect = createVetoGuard(veto, {
agentId: "support-bot",
onError: "allow", // proceed even when Veto is unreachable
});
Setting onError: "allow" means your agent will execute tool calls without authorization checks if Veto is down. Only use this during development or in low-risk environments.
Logging denied actions
Use the onDenied callback to record when authorization is denied:
const protect = createVetoGuard(veto, {
agentId: "support-bot",
onDenied: (toolName, reason) => {
console.warn(`[veto] Denied: ${toolName} — ${reason}`);
metrics.increment("veto.denied", { tool: toolName });
},
});
The tool name you pass to protect() or guard() is the name Veto uses when evaluating policies. Make sure it matches the tool names you configured in your policies.
You can use different names in your MCP server and your Veto policies if needed — just pass the policy tool name to protect() or guard(), not necessarily the MCP tool name.