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

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

Architecture

 Editor / IDE  <--stdio-->  Attesta MCPProxy  <--stdio-->  Real MCP Server
                                  |
                                  +-- risk scoring per tool call
                                  +-- domain-aware evaluation (custom profiles)
                                  +-- policy enforcement (approve / deny / audit)
                                  +-- tamper-proof audit trail
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

attesta_tool_handler(attesta, *, risk_overrides=None) -> Callable
ParameterTypeDescription
attestaAttestaA configured Attesta instance
risk_overridesdict[str, str] | NoneOptional {tool_name: risk_level} mapping

Full Example

from mcp.server import Server
from mcp.types import TextContent
from attesta import Attesta
from attesta.integrations.mcp import attesta_tool_handler

server = Server("my-devops-server")
attesta = Attesta.from_config("attesta.yaml")


@server.list_tools()
async def list_tools():
    return [
        {
            "name": "run_bash",
            "description": "Execute a bash command",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "command": {"type": "string"},
                },
                "required": ["command"],
            },
        },
        {
            "name": "read_file",
            "description": "Read file contents",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                },
                "required": ["path"],
            },
        },
    ]


@server.call_tool()
@attesta_tool_handler(
    attesta,
    risk_overrides={"run_bash": "critical"},
)
async def call_tool(name: str, arguments: dict):
    """Only executes if Attesta approves the tool call."""
    if name == "run_bash":
        import subprocess
        result = subprocess.run(
            arguments["command"], shell=True, capture_output=True, text=True,
        )
        return [TextContent(type="text", text=result.stdout)]

    if name == "read_file":
        with open(arguments["path"]) as f:
            return [TextContent(type="text", text=f.read())]

    return [TextContent(type="text", text=f"Unknown tool: {name}")]

Behavior on Denial

When a tool call is denied, the decorator returns an MCP-compatible TextContent error instead of calling the handler:
[TextContent(
    type="text",
    text="Action denied by Attesta: run_bash (risk: critical, score: 0.92)",
)]
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

MCPProxy(attesta, upstream_command, *, risk_overrides=None)
ParameterTypeDescription
attestaAttestaA configured Attesta instance
upstream_commandlist[str]Command to start the upstream MCP server
risk_overridesdict[str, str] | NoneOptional {tool_name: risk_level} mapping
Methods:
MethodDescription
run()Start the proxy. Blocks until the upstream server exits or the client disconnects.

CLI Usage

The simplest way to use the proxy is through the attesta mcp wrap CLI command:
# Wrap the filesystem MCP server
attesta mcp wrap -- npx @modelcontextprotocol/server-filesystem /home/user/projects

# Wrap a custom MCP server
attesta mcp wrap -- python my_mcp_server.py

# Wrap with a custom config file
attesta mcp wrap --config attesta.yaml -- npx @modelcontextprotocol/server-github

Editor Configuration

Configure your editor to use the proxy instead of calling the MCP server directly:
{
  "mcp.servers": {
    "filesystem": {
      "command": "attesta",
      "args": [
        "mcp", "wrap", "--",
        "npx", "@modelcontextprotocol/server-filesystem", "/home/user/projects"
      ]
    }
  }
}

Programmatic Usage

from attesta import Attesta
from attesta.integrations.mcp import MCPProxy

attesta = Attesta.from_config("attesta.yaml")

proxy = MCPProxy(
    attesta,
    upstream_command=["npx", "@modelcontextprotocol/server-filesystem", "/path"],
    risk_overrides={
        "write_file": "high",
        "delete_file": "critical",
    },
)

# Blocks until the upstream server exits
proxy.run()

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:
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Action denied by Attesta: run_bash (risk: critical, score: 0.92). This action requires human approval that was not granted."
      }
    ],
    "isError": true
  }
}

Protocol Support

The proxy auto-detects the framing format used by the MCP client and server:
FormatDescriptionUsed By
Content-Length framingContent-Length: N\r\n\r\n{...} (official MCP/LSP spec)Most MCP servers
Newline-delimited JSONOne JSON object per lineSome MCP implementations
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):
[attesta] Attesta MCP proxy started, wrapping: npx @modelcontextprotocol/server-filesystem /path
[attesta]   [approved] read_file (risk: low, score: 0.15)
[attesta]   [DENIED]   write_file (risk: high, score: 0.72)
[attesta]   [DENIED]   run_bash (risk: critical, score: 0.95)
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