Skip to main content
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.
createVetoGuardvetoMiddleware
Denial behaviorReturns MCP error response (isError: true)Throws VetoError
BoilerplateLess — wraps the handlerMore — called inside the handler
RecommendedYesWhen you need custom denial logic

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

agentId
string
required
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 });
  },
});

Matching tool names to policies

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.