MCP Auth
Bulwark is the authorization server for Model Context Protocol (MCP) servers. When an agent calls an MCP tool, Bulwark verifies the agent's identity, confirms it holds the required scopes, and provides the MCP server with a verified context it can trust.
The Problem with Unauthenticated MCP
MCP servers expose tools that can read files, call APIs, execute code, and interact with databases. Without authentication:
- Any process can call any tool
- There is no audit trail of which agent used which tool
- Tools cannot enforce per-agent permission boundaries
- Credential injection into tools is unsafe
Bulwark brings the same identity and authorization model it applies to API calls to MCP tool invocations.
Architecture
Agent (with Biscuit token)
↓ MCP request + X-Bulwark-Token header
MCP Server (@bulwarkauth/mcp-auth middleware)
↓ POST /api/v1/mcp/introspect
Bulwark Authorization Server
↓ { authorized: true, agent_id, scopes, session_id }
MCP Server executes tool with verified context
Bulwark acts as a Protected Resource authorization server per RFC 9728. MCP servers discover Bulwark's introspection endpoint via the Protected Resource Metadata document served at /api/v1/mcp/servers/{id}/metadata.
Biscuit Token Scoping at the Tool Level
Bulwark issues Biscuit tokens that can be attenuated to specific tools. An agent calling an MCP server presents a token scoped to only the tools it intends to use:
// Authority block (Bulwark)
right("files:read");
right("files:write");
valid_agent("agent_01j");
// Attenuation block (agent, scoped to one tool)
check if right("files:read");
check if tool($t), $t == "read_file";
The MCP server introspects the token. If the agent attempts to call write_file with this token, the introspection returns authorized: false because the files:write scope is present in the authority block but the attenuated caveat restricts to read_file only.
This is the core advantage of Biscuit over JWT for tool-level authorization: the same token can be attenuated without back-channel revocation.
Tool Registration and Scope Mapping
When you register an MCP server with Bulwark, you declare each tool and the scopes it requires:
{
"tools": [
{ "name": "read_file", "scopes_required": ["files:read"] },
{ "name": "write_file", "scopes_required": ["files:write"] },
{ "name": "delete_file", "scopes_required": ["files:delete"] }
]
}
Bulwark uses this mapping during introspection to verify that the presented token covers the scope for the requested tool.
@bulwarkauth/mcp-auth SDK
The @bulwarkauth/mcp-auth package wraps your MCP server with Bulwark authentication in a few lines:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { withBulwark } from "@bulwarkauth/mcp-auth";
const server = new Server({ name: "my-server", version: "1.0.0" });
withBulwark(server, {
tenantId: process.env.BULWARK_TENANT_ID!,
apiKey: process.env.BULWARK_API_KEY!,
serverId: process.env.BULWARK_MCP_SERVER_ID!,
});
Every tool handler receives a BulwarkMcpContext with the verified agent identity and an active session object for credential injection:
server.setRequestHandler(CallToolRequestSchema, async (req, context) => {
const { agent, session } = context as BulwarkMcpContext;
// agent.agentId, agent.trustLevel, agent.scopes are all verified
// session.proxy() injects credentials without exposing them to the LLM
});
Credential Injection in Tools
MCP tools commonly need to call external APIs. Rather than embedding credentials in tool arguments (which would expose them in LLM context), tools use session.proxy():
const result = await session.proxy({
credentialId: "cred_github",
method: "GET",
url: `https://api.github.com/repos/${owner}/${repo}`,
});
Bulwark decrypts the stored credential, injects it as an Authorization header, proxies the request, and returns the response. The agent — and the LLM — never see the token.
Human Approval for Sensitive Tools
Tools that perform destructive or high-value actions can require out-of-band approval using CIBA before executing:
import { requireHumanApproval } from "@bulwarkauth/mcp-auth";
const approval = await requireHumanApproval(session, {
userId: agent.ownerId,
scope: "approve:file:delete",
bindingMessage: `Agent wants to delete ${req.params.arguments.path}`,
timeout: 120,
});
if (!approval.granted) {
return { content: [{ type: "text", text: "Action denied by user." }] };
}
Protected Resource Metadata Discovery
MCP clients that implement RFC 9728 will discover Bulwark automatically by fetching the metadata document from your MCP server. Bulwark serves this document at:
GET /api/v1/mcp/servers/{id}/metadata
No manual configuration in the MCP client is required.