Module 40: Micronaut
Make an annotated ACP agent a Micronaut bean with com.agentclientprotocol:acp-micronaut: the same application runs as a stdio agent and over Streamable HTTP and WebSocket, with no @Initialize method, a Micronaut @Around interceptor, and a Publisher return.
What You’ll Learn
@Singleton plus @AcpAgent: compile-time bean discovery, no classpath scan, exactly one agent
- Reading the
initialize answer a client receives, and matching each field back to the annotation that produced it
- Configuring the agent (
acp.agent.*) and a client (acp.client.*) from the same module
- A Micronaut
@Around interceptor running around a prompt handler through the SDK’s discovery
- Returning a Reactive Streams
Publisher instead of a plain value
The Code
The agent bean
Two annotations make this an agent bean: @Singleton makes it a bean at all, so Greeter injects
into its constructor like into any other bean; @AcpAgent makes it the agent. Micronaut finds it
from the bean definition its annotation processor wrote at compile time, with no classpath scan, and
exactly one such bean is allowed (startup names them all if there’s more than one).
No @Initialize method
Nothing here answers initialize directly. The SDK derives it from the class:
agentInfo from @AcpAgent(name, version, title).
sessionCapabilities.list from @ListSessions, sessionCapabilities.close from @CloseSession.
promptCapabilities.embeddedContext from @Prompt(embeddedContext = true).
- No
loadSession (there’s no @LoadSession method).
The demo prints the exact initialize answer a client receives, field by field, matched back to the
annotation that produced it:
See Clients and Agents in Java for the full derivation rules
this applies.
The @Around interceptor
@Audited (a plain @Around-advised annotation, nothing ACP-specific) applies this interceptor to
the prompt handler. Applying advice makes Micronaut generate a subclass of the agent class at compile
time, an “intercepted” bean carrying no @AcpAgent annotation of its own. The SDK finds the handlers
through the class the subclass extends, and invokes them on the generated subclass, so the
interceptor actually runs. No special ACP-side wiring is needed beyond the ordinary Micronaut AOP
annotations; the SDK’s own AcpInterceptor beans remain the ACP-aware alternative, applied to every
handler regardless of which method it serves.
Return types: Mono, Flux, CompletionStage, or any Publisher
Both handlers above return Publisher<T> rather than T directly or a bare Mono<T>: any
single-value Reactive Streams Publisher works, not only Mono, which lets handler code use
whichever reactive type a Micronaut data or HTTP client call already hands back. Its first element
becomes the response.
If you build the Publisher with Micronaut’s own Publishers helper rather than Reactor or Mutiny
directly, add micronaut-core-reactive to the classpath; acp-micronaut alone doesn’t pull it in.
Returning a Mono/Flux built directly needs nothing extra.
A client, and sharing the classpath with the agent
Each client application in this module sets acp.agent.enabled=false, since it shares this module’s
classpath (and so the agent’s own bean definition) with the agent application; in production they’re
separate applications entirely. acp.client.transport.http.uri and .websocket.uri build the other
two transports the same way, against acp.agent.transport.type=http’s listener. Customize the
autoconfigured client with an AcpClientCustomizer bean, guarded by
@Requires(property = "acp.client.transport") so it’s absent from the agent-only application:
AcpClientCustomizer is com.agentclientprotocol.sdk.integration.AcpClientCustomizer, the
framework-neutral type Spring Boot and Quarkus customizers implement too, not a Micronaut-specific
interface.
Over HTTP: one bean, many connections
Serving ACP over Streamable HTTP and WebSocket needs com.agentclientprotocol:acp-streamable-http-jetty
on the classpath, an optional dependency of acp-micronaut this module adds explicitly in its own
pom.xml; without it, stdio is the only transport available.
As of the listener-key rename, the port (and host) live under acp.agent.transport.http.listener.*,
matching Spring Boot: the listener binds 127.0.0.1 by default unless
acp.agent.transport.http.listener.host says otherwise. An ACP WebSocket is a long-lived session, so
ACP has its own idle timeout, separate from HTTP keep-alive tuning; the initialize deadline closes
connections that never start. This module sets both explicitly, at their defaults:
Port 0 picks a free port; AcpAgentRuntime.port() reads back what was actually bound. A
Streamable HTTP client and a WebSocket client, each its own application context, connect to the same
listener; the @Around interceptor’s call count keeps climbing across both connections, since one
agent bean serves every connection the listener accepts.
Source Code
View on GitHub
Running the Example
The demo runs the agent two ways: a stdio client launches the packaged agent as a subprocess, prints
the derived initialize answer, prompts it (through the interceptor), lists sessions, then closes
one; then the same application serves HTTP and WebSocket on one listener, with a client on each
transport talking to the same agent bean. No API key required.
Next Module
Module 41: Quarkus: the same shape, on the Quarkus extension, with Mutiny Uni/Multi and a cancellable stream.