Claude Code ACP (Python)
Python implementation of ACP (Agent Client Protocol) for Claude Code.
This package combines the Claude Agent SDK with the Agent Client Protocol (ACP) to provide a complete Python solution for working with AI agents.
What It Does
| Component | Type | Description |
|---|---|---|
ClaudeAcpAgent |
ACP Server | Lets editors (Zed, Neovim, JetBrains) use Claude |
ClaudeClient |
Python API | Event-driven wrapper for building Python apps with Claude |
AcpClient |
ACP Client | Connect to any ACP-compatible agent (Claude, Gemini, custom) |
AcpProxyServer |
Copilot Proxy | Bridge Copilot SDK to any ACP backend |
Two CLI commands:
claude-code-acp- ACP server for editorscopilot-acp-proxy- Copilot SDK proxy to ACP backends
Features
- Uses Claude CLI subscription - No API key needed
- Full ACP protocol - Compatible with Zed, Neovim, and other ACP clients
- Universal ACP client - Connect to any ACP agent (Claude, Gemini, or custom)
- Copilot SDK bridge - Use Copilot SDK with any ACP backend
- Bidirectional communication - Permission requests, tool calls, streaming
- Event-driven Python API - Decorator-based handlers
- Session management - Create, fork, resume, list sessions
- MCP server support - Dynamic loading via stdio, HTTP, SSE
- Model & command enumeration - Discover available models and commands at runtime
Installation
pip install claude-code-acp
Or with uv:
uv tool install claude-code-acp
Requirements
- Python 3.10+
- Claude CLI installed and authenticated (
claude /login)
Usage 1: ACP Server for Editors
Run as an ACP server so editors can use Claude:
claude-code-acp
Zed Editor
Add to your Zed settings.json:
{
"agent_servers": {
"Claude Code Python": {
"type": "custom",
"command": "claude-code-acp",
"args": [],
"env": {}
}
}
}
Then open the Agent Panel (Ctrl+? / Cmd+?) and select "Claude Code Python" from the + menu.
Other Editors
Any ACP-compatible client can connect by spawning claude-code-acp as a subprocess and communicating via stdio.
Usage 2: Python Event-Driven API
Use ClaudeClient for building Python applications with Claude:
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient(cwd=".", system_prompt="You are a helpful assistant.")
@client.on_text
async def handle_text(text: str):
print(text, end="", flush=True)
@client.on_tool_start
async def handle_tool_start(tool_id: str, name: str, input: dict):
print(f"\n[Tool] {name}")
@client.on_tool_end
async def handle_tool_end(tool_id: str, status: str, output):
icon = "ok" if status == "completed" else "fail"
print(f" [{icon}]")
@client.on_permission
async def handle_permission(name: str, input: dict) -> bool:
print(f"Permission requested: {name}")
return True # or prompt user
@client.on_complete
async def handle_complete():
print("\n--- Done ---")
async with client:
response = await client.query("Create a hello.py file that prints Hello World")
print(f"\nFull response: {response}")
asyncio.run(main())
Event Handlers
| Decorator | Arguments | Description |
|---|---|---|
@client.on_text |
(text: str) |
Streaming text chunks |
@client.on_thinking |
(text: str) |
Thinking/reasoning blocks |
@client.on_tool_start |
(tool_id, name, input) |
Tool execution started |
@client.on_tool_end |
(tool_id, status, output) |
Tool execution completed |
@client.on_permission |
(name, input) -> bool |
Permission request (return True/False) |
@client.on_error |
(exception) |
Error occurred |
@client.on_complete |
() |
Query completed |
Client Methods
# Init with optional MCP servers and system prompt
client = ClaudeClient(cwd=".", mcp_servers=[...], system_prompt="...")
# Recommended: use async context manager for automatic cleanup
async with client:
response = await client.query("Your prompt here")
# Or manage lifecycle manually
session_id = await client.start_session()
response = await client.query("Your prompt here")
await client.close() # Clean up subprocess connections
# Set permission mode
await client.set_mode("acceptEdits") # or "default", "plan", "bypassPermissions"
Usage 3: ACP Client (Connect to Any Agent)
Since this package implements a full ACP client, you can use AcpClient to connect to any ACP-compatible agent - not just Claude.
import asyncio
from claude_code_acp import AcpClient
async def main():
# Connect to any ACP agent
client = AcpClient(command="claude-code-acp")
@client.on_text
async def handle_text(text: str):
print(text, end="", flush=True)
@client.on_tool_start
async def handle_tool(tool_id: str, name: str, input: dict):
print(f"\n[Tool] {name}")
@client.on_permission
async def handle_permission(name: str, input: dict, options: list) -> str:
"""Return option_id: 'allow', 'reject', or 'allow_always'"""
print(f"Permission: {name}")
return "allow"
@client.on_complete
async def handle_complete():
print("\n--- Done ---")
async with client:
response = await client.prompt("What files are here?")
asyncio.run(main())
Connect to Different Agents
from claude_code_acp import AcpClient
# Claude (this package)
claude = AcpClient(command="claude-code-acp")
# Gemini CLI
gemini = AcpClient(command="gemini", args=["--experimental-acp"])
# TypeScript version
ts_claude = AcpClient(command="npx", args=["@zed-industries/claude-code-acp"])
# Any custom ACP agent
custom = AcpClient(command="my-custom-agent")
Tested Agents
| Agent | Command | Status |
|---|---|---|
| claude-code-acp (this package) | claude-code-acp |
Works |
| Gemini CLI | gemini --experimental-acp |
Works |
| TypeScript version | npx @zed-industries/claude-code-acp |
Compatible |
File Operation Handlers
Intercept file read/write operations for security or custom handling:
@client.on_file_read
async def handle_read(path: str) -> str | None:
"""Return content to override, or None to proceed normally."""
print(f"Reading: {path}")
return None
@client.on_file_write
async def handle_write(path: str, content: str) -> bool:
"""Return True to allow, False to block."""
print(f"Writing: {path}")
return input("Allow? [y/N]: ").lower() == "y"
Terminal Operation Handlers
Intercept shell execution for security:
@client.on_terminal_create
async def handle_terminal(command: str, cwd: str) -> bool:
"""Return True to allow, False to block."""
print(f"Command: {command} in {cwd}")
return input("Allow? [y/N]: ").lower() == "y"
@client.on_terminal_output
async def handle_output(terminal_id: str, output: str) -> None:
print(output, end="")
Model Selection
# Set model before connecting (pending)
client = AcpClient(command="claude-code-acp")
client.set_model("opus") # Will be applied when session starts
# Or set model on active session
async with client:
await client.set_model("sonnet")
response = await client.prompt("Hello!")
AcpClient vs ClaudeClient
| Feature | ClaudeClient |
AcpClient |
|---|---|---|
| Uses | Claude Agent SDK directly | Any ACP agent via subprocess |
| Connection | In-process | Subprocess + stdio |
| Agents | Claude only | Any ACP-compatible agent |
| File/Terminal hooks | No | Yes |
| Use case | Simple Python apps | Multi-agent, testing, flexibility |
Usage 4: Copilot SDK Proxy
The copilot-acp-proxy command bridges the Copilot SDK to any ACP backend, allowing Copilot SDK applications to use Claude, Gemini, or other ACP agents.
# Connect Copilot SDK to Gemini
copilot-acp-proxy --headless --stdio --backend gemini
# Connect Copilot SDK to Claude
copilot-acp-proxy --headless --stdio --backend claude-code-acp
# Connect Copilot SDK to Copilot CLI
copilot-acp-proxy --headless --stdio --backend copilot
Proxy with Copilot SDK (Python)
import asyncio
from copilot import CopilotClient
async def main():
client = CopilotClient({"cli_path": "copilot-acp-proxy"})
await client.start()
session = await client.create_session({"model": "sonnet"})
def on_event(event):
event_type = event.type.value if hasattr(event.type, 'value') else str(event.type)
if event_type == "assistant.message_delta":
delta = getattr(event.data, 'deltaContent', None)
if delta:
print(delta, end="", flush=True)
session.on(on_event)
await session.send({"prompt": "Hello!"})
asyncio.run(main())
Supported Backends
| Backend | Command | Model Examples |
|---|---|---|
| Gemini | gemini |
gemini-2.0-flash, gemini-2.5-flash |
| Claude | claude-code-acp |
opus, sonnet |
| Copilot | copilot |
gpt-4, gpt-4o |
Environment variables:
ACP_PROXY_BACKEND- Default backend (default: gemini)ACP_PROXY_LOG_LEVEL- Log level (none/error/warning/info/debug/all)ACP_PROXY_LOG_FILE- Log file path
Gemini ACP Usage
Note: Gemini takes ~12 seconds to initialize on first connection.
import asyncio
from claude_code_acp import AcpClient
async def main():
client = AcpClient(
command="gemini",
args=["--experimental-acp"],
cwd="/tmp",
)
@client.on_text
async def on_text(text):
print(text, end="", flush=True)
@client.on_thinking
async def on_thinking(text):
print(f"[Thinking] {text[:50]}...")
async with client:
response = await client.prompt("Hello!")
print(f"\nResponse: {response}")
asyncio.run(main())
Gemini with MCP Servers
Gemini requires MCP servers to be pre-configured via CLI:
# Add MCP server to Gemini config
gemini mcp add nanobanana "uvx nanobanana"
# Verify
gemini mcp list
Then enable via --allowed-mcp-server-names:
client = AcpClient(
command="gemini",
args=["--experimental-acp", "--allowed-mcp-server-names", "nanobanana"],
cwd="/tmp",
)
MCP Configuration Comparison
| Agent | Dynamic MCP (via ACP) | Pre-configured MCP |
|---|---|---|
| claude-code-acp | Supported | Supported |
| Gemini CLI | Not supported | Use --allowed-mcp-server-names |
claude-code-acp supports dynamic MCP configuration:
client = AcpClient(
command="claude-code-acp",
cwd="/tmp",
mcp_servers=[{
"name": "nanobanana",
"command": "uvx",
"args": ["nanobanana"],
"env": {"GEMINI_API_KEY": "your-key"},
}],
)
Architecture
┌─────────────────────────────────────────┐
│ Editors / ACP Clients │
│ (Zed, Neovim, JetBrains, etc.) │
└──────────────┬──────────────────────────┘
│ ACP (stdio)
▼
┌──────────────────────────────────────────────────────────────┐
│ claude-code-acp │
│ │
│ ┌────────────────┐ ┌─────────────┐ ┌───────────────────┐ │
│ │ ClaudeAcpAgent │ │ ClaudeClient│ │ AcpClient │ │
│ │ (ACP Server) │ │ (Python API)│ │ (ACP Client) │ │
│ └───────┬────────┘ └──────┬──────┘ └─────┬─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Claude CLI Claude CLI Any ACP Agent │
│ (Agent SDK) (Agent SDK) ┌──────────────┐ │
│ │ claude-code │ │
│ ┌─────────────────────────────┐ │ gemini │ │
│ │ AcpProxyServer │ │ custom agent │ │
│ │ (copilot-acp-proxy) │ └──────────────┘ │
│ │ Copilot SDK → ACP backend │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Component Summary
| Component | Role | ACP? | Connects to |
|---|---|---|---|
ClaudeAcpAgent |
Server | Yes (Server) | Editors connect to it |
ClaudeClient |
Python API | No | Claude CLI directly |
AcpClient |
Client | Yes (Client) | Any ACP agent |
AcpProxyServer |
Proxy | Translates | Copilot SDK <-> ACP backends |
What We Built
This project integrates two official Anthropic SDKs:
| Component | Source | Purpose |
|---|---|---|
| Agent Client Protocol SDK | Anthropic | ACP server/client protocol |
| Claude Agent SDK | Anthropic | Claude CLI wrapper with streaming |
Our Contributions
-
ClaudeAcpAgent (
agent.py) - ACP server bridging Claude Agent SDK with ACP protocol. Handles bidirectional permissions, session management, streaming, MCP servers, model/command enumeration. -
ClaudeClient (
client.py) - Event-driven Python API with decorator handlers, streaming text deduplication, and simple async/await interface. -
AcpClient (
acp_client.py) - Full ACP client implementation that can connect to any ACP-compatible agent via subprocess. Includes file/terminal operation interception. -
AcpProxyServer (
proxy/) - Copilot SDK compatibility layer. Translates Copilot SDK JSON-RPC protocol to ACP, enabling Copilot SDK apps to use Claude, Gemini, or other backends.
Why This Package?
| Approach | API Key | Subscription | ACP Support | Multi-Agent | Event-Driven |
|---|---|---|---|---|---|
| Anthropic API directly | Required | No | No | No | No |
| Claude Agent SDK | No | Uses CLI | No | No | Partial |
| claude-code-acp | No | Uses CLI | Full | Yes | Full |
Examples
Simple Chat
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient()
@client.on_text
async def on_text(text):
print(text, end="")
async with client:
while True:
user_input = input("\nYou: ")
if user_input.lower() == "quit":
break
await client.query(user_input)
asyncio.run(main())
Auto-approve Mode
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient(cwd=".")
@client.on_text
async def on_text(text):
print(text, end="")
async with client:
await client.set_mode("bypassPermissions")
await client.query("Create a complete Flask app with tests")
asyncio.run(main())
Multi-Agent Comparison
import asyncio
from claude_code_acp import AcpClient
async def ask(agent_name, command, args, prompt):
client = AcpClient(command=command, args=args)
@client.on_text
async def on_text(text):
pass # collect silently
async with client:
response = await client.prompt(prompt)
print(f"{agent_name}: {response[:100]}...")
async def main():
await asyncio.gather(
ask("Claude", "claude-code-acp", [], "What is ACP?"),
ask("Gemini", "gemini", ["--experimental-acp"], "What is ACP?"),
)
asyncio.run(main())
Development
# Clone
git clone https://github.com/yazelin/claude-code-acp-py
cd claude-code-acp-py
# Install dependencies
uv sync
# Run ACP server locally
uv run claude-code-acp
# Run Copilot proxy locally
uv run copilot-acp-proxy --backend gemini
# Run tests
uv run pytest tests/test_unit_*.py -v # Unit tests (fast)
uv run pytest tests/ -m integration -v # Integration tests (need Claude CLI)
Related Projects
- claude-code-acp - TypeScript version by Zed Industries
- agent-client-protocol - ACP specification and SDKs
- claude-agent-sdk-python - Official Claude Agent SDK
License
MIT
Metadata
Release files for claude-code-acp 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| claude_code_acp-0.5.1.tar.gz | 177.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| claude_code_acp-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 215.9 kB
Release files / claude_code_acp-0.5.1.tar.gz
| Download URL | claude_code_acp-0.5.1.tar.gz |
|---|---|
| Size | 177.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c7580315287743cde0918bab72b3318738108cc94789a98c10dbb0c2eeb0873
|
|
BLAKE2b-256 checksum How to use checksums |
1f5d787990c2b32dc82b97a7cc378dcffd565f25a79ddc68f988b60f1d6a6524
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 19, 2026.
Transparency logRelease files / claude_code_acp-0.5.1-py3-none-any.whl
| Download URL | claude_code_acp-0.5.1-py3-none-any.whl |
|---|---|
| Size | 38.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f19e44baaaffdcba59c2289b8f5c1bec82861ec12f0c3017f137b2d09d5d621
|
|
BLAKE2b-256 checksum How to use checksums |
ab57e0c709dd07339f593fd1fe9f9f544727579184e47b9e557741859d8dc59e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 19, 2026.
Transparency log