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
- Annotation-based agents - Spring MVC-style
@AcpAgent,@Promptannotations
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 annotation-based agent development:
<dependency>
<groupId>com.agentclientprotocol</groupId>
<artifactId>acp-agent-support</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, context) -> {
// Send updates using blocking void method
context.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, context) ->
context.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();For the simplest agent development experience, use annotations (see full documentation):
import com.agentclientprotocol.sdk.annotation.*;
import com.agentclientprotocol.sdk.agent.SyncPromptContext;
import com.agentclientprotocol.sdk.agent.support.AcpAgentSupport;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
@AcpAgent
class HelloAgent {
@Initialize
InitializeResponse init() {
return InitializeResponse.ok();
}
@NewSession
NewSessionResponse newSession() {
return new NewSessionResponse(UUID.randomUUID().toString(), null, null);
}
@Prompt
PromptResponse prompt(PromptRequest req, SyncPromptContext ctx) {
ctx.sendMessage("Hello from the agent!");
return PromptResponse.endTurn();
}
}
// Bootstrap and run
AcpAgentSupport.create(new HelloAgent())
.transport(new StdioAcpAgentTransport())
.run();This approach reduces boilerplate by ~50% compared to the builder API while producing identical runtime behavior.
Send real-time updates to the client during prompt processing.
Agent (Sync) - recommended:
.promptHandler((req, context) -> {
// Blocking void calls - simple and straightforward
context.sendUpdate(req.sessionId(),
new AgentThoughtChunk("agent_thought_chunk",
new TextContent("Thinking...")));
context.sendUpdate(req.sessionId(),
new AgentMessageChunk("agent_message_chunk",
new TextContent("Here's my response.")));
return new PromptResponse(StopReason.END_TURN);
})Agent (Async):
.promptHandler((request, context) -> {
return context.sendUpdate(request.sessionId(),
new AgentThoughtChunk("agent_thought_chunk",
new TextContent("Thinking...")))
.then(context.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. The context parameter provides access to all agent capabilities.
Agent (Sync) - reading files:
AcpSyncAgent agent = AcpAgent.sync(transport)
.promptHandler((req, context) -> {
// Read a file from the client's filesystem
var fileResponse = context.readTextFile(
new ReadTextFileRequest(req.sessionId(), "pom.xml", null, 10));
String content = fileResponse.content();
// Write a file
context.writeTextFile(
new WriteTextFileRequest(req.sessionId(), "output.txt", "Hello!"));
return new PromptResponse(StopReason.END_TURN);
})
.build();
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.agent.support |
Annotation-based agent runtime (AcpAgentSupport) |
com.agentclientprotocol.sdk.annotation |
Agent annotations (@AcpAgent, @Prompt, etc.) |
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 unit tests (258 tests)
./mvnw verify # Run unit tests + integration tests
./mvnw install # Install to local Maven repositoryIntegration tests connect to real ACP agents and require additional setup:
# Gemini CLI integration tests (requires API key and gemini CLI)
export GEMINI_API_KEY=your_key_here
./mvnw verify -pl acp-coreTest Categories:
| Type | Command | Count | Requirements |
|---|---|---|---|
| Unit tests | ./mvnw test |
258 | None |
| Clean shutdown IT | ./mvnw verify |
4 | None |
| Gemini CLI IT | ./mvnw verify |
5 | GEMINI_API_KEY, gemini CLI in PATH |
Use 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) - 258 tests
- Maven Central publishing
- Production hardening
- Performance optimizations