Skip to main content

MCP

The MCP (Model Context Protocol) adapter intercepts tool calls on an MCP server, records each invocation through a ProofRail chain, and only forwards the call to the underlying handler when the policy engine allows it. If you’re building MCP servers exposing tools to Claude Desktop, Cursor, or any other MCP client, this is how you add governance without changing your tool code.

Install

pip install "proofrail[mcp]"
This installs the official Anthropic mcp Python SDK if not already present.
Supported MCP Python SDK versions: 1.0.x. The protocol is still evolving — pin the specific version range you’ve tested against.

Quick start

The simplest integration: open a chain, attach the adapter, install it on your MCP server.
import asyncio
import proofrail
from proofrail.mcp import ProofRailMcpAdapter
from mcp.server import Server

proofrail.init(api_key="prail_...")

server = Server("my-tools")

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict):
    if name == "query_database":
        return await query_database(arguments)
    if name == "send_email":
        return await send_email(arguments)
    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with proofrail.Chain("mcp-session") as chain:
        adapter = ProofRailMcpAdapter(chain=chain, agent_name="my-tools")
        adapter.install(server)
        await server.run(...)

asyncio.run(main())
After adapter.install(server), every tool call passes through ProofRail’s policy engine. Denied calls return an MCP-compatible error to the client. Allowed calls execute normally.

Three usage patterns

The MCP adapter supports three integration styles depending on how your server is structured.

Pattern 1: install() — wrap an existing server

If you already have a Server instance with @server.call_tool() registered, install() patches the call handler in place. Your existing code keeps working; you just gain governance.
async with proofrail.Chain("mcp-session") as chain:
    adapter = ProofRailMcpAdapter(chain=chain, agent_name="my-tools")
    adapter.install(server)  # patches server._call_tool_handler
    await server.run(...)
This is the recommended approach for most cases.

Pattern 2: handle_tool_call() — manual interception

If you don’t want to patch the server, you can route calls through the adapter manually:
async def my_handler(tool_name: str, arguments: dict):
    if tool_name == "query_database":
        return await query_database(arguments)
    # ...

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    return await adapter.handle_tool_call(name, arguments, my_handler)
This gives you full control over the dispatching logic while still recording every call.

Pattern 3: @adapter.tool() decorator — per-tool wrapping

For more granular control, decorate individual tool implementations:
@adapter.tool("query_database")
async def query_database(name: str, arguments: dict):
    return {"rows": await db.fetch(arguments["sql"])}

@adapter.tool("send_email")
async def send_email(name: str, arguments: dict):
    return await mailer.send(arguments)
Each decorated function records a governance event before executing. Use this when you want explicit governance scope per tool rather than blanket coverage.

What gets recorded

For each tool invocation:
  • A tool_call event with the tool name as action_name and the arguments as the payload
  • agent_name is the value you passed to ProofRailMcpAdapter
  • parent_agent_name if you supplied one (useful for tracking MCP client identity)
The MCP session corresponds to a ProofRail chain. Each tool call within the session is one event in that chain.

Policy enforcement

When a policy denies a tool call:
from proofrail.exceptions import ActionDeniedError

try:
    result = await adapter.handle_tool_call(name, arguments, handler)
except ActionDeniedError as exc:
    # The MCP server should return an error to the client
    return {"error": str(exc)}
If you’re using install(), the adapter handles error conversion automatically — denied calls return an MCP-compatible error response without raising in your server code. For approval-required decisions, handle_tool_call blocks until the approver responds. The MCP client sees a delayed response (until approval times out, your client may consider the call hung — set reasonable client timeouts).

Identifying the MCP client

If your MCP server knows which client is connected, pass that identity as parent_agent_name:
adapter = ProofRailMcpAdapter(
    chain=chain,
    agent_name="my-tools",
    parent_agent_name="claude-desktop",  # or "cursor", "custom-client", etc.
)
This appears in the audit trail and helps with cross-organization tracking when multiple clients connect to the same server.

Session lifetime

A ProofRail chain corresponds to an MCP session. The natural pattern:
# One chain per MCP server lifetime
async def main():
    async with proofrail.Chain("server-lifetime") as chain:
        adapter = ProofRailMcpAdapter(chain=chain, agent_name="my-tools")
        adapter.install(server)
        await server.run(...)  # runs until server exits
For long-running servers with many sessions, you might want a chain per session instead. This requires more custom wiring — see the SDK’s proofrail/mcp/adapter.py for the underlying primitives.

Edge cases

Server doesn’t have _call_tool_handler registered yet. install() raises RuntimeError. Register at least one @server.call_tool() before calling install, or use Pattern 2 / Pattern 3 instead. Multiple tools registered. install() wraps the single dispatch function MCP uses internally. All tools are covered. Tool with no arguments. Works fine — arguments={} is a valid payload. Streaming tool responses. MCP tools that stream responses are tracked at the request level only. The response stream is forwarded to the client unchanged; ProofRail doesn’t intercept individual stream chunks. Tool errors. If the underlying handler raises, the exception propagates after the governance event is recorded. The event records the call as “attempted” — the error itself is part of the MCP response.

Where to go next

LangGraph adapter

For LangGraph-orchestrated agent workflows.

LangChain adapter

For LangChain agent executors and chains.

Policies

What decisions get applied to your tool calls.

SDK reference

The complete ProofRailMcpAdapter API.