Skip to main content

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.