ACP-compatible agent for Claude Code (Python version)
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_code_acp-0.4.4.tar.gz.
File metadata
- Download URL: claude_code_acp-0.4.4.tar.gz
- Upload date:
- Size: 176.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a43b45b70f81e8763a77234acff1ff55b97286cf0fcc644886e55e804b76a1da
|
|
| MD5 |
1844e7e5c438d1fb16647c208b5b54c6
|
|
| BLAKE2b-256 |
152ae8a2fadbca5f9e524d678505ea770200c492c3874c6dc79667abc628bd4a
|
Provenance
The following attestation bundles were made for claude_code_acp-0.4.4.tar.gz:
Publisher:
publish.yml on yazelin/claude-code-acp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_code_acp-0.4.4.tar.gz -
Subject digest:
a43b45b70f81e8763a77234acff1ff55b97286cf0fcc644886e55e804b76a1da - Sigstore transparency entry: 940058229
- Sigstore integration time:
-
Permalink:
yazelin/claude-code-acp-py@a3b7e9ff52d092f47e8153b4f51f44a2fbfa26c7 -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/yazelin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a3b7e9ff52d092f47e8153b4f51f44a2fbfa26c7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file claude_code_acp-0.4.4-py3-none-any.whl.
File metadata
- Download URL: claude_code_acp-0.4.4-py3-none-any.whl
- Upload date:
- Size: 37.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
136288b861324883e6334965bd44c13cbc890ea34f8f4ae5e7e6a9b1b8bff2cf
|
|
| MD5 |
259e9c28fc6dee730d2cda99d5dc4ed3
|
|
| BLAKE2b-256 |
8885f8c95c74d33669cfb22523a107482ff068c4fe89f7bacf50f07f529ddfca
|
Provenance
The following attestation bundles were made for claude_code_acp-0.4.4-py3-none-any.whl:
Publisher:
publish.yml on yazelin/claude-code-acp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_code_acp-0.4.4-py3-none-any.whl -
Subject digest:
136288b861324883e6334965bd44c13cbc890ea34f8f4ae5e7e6a9b1b8bff2cf - Sigstore transparency entry: 940058236
- Sigstore integration time:
-
Permalink:
yazelin/claude-code-acp-py@a3b7e9ff52d092f47e8153b4f51f44a2fbfa26c7 -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/yazelin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a3b7e9ff52d092f47e8153b4f51f44a2fbfa26c7 -
Trigger Event:
release
-
Statement type: