Skip to main content

AG-UI Claude Adapter

A transport-agnostic protocol adapter bridging AG-UI (Agent-User Interaction Protocol) ↔ Claude Agent SDK (Python, drives the claude CLI as a subprocess).

frontend (AG-UI events)  ⇄  Adapter (this package)  ⇄  ClaudeSDKClient  ⇄  claude CLI

Design principles

  • Stateless. The Adapter holds no cross-run mutable state — no client cache, no session manager, no generated IDs. Each process_run() call is self-contained.
  • No opinion on session identity. thread_id / run_id are taken verbatim from the caller; resume/session behavior is the caller's decision via sdk_options.
  • Converters are standalone, first-class citizens. MessageConverter (AG-UI → SDK) and EventBuilder (SDK → AG-UI) can be used independently.
  • Transport-agnostic tool bridge. Frontend-defined tools are bridged through an in-process MCP server — no dependency on any web framework.

Install

pip install agui-claude-adapter            # core converters + adapter
pip install agui-claude-adapter[tools]     # + frontend-tool bridge (mcp)

Prerequisite: claude CLI on PATH (npm install -g @anthropic-ai/claude-code).


Usage paths

This package gives you three levels of control. Pick whichever matches your architecture.

Path 1 — Standalone converters (no Adapter needed)

You own the ClaudeSDKClient lifecycle. Call the two converters directly wherever you need them.

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from agui_claude_adapter import MessageConverter, EventBuilder

async def my_agent_turn(run_input):
    client = ClaudeSDKClient(options=my_options)
    await client.connect()
    try:
        # 1. AG-UI messages → SDK format
        sdk_messages = MessageConverter.agui_to_sdk(run_input.messages)
        await client.query(sdk_messages[0]["content"])

        # 2. SDK output → AG-UI events
        builder = EventBuilder()
        async for event in builder.process(
            client.receive_response(),
            thread_id=run_input.thread_id,
            run_id=run_input.run_id,
        ):
            yield event   # each is an ag_ui.core.Event subclass
    finally:
        await client.disconnect()

Use this path when:

  • You already manage your own ClaudeSDKClient pool / per-thread workers.
  • You need fine-grained control over connect / query / disconnect (e.g. multi-turn with a persistent subprocess — see the demo for a worked example).
  • You want minimal overhead — no extra layers between you and the SDK.

Path 2 — Adapter.process_run() (batteries-included)

The Adapter is a stateless convenience: it creates a fresh ClaudeSDKClient per call, connects, sends, streams, and disconnects — all for you.

from agui_claude_adapter import Adapter

adapter = Adapter()
async for event in adapter.process_run(run_input):
    print(event.type, event)

You can pass per-run sdk_options (resume, model, system prompt, …) — the adapter forwards them unchanged:

from claude_agent_sdk import ClaudeAgentOptions

opts = ClaudeAgentOptions(
    include_partial_messages=True,
    max_turns=10,
    permission_mode="bypassPermissions",
    resume="cli-session-id-from-last-turn",   # business-layer decision
)
async for event in adapter.process_run(run_input, sdk_options=opts):
    ...

Use this path when:

  • You want a single-turn, fire-and-forget agent call.
  • You don't need persistent subprocesses across turns (each process_run is isolated).

