Zero-Config ACP Agents with Spring Boot
One dependency, a few properties, an annotated bean. Spring Boot handles the rest - transport, lifecycle, graceful shutdown.
ACP standardizes how IDEs talk to AI agents - LSP for agents instead of language servers. If you want the full story, see ACP Java SDK: Building IDE Agents in Java.
The ACP Java SDK already has an annotation-based programming model - annotate a class with @AcpAgent, add @Initialize, @Prompt, and @NewSession handlers, and you're done. But to use that in Spring Boot, you still need to wire the beans yourself: create the transport, build the AcpAgentSupport, manage lifecycle with SmartLifecycle, handle graceful shutdown.
That's the kind of boilerplate Spring Boot autoconfiguration exists to eliminate. So I wrote one. It's part of the spring-ai-community GitHub organization.
What you'd write without it
To run an @AcpAgent bean in Spring Boot without the starter, you'd need a configuration class like this:
@Configuration
public class AcpConfig {
@Bean
AcpAgentTransport acpAgentTransport() {
return new StdioAcpAgentTransport();
}
@Bean
SmartLifecycle acpAgentLifecycle(MyAgent agent,
AcpAgentTransport transport) {
var support = AcpAgentSupport.create(agent)
.transport(transport)
.build();
return new SmartLifecycle() {
private volatile boolean running = false;
public void start() { support.start(); running = true; }
public void stop() { support.close(); running = false; }
public boolean isRunning() { return running; }
};
}
}
Plus the same for the client side - transport detection, AcpSyncClient and AcpAsyncClient beans, DisposableBean for shutdown. That's four configuration classes worth of plumbing.
What the autoconfiguration handles
Add the starter dependency, and all of that disappears. Your agent is just a Spring bean:
@Component
@AcpAgent(name = "echo-agent", version = "1.0")
public class EchoAgentBean {
@Initialize
public InitializeResponse initialize(InitializeRequest request) {
return InitializeResponse.ok();
}
@NewSession
public NewSessionResponse newSession(NewSessionRequest request) {
return new NewSessionResponse(
UUID.randomUUID().toString(), null, null);
}
@Prompt
public PromptResponse prompt(PromptRequest request,
SyncPromptContext context) {
context.sendMessage("Echo: " + request.text());
return PromptResponse.endTurn();
}
}
The autoconfiguration discovers it, creates the transport, wires AcpAgentSupport, and manages start/stop through SmartLifecycle. Four @AutoConfiguration classes, each backing off if you provide your own beans:
| Class | What it creates |
|---|---|
AcpAgentTransportAutoConfiguration |
Stdio transport (default), auto-detected from properties |
AcpAgentAutoConfiguration |
Finds your @AcpAgent bean, wires interceptors, manages lifecycle |
AcpClientTransportAutoConfiguration |
Client-side transport (stdio or WebSocket, auto-detected) |
AcpClientAutoConfiguration |
AcpSyncClient + AcpAsyncClient beans with graceful shutdown |
Stdio gotchas
Three configuration changes are essential for stdio agents:
application.properties:
# Disable banner - stdout is reserved for the JSON-RPC protocol
spring.main.banner-mode=off
# Keep the JVM alive (no web server to block the main thread)
spring.main.keep-alive=true
Without keep-alive, the application starts the agent, then exits immediately because there's no web server holding the JVM open. Without banner-mode=off, the Spring banner writes to stdout and corrupts the JSON-RPC stream.
logback-spring.xml - redirect all logging to stderr:
<configuration>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDERR"/>
</root>
</configuration>
Agent stdout is reserved for the protocol. All logging must go to stderr.
Client side
The starter also autoconfigures the client side - useful for testing agents or building multi-agent systems. Configure via properties:
spring.acp.client.transport.stdio.command=java
spring.acp.client.transport.stdio.args=-jar,echo-agent.jar
spring.acp.client.request-timeout=60s
Then inject and use:
@Bean
CommandLineRunner demo(AcpSyncClient client) {
return args -> {
client.initialize();
String cwd = System.getProperty("user.dir");
var session = client.newSession(
new NewSessionRequest(cwd, List.of()));
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello from Spring Boot client!"))));
};
}
Transport is auto-detected. Set spring.acp.client.transport.stdio.command and you get stdio. Set spring.acp.client.transport.websocket.uri and you get WebSocket. No explicit type selector needed.
Try it
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>acp-spring-boot-starter</artifactId>
<version>0.11.0</version>
</dependency>
The ACP Java SDK tutorial has working examples: Module 23 (Spring Boot agent) and Module 24 (Spring Boot client). The tutorial docs walk through both.