Skip to main content
Attesta provides two patterns for enforcing human-in-the-loop approval on MCP tool invocations, regardless of which client (VS Code, Cursor, Claude Code, Windsurf, etc.) calls the tools.
  1. attesta_tool_handler — a decorator for MCP call_tool handlers. Use this when you author your own MCP servers in Python.
  2. MCPProxy — a stdio proxy that wraps any existing MCP server with Attesta approval, requiring zero code changes to the upstream server.

Installation

MCP support is included in the core package — no additional extras required.

Architecture

The proxy sits transparently between the MCP client and server. It intercepts tools/call JSON-RPC requests, evaluates them through Attesta, and either forwards the request to the upstream server (approved) or returns an error response directly (denied). All other messages (tool listings, notifications, etc.) pass through unchanged.

Pattern 1: Decorator (Custom MCP Servers)

Use attesta_tool_handler when you are writing your own MCP server in Python. Place it between @server.call_tool() and your handler function.

API

Full Example

Behavior on Denial

When a tool call is denied, the decorator returns an MCP-compatible TextContent error instead of calling the handler:
The MCP client (editor/IDE) displays this message to the user. The handler function is never executed.
The decorator sets metadata={"source": "mcp"} on all ActionContext objects. This allows you to write audit queries that filter for MCP-specific tool calls.

Pattern 2: MCPProxy (Zero Code Changes)

MCPProxy wraps any existing MCP server with Attesta approval. No modifications to the upstream server are needed. This is the recommended approach for enforcing organization-wide HITL policies across all MCP tools.

API

Methods:

CLI Usage

The simplest way to use the proxy is through the attesta mcp wrap CLI command:

Editor Configuration

Configure your editor to use the proxy instead of calling the MCP server directly:

Programmatic Usage


How the Proxy Works

1

Startup

The proxy spawns the upstream MCP server as a child process via subprocess.Popen, connected by stdin/stdout pipes. The upstream server’s stderr passes through to the terminal for debugging.
2

Request Interception

The proxy reads JSON-RPC messages from its own stdin (the MCP client). When it sees a tools/call request, it extracts the tool name and arguments for evaluation.
3

Attesta Evaluation

The tool call is evaluated through attesta.evaluate() with an ActionContext containing:
  • function_name: the tool name from the request
  • kwargs: the tool arguments
  • metadata: {"source": "mcp_proxy"}
4

Forwarding or Denial

  • Approved: the original request is forwarded to the upstream server’s stdin. The response flows back through the proxy to the client.
  • Denied: the proxy generates a JSON-RPC error response directly. The request never reaches the upstream server.

Denial Response Format

Denied tool calls return a JSON-RPC response with isError: true:

Protocol Support

The proxy auto-detects the framing format used by the MCP client and server: Both formats are supported transparently. The proxy always writes responses using Content-Length framing.

Logging

The proxy logs all approval and denial decisions to stderr (visible in the terminal, not sent to the MCP client):
The proxy runs attesta.evaluate() synchronously using asyncio.run() for each intercepted tool call. This means tool calls are evaluated one at a time, which adds latency. For most MCP use cases (editor-driven tool calls), this is acceptable because humans are in the loop anyway.

CLI Reference: mcp wrap

Full CLI options for attesta mcp wrap

Vercel AI SDK

TypeScript tool wrappers and middleware