Pure Java implementation of the Agent Client Protocol (ACP) specification for building both clients and agents.
The Agent Client Protocol (ACP) standardizes communication between code editors and coding agents. This SDK enables Java applications to:
- Connect to ACP-compliant agents (Client SDK)
- Build ACP-compliant agents in Java (Agent SDK)
Key Features:
- Java 17+, reactive (Project Reactor), type-safe
- Async and sync APIs
- Stdio and WebSocket transports
- Capability negotiation and structured error handling
Note: Not yet published to Maven Central. For now, build and install locally using
./mvnw install.
<dependency>
<groupId>com.agentclientprotocol</groupId>
<artifactId>acp-core</artifactId>
<version>0.9.0</version>
</dependency>For WebSocket server support (agents accepting WebSocket connections):
<dependency>
<groupId>com.agentclientprotocol</groupId>
<artifactId>acp-websocket-jetty</artifactId>
<version>0.9.0</version>
</dependency>Connect to an ACP agent and send a prompt:
import com.agentclientprotocol.sdk.client.*;
import com.agentclientprotocol.sdk.client.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.List;
// Connect to an agent via stdio
var params = AgentParameters.builder("gemini").arg("--experimental-acp").build();
var transport = new StdioAcpClientTransport(params);
// Create client
AcpSyncClient client = AcpClient.sync(transport).build();
// Initialize, create session, send prompt
client.initialize();
var session = client.newSession(new NewSessionRequest("/workspace", List.of()));
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello, world!"))
));
client.close();Create a minimal ACP agent using the sync API (recommended for simplicity):
import com.agentclientprotocol.sdk.agent.*;
import com.agentclientprotocol.sdk.agent.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.List;
import java.util.UUID;
// Create stdio transport
var transport = new StdioAcpAgentTransport();
// Build sync agent - handlers use plain return values (no Mono!)
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req ->
new InitializeResponse(1, new AgentCapabilities(), List.of()))
.newSessionHandler(req ->
new NewSessionResponse(UUID.randomUUID().toString(), null, null))
.promptHandler((req, updater) -> {
// Send updates using blocking void method
updater.sendUpdate(req.sessionId(),
new AgentMessageChunk("agent_message_chunk",
new TextContent("Hello from the agent!")));
// Return response directly (no Mono!)
return new PromptResponse(StopReason.END_TURN);
})
.build();
// Run agent (blocks until client disconnects)
agent.run();For reactive applications, use the async API:
import com.agentclientprotocol.sdk.agent.*;
import com.agentclientprotocol.sdk.agent.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.UUID;
var transport = new StdioAcpAgentTransport();
AcpAsyncAgent agent = AcpAgent.async(transport)
.initializeHandler(req -> Mono.just(
new InitializeResponse(1, new AgentCapabilities(), List.of())))
.newSessionHandler(req -> Mono.just(
new NewSessionResponse(UUID.randomUUID().toString(), null, null)))
.promptHandler((req, updater) ->
updater.sendUpdate(req.sessionId(),
new AgentMessageChunk("agent_message_chunk",
new TextContent("Hello from the agent!")))
.then(Mono.just(new PromptResponse(StopReason.END_TURN))))
.build();
// Start and await termination
agent.start().then(agent.awaitTermination()).block();Send real-time updates to the client during prompt processing.
Agent (Sync) - recommended:
.promptHandler((req, updater) -> {
// Blocking void calls - simple and straightforward
updater.sendUpdate(req.sessionId(),
new AgentThoughtChunk("agent_thought_chunk",
new TextContent("Thinking...")));
updater.sendUpdate(req.sessionId(),
new AgentMessageChunk("agent_message_chunk",
new TextContent("Here's my response.")));
return new PromptResponse(StopReason.END_TURN);
})Agent (Async):
.promptHandler((request, updater) -> {
return updater.sendUpdate(request.sessionId(),
new AgentThoughtChunk("agent_thought_chunk",
new TextContent("Thinking...")))
.then(updater.sendUpdate(request.sessionId(),
new AgentMessageChunk("agent_message_chunk",
new TextContent("Here's my response."))))
.then(Mono.just(new PromptResponse(StopReason.END_TURN)));
})Client - receiving updates:
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
var update = notification.update();
if (update instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
}
})
.build();Agents can request file operations from the client.
Agent (Sync) - reading files:
// Store agent reference for use in prompt handler
AtomicReference<AcpSyncAgent> agentRef = new AtomicReference<>();
AcpSyncAgent agent = AcpAgent.sync(transport)
.promptHandler((req, updater) -> {
AcpSyncAgent agentInstance = agentRef.get();
// Read a file from the client's filesystem
var fileResponse = agentInstance.readTextFile(
new ReadTextFileRequest(req.sessionId(), "pom.xml", null, 10));
String content = fileResponse.content();
// Write a file
agentInstance.writeTextFile(
new WriteTextFileRequest(req.sessionId(), "output.txt", "Hello!"));
return new PromptResponse(StopReason.END_TURN);
})
.build();
agentRef.set(agent); // Store reference before running
agent.run();Client - registering file handlers:
AcpSyncClient client = AcpClient.sync(transport)
.readTextFileHandler((ReadTextFileRequest req) -> {
// Handlers receive typed requests directly
String content = Files.readString(Path.of(req.path()));
return new ReadTextFileResponse(content);
})
.writeTextFileHandler((WriteTextFileRequest req) -> {
Files.writeString(Path.of(req.path()), req.content());
return new WriteTextFileResponse();
})
.build();Check what features the peer supports before using them:
// Client: check agent capabilities after initialize
client.initialize(new InitializeRequest(1, clientCaps));
NegotiatedCapabilities agentCaps = client.getAgentCapabilities();
if (agentCaps.supportsLoadSession()) {
// Agent supports session persistence
}
if (agentCaps.supportsImageContent()) {
// Agent can handle image content in prompts
}// Agent: check client capabilities before requesting operations
NegotiatedCapabilities clientCaps = agent.getClientCapabilities();
if (clientCaps.supportsReadTextFile()) {
agent.readTextFile(...);
} else {
// Client doesn't support file reading - handle gracefully
}
// Or use require methods (throws AcpCapabilityException if not supported)
clientCaps.requireWriteTextFile();
agent.writeTextFile(...);Handle protocol errors with structured exceptions:
import com.agentclientprotocol.sdk.error.*;
try {
client.prompt(request);
} catch (AcpProtocolException e) {
if (e.isConcurrentPrompt()) {
// Another prompt is already in progress
} else if (e.isMethodNotFound()) {
// Agent doesn't support this method
}
System.err.println("Error " + e.getCode() + ": " + e.getMessage());
} catch (AcpCapabilityException e) {
// Tried to use a capability the peer doesn't support
System.err.println("Capability not supported: " + e.getCapability());
} catch (AcpConnectionException e) {
// Transport-level connection error
}Use WebSocket instead of stdio for network-based communication:
Client (JDK-native, no extra dependencies):
import com.agentclientprotocol.sdk.client.transport.WebSocketAcpClientTransport;
import java.net.URI;
var transport = new WebSocketAcpClientTransport(
URI.create("ws://localhost:8080/acp"),
McpJsonMapper.getDefault()
);
AcpSyncClient client = AcpClient.sync(transport).build();Agent (requires acp-websocket-jetty module):
import com.agentclientprotocol.sdk.agent.transport.WebSocketAcpAgentTransport;
var transport = new WebSocketAcpAgentTransport(
8080, // port
"/acp", // path
McpJsonMapper.getDefault()
);
AcpAsyncAgent agent = AcpAgent.async(transport)
// ... handlers ...
.build();
agent.start().block(); // Starts WebSocket server on port 8080| Package | Description |
|---|---|
com.agentclientprotocol.sdk.spec |
Protocol types (AcpSchema.*) |
com.agentclientprotocol.sdk.client |
Client SDK (AcpClient, AcpAsyncClient, AcpSyncClient) |
com.agentclientprotocol.sdk.agent |
Agent SDK (AcpAgent, AcpAsyncAgent, AcpSyncAgent) |
com.agentclientprotocol.sdk.capabilities |
Capability negotiation (NegotiatedCapabilities) |
com.agentclientprotocol.sdk.error |
Exceptions (AcpProtocolException, AcpCapabilityException) |
| Transport | Client | Agent | Module |
|---|---|---|---|
| Stdio | StdioAcpClientTransport |
StdioAcpAgentTransport |
acp-core |
| WebSocket | WebSocketAcpClientTransport |
WebSocketAcpAgentTransport |
acp-core / acp-websocket-jetty |
./mvnw compile # Compile
./mvnw test # Run tests (246 tests across 2 modules)
./mvnw install # Install to local Maven repositoryUse the mock utilities for testing:
import com.agentclientprotocol.sdk.test.*;
// Create in-memory transport pair for testing
InMemoryTransportPair pair = InMemoryTransportPair.create();
// Use pair.clientTransport() for client, pair.agentTransport() for agent
MockAcpClient mockClient = MockAcpClient.builder(pair.clientTransport())
.fileContent("/test.txt", "test content")
.build();- Client and Agent SDKs with async/sync APIs
- Stdio and WebSocket transports
- Capability negotiation
- Structured error handling
- Full protocol compliance (all SessionUpdate types, MCP configs,
_metaextensibility) - 232 tests
- Maven Central publishing
- Production hardening
- Performance optimizations