mcp.ClientSession so every call_tool() is gated by the governance evaluator and produces an audit event. Denied calls come back as a normal CallToolResult(isError=True) — your harness doesn’t need any special-casing.
Install
[mcp] extra pulls mcp as a dependency. If you already have it installed, pip install rubric-app is enough.
Basic usage
ClientSession — list_tools, call_tool, etc. — so it’s a drop-in replacement.
What gets evaluated
Everywrapped.call_tool(name, args) triggers gov.evaluate(name, session_id=…, metadata=EvaluationMetadata(input=args)). The tool_name field in your policies matches the MCP tool name as the server advertised it.
tool_name= the MCP tool name (e.g.list_files,delete_file).input.<key>= entries in theargsdict (e.g.input.path).framework="mcp"on the audit event.
Denied calls
When a policy denies the call:evaluate()returnsdecision="deny"with the rule’s reason.- The adapter constructs a
CallToolResult(isError=True, content=[TextContent(...)])containing the deny reason. - The actual MCP server is never called.
- The audit event ships with
decision="deny"and the policy id.
With traces
Pass aTraceContext and it’s used for every governed call in this session:
Custom session id strategy
session_id is whatever string makes sense for your harness. Two common choices:
- Per-conversation. Use the user’s chat thread id — every audit event for that conversation groups under one id.
- Per-process. Use
os.getpid()or a uuid generated at startup — useful for batch jobs.
Full example
A working end-to-end script ships with the SDK atexamples/mcp_quickstart.py. It spawns an in-tree FastMCP server, governs the client, and runs an allow and a deny call against the bundle.