> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arclasp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP

> Govern MCP server tool calls before they execute

# MCP

The MCP (Model Context Protocol) adapter intercepts tool calls on an MCP server, records each invocation through an Arclasp 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

```bash theme={null}
pip install "arclasp[mcp]"
```

This installs the official Anthropic `mcp` Python SDK if not already present.

<Note>
  Supported MCP Python SDK versions: **1.0.x**. The protocol is still evolving — pin the specific version range you've tested against.
</Note>

## Quick start

The `mcp` SDK (>= 1.0) no longer exposes a patchable call-handler attribute, so there's no server-patching shortcut. Wire governance directly inside your `@server.call_tool()` handler by calling `handle_tool_call()`:

```python theme={null}
import asyncio
import arclasp
from arclasp.mcp import ArclaspMcpAdapter
from mcp.server import Server

arclasp.init(api_key="prail_...", backend_url="https://api.proofrail.dev")

server = Server("my-tools")

async def dispatch(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 arclasp.Chain("mcp-session") as chain:
        adapter = ArclaspMcpAdapter(chain=chain, agent_name="my-tools")

        @server.call_tool()
        async def handle_call_tool(name: str, arguments: dict):
            return await adapter.handle_tool_call(
                tool_name=name,
                arguments=arguments,
                handler=dispatch,
            )

        # `read_stream`, `write_stream`, and `init_options` come from your
        # chosen MCP transport (stdio, SSE, etc.) — see the `mcp` SDK's own
        # transport docs for how to construct them. This isn't a turnkey
        # value; it depends on how you're serving the MCP connection.
        await server.run(read_stream, write_stream, init_options)

asyncio.run(main())
```

Every tool call passed through `handle_tool_call()` goes through Arclasp's policy engine before `dispatch` runs. Denied calls raise `ActionDeniedError` in your handler — catch it and return an MCP-compatible error to the client.

MCP integration records the governed invocation and policy decision before tool execution. Tool return values and execution exceptions are not persisted as MCP action evidence in this release.

## Two usage patterns

The MCP adapter supports two integration styles depending on how much control you want over dispatching.

### Pattern 1: `handle_tool_call()` — wrap your existing dispatch

Route calls through the adapter from inside your `@server.call_tool()` handler, as shown above. This gives you full control over the dispatching logic while recording every call. This is the recommended approach for most cases.

<Note>
  `ArclaspMcpAdapter.install()` exists on the class but raises `RuntimeError` — it is **not supported** with `mcp >= 1.0`, which removed the patchable `_call_tool_handler` attribute the old implementation relied on. Do not call it; use `handle_tool_call()` inside your own handler instead.
</Note>

### Pattern 2: `@adapter.tool()` decorator — per-tool wrapping

For more granular control, decorate individual tool implementations:

```python theme={null}
@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 `ArclaspMcpAdapter`
* `parent_agent_name` if you supplied one (useful for tracking MCP client identity)

The MCP session corresponds to an Arclasp chain. Each tool call within the session is one event in that chain.

## Policy enforcement

When a policy denies a tool call:

```python theme={null}
from arclasp.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)}
```

Catch `ActionDeniedError` inside your `@server.call_tool()` handler (as shown above) and convert it to whatever error shape your MCP transport expects — the adapter itself does not do this conversion for you.

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`:

```python theme={null}
adapter = ArclaspMcpAdapter(
    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

An Arclasp chain corresponds to an MCP session. The natural pattern:

```python theme={null}
# One chain per MCP server lifetime
async def main():
    async with arclasp.Chain("server-lifetime") as chain:
        adapter = ArclaspMcpAdapter(chain=chain, agent_name="my-tools")

        @server.call_tool()
        async def handle_call_tool(name: str, arguments: dict):
            return await adapter.handle_tool_call(
                tool_name=name, arguments=arguments, handler=dispatch,
            )

        await server.run(read_stream, write_stream, init_options)  # 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 `arclasp/mcp/adapter.py` for the underlying primitives.

## Edge cases

**Calling `install()`.** It exists on `ArclaspMcpAdapter` for backward compatibility but always raises `RuntimeError` — it is not supported with `mcp >= 1.0`. Use `handle_tool_call()` inside your own `@server.call_tool()` handler instead.

**Multiple tools registered.** Since dispatching happens in your own handler, every tool your handler routes through `handle_tool_call()` is covered — there's no separate registration step with the adapter.

**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; Arclasp 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 governed invocation — the error itself is part of the MCP response.

## Where to go next

<CardGroup cols={2}>
  <Card title="LangGraph adapter" icon="diagram-project" href="/frameworks/langgraph">
    For LangGraph-orchestrated agent workflows.
  </Card>

  <Card title="LangChain adapter" icon="link" href="/frameworks/langchain">
    For LangChain agent executors and chains.
  </Card>

  <Card title="Policies" icon="shield" href="/concepts/policies">
    What decisions get applied to your tool calls.
  </Card>

  <Card title="SDK reference" icon="code" href="/reference/sdk-api">
    The complete `ArclaspMcpAdapter` API.
  </Card>
</CardGroup>