Path 3 — Business-layer orchestration (demo's pattern)

For multi-turn conversations with memory, you keep a long-lived ClaudeSDKClient (one CLI subprocess per thread) and drive the converters against it directly — just like Path 1, but with a persistent client.

The demo (demo/server.py) implements this with a _ThreadWorker per thread: the worker owns one ClaudeSDKClient inside a dedicated asyncio.Task, request tasks submit messages via a queue, and the two standalone converters do the rest. This pattern keeps the core adapter stateless while the business layer owns session identity and client lifecycle.

See demo/server.py for the full implementation — it is extensively commented and serves as the reference for this pattern.


Working with multimodal messages (images, documents)

AG-UI represents multimodal content as an array of content parts. The converter maps them to Anthropic's native format automatically.

Frontend to AG-UI UserMessage

The frontend sends an AG-UI content-part array:

{
  "message": [
    {"type": "text",  "text": "Look at this image"},
    {"type": "image", "source": {"type": "data", "value": "<base64>", "mimeType": "image/png"}}
  ]
}

What the converter does

MessageConverter.agui_to_sdk maps each part:

AG-UI input Anthropic output
{"type":"text","text":"..."} {"type":"text","text":"..."}
{"type":"image","source":{"type":"data","value":"...","mimeType":"image/png"}} {"type":"image","source":{"type":"base64","media_type":"image/png","data":"..."}}
{"type":"document","source":...} Same mapping (data→base64, mimeType→media_type, value→data)

The SDK routes the converted content to the CLI, which delivers it to the model. Text-only paths are unchanged.

Single vs multiple messages

  • One multimodal message: goes through the async-iterable path (the string fast-path is guarded so lists don't cause TypeError).
  • Multiple messages (full history): already uses the async-iterable path — multimodal content works the same way.

Frontend-defined tools

When the model calls a tool that the frontend executes (not a built-in CLI tool), you need a bridge between the CLI's MCP handler and the frontend's result:

CLI calls tool  →  MCP call_tool  →  bridge.wait_for(name, args)  →  BLOCK
Frontend runs   →  POST /tool_result                            →  bridge.resolve(name, args, result)
                                                                     UNBLOCK → return to CLI

Using Adapter.process_run with tools

async for event in adapter.process_run(
    run_input,
    tools=run_input.tools,  # list of ag_ui.core.Tool
):
    ...

When tools is provided the adapter wires up an in-process MCP server (FrontendToolBridge + FrontendToolRegistry + build_frontend_tool_mcp_server) internally and tears it down at run end.

Using the tool bridge directly (Path 1 / Path 3)

from agui_claude_adapter import (
    FrontendToolBridge,
    FrontendToolRegistry,
    build_frontend_tool_mcp_server,
)

bridge = FrontendToolBridge()
registry = FrontendToolRegistry(agui_tools)
mcp_server = build_frontend_tool_mcp_server(bridge, registry)

sdk_options.mcp_servers = {
    "frontend-tools": {
        "type": "sdk",
        "name": "frontend-tools",
        "instance": mcp_server,
    }
}

When the frontend returns a tool result, deliver it through the bridge:

# In your POST /api/tool_result handler (or WebSocket, or …)
matched = bridge.resolve(tool_name, arguments, result)
# matched == True  → a waiting call_tool handler was unblocked
# matched == False → result buffered (arrived before call_tool), or stale POST

Important: The MCP call_tool handler never learns the Anthropic tool_use_id, so the bridge correlates by a (tool_name, arguments) fingerprint instead. The frontend must send tool_name + arguments (not just tool_call_id) when reporting a result. Both sides canonicalise with json.dumps(sort_keys=True) inside Python, so key order and JS/Python serialization differences don't cause mismatches.


Exception handling

Adapter.process_run classifies exceptions into typed AdapterError subclasses and emits a RunErrorEvent rather than raising:

Exception Error code
AdapterConnectionError CONNECTION_ERROR
AdapterAuthError AUTH_ERROR
AdapterTimeoutError TIMEOUT_ERROR
AdapterRateLimitError RATE_LIMIT_ERROR
AdapterInvalidRequestError INVALID_REQUEST
AdapterServerError SERVER_ERROR
Other / unknown UNKNOWN_ERROR

When using the standalone converters (Path 1), you handle SDK exceptions yourself — the converters only do format translation.


Package reference

Component Import Description
MessageConverter agui_claude_adapter Static methods: agui_to_sdk(messages) → list[dict], sdk_to_agui_message(sdk_msg) → AssistantMessage
EventBuilder agui_claude_adapter process(sdk_stream, *, thread_id, run_id) → AsyncIterator[AGUIEvent]
Adapter agui_claude_adapter process_run(input, *, sdk_options=None, tools=None) → AsyncIterator[AGUIEvent]
FrontendToolBridge agui_claude_adapter wait_for(name, args, *, timeout), resolve(name, args, result) → bool, cancel_all()
FrontendToolRegistry agui_claude_adapter replace(tools), snapshot(), names()
build_frontend_tool_mcp_server agui_claude_adapter (bridge, registry, *, server_name, call_timeout) → McpServer
ToolConverter agui_claude_adapter AG-UI Tool ↔ OpenAI/Anthropic schema conversion utilities

Run the demo

# Backend (requires claude CLI)
uv run python demo/server.py       # → http://127.0.0.1:8000

# Frontend
cd demo/frontend && npm run dev    # → http://localhost:5173 (proxies /api → :8000)

The demo is a full-stack reference: multi-turn chat (persistent _ThreadWorker per thread), file upload with image → Anthropic conversion, and generative-UI tools (setBackgroundColor, showChoiceCard, flightBookingWizard).


Development

uv run pytest tests/                          # full suite (102 tests)
uv run pytest tests/test_adapter.py -v        # single file

asyncio_mode = "auto" is set — no @pytest.mark.asyncio needed on async tests.


Documentation

Document Purpose
docs/KNOWN_ISSUES.md Prioritized defect log (all core-adapter issues fixed as of 2026-07-09)
docs/stateless-adapter-plan.md Design rationale for the stateless refactor
docs/tool-bridge-core-plan.md Transport-agnostic tool-bridge architecture
docs/python-packaging-guide.md Step-by-step Python packaging & publishing tutorial

Metadata

Release files for agui-claude-adapter 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agui-claude-adapter 0.1.1
File Size Uploaded Provenance
agui_claude_adapter-0.1.1.tar.gz 39.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agui-claude-adapter 0.1.1
File Interpreter ABI Platform
agui_claude_adapter-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 63.1 kB

Release files / agui_claude_adapter-0.1.1.tar.gz

Download URL agui_claude_adapter-0.1.1.tar.gz
Size 39.3 kB
Tags Source
SHA-256 checksum
How to use checksums
a87d9c9692efb11748df3020238857bfec47d9cff8d6b676e46f41c0efda19a1
BLAKE2b-256 checksum
How to use checksums
4d4d8d8338c7d6db4501c865c72add481e12b3231dfe9066553736d42e045e28
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release files / agui_claude_adapter-0.1.1-py3-none-any.whl

Download URL agui_claude_adapter-0.1.1-py3-none-any.whl
Size 23.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dd71c89f620bfdd4331dd5541c30e0a0dba71434ab8b4f1d58aded19721b1ae8
BLAKE2b-256 checksum
How to use checksums
27e552d0cb88ecb715eef0b06ae1b3cae5c87a855acea9072446d523478d1462
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Security for agui-claude-adapter 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page