Skip to content

Commit e2ba51c

Browse files
committed
test: add streamable HTTP demo agent server
1 parent 23858f1 commit e2ba51c

7 files changed

Lines changed: 296 additions & 0 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,17 @@ agent.start().block(); // Starts WebSocket server on port 8080
392392
| WebSocket | `WebSocketAcpClientTransport` | `WebSocketAcpAgentTransport` | acp-core / acp-websocket-jetty |
393393
| Streamable HTTP | `StreamableHttpAcpClientTransport` | `StreamableHttpAcpAgentTransport` | acp-core / acp-streamable-http-jetty |
394394

395+
### Streamable HTTP Demo Server
396+
397+
Build and run a local demo ACP agent over HTTP/SSE:
398+
399+
```bash
400+
./mvnw -q -pl test-fixtures/streamable-http-agent-server -am -DskipTests package
401+
java -jar test-fixtures/streamable-http-agent-server/target/acp-streamable-http-agent-server.jar --port 8080
402+
```
403+
404+
The endpoint will be available at `http://127.0.0.1:8080/acp`.
405+
395406
---
396407

397408
## Building

plans/STREAMABLE-HTTP-AGENT-TRANSPORT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ Covered scenarios:
109109

110110
The Java module also keeps focused integration coverage for strict unknown-session behavior.
111111

112+
## Demo Server
113+
114+
Add a runnable Java demo server at:
115+
116+
```text
117+
test-fixtures/streamable-http-agent-server/
118+
```
119+
120+
It packages a small echo-style ACP agent into a runnable jar backed by the real
121+
Jetty `StreamableHttpAcpAgentTransport`, so manual testing can exercise a live
122+
HTTP/SSE endpoint instead of only the integration-test fixture lifecycle.
123+
112124
## PLAN / Follow-Up Work
113125

114126
- extract a shared remote-core layer only after HTTP parity is proven

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<module>acp-agent-support</module>
2020
<module>acp-test</module>
2121
<module>acp-streamable-http-jetty</module>
22+
<module>test-fixtures/streamable-http-agent-server</module>
2223
<module>acp-websocket-jetty</module>
2324
</modules>
2425

@@ -115,6 +116,11 @@
115116
<artifactId>acp-test</artifactId>
116117
<version>${project.version}</version>
117118
</dependency>
119+
<dependency>
120+
<groupId>com.agentclientprotocol</groupId>
121+
<artifactId>acp-streamable-http-jetty</artifactId>
122+
<version>${project.version}</version>
123+
</dependency>
118124

119125
<!-- Logging -->
120126
<dependency>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Streamable HTTP Agent Demo Server
2+
3+
This is a small runnable Java ACP agent that serves the new Streamable HTTP
4+
agent transport from a real Jetty web server.
5+
6+
Build the runnable jar:
7+
8+
```bash
9+
./mvnw -q -pl test-fixtures/streamable-http-agent-server -am -DskipTests package
10+
```
11+
12+
Run it:
13+
14+
```bash
15+
java -jar test-fixtures/streamable-http-agent-server/target/acp-streamable-http-agent-server.jar --port 8080
16+
```
17+
18+
Then drive it with the fixture client from another shell:
19+
20+
```bash
21+
cd test-fixtures/streamable-http-client
22+
npm install
23+
npm run build
24+
node dist/client.js --endpoint http://127.0.0.1:8080/acp --scenario happy-path
25+
```
26+
27+
The demo supports `initialize`, `session/new`, `session/load`, `session/prompt`,
28+
and `session/cancel`. Prompts containing the word `permission` also exercise the
29+
agent-to-client `session/request_permission` round trip.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.agentclientprotocol</groupId>
9+
<artifactId>acp-java-sdk</artifactId>
10+
<version>0.12.0-SNAPSHOT</version>
11+
<relativePath>../../pom.xml</relativePath>
12+
</parent>
13+
14+
<artifactId>acp-streamable-http-agent-server</artifactId>
15+
<packaging>jar</packaging>
16+
17+
<name>ACP Streamable HTTP Agent Server Demo</name>
18+
<description>Runnable demo server for the Streamable HTTP agent transport</description>
19+
20+
<properties>
21+
<!-- This is a runnable fixture, not a published SDK artifact. -->
22+
<maven.deploy.skip>true</maven.deploy.skip>
23+
</properties>
24+
25+
<dependencies>
26+
<dependency>
27+
<groupId>com.agentclientprotocol</groupId>
28+
<artifactId>acp-streamable-http-jetty</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>ch.qos.logback</groupId>
32+
<artifactId>logback-classic</artifactId>
33+
<scope>runtime</scope>
34+
</dependency>
35+
</dependencies>
36+
37+
<build>
38+
<plugins>
39+
<plugin>
40+
<groupId>org.apache.maven.plugins</groupId>
41+
<artifactId>maven-shade-plugin</artifactId>
42+
<version>3.5.3</version>
43+
<executions>
44+
<execution>
45+
<phase>package</phase>
46+
<goals>
47+
<goal>shade</goal>
48+
</goals>
49+
<configuration>
50+
<createDependencyReducedPom>false</createDependencyReducedPom>
51+
<finalName>acp-streamable-http-agent-server</finalName>
52+
<transformers>
53+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
54+
<mainClass>com.agentclientprotocol.sdk.fixtures.StreamableHttpAgentDemoServer</mainClass>
55+
</transformer>
56+
</transformers>
57+
</configuration>
58+
</execution>
59+
</executions>
60+
</plugin>
61+
</plugins>
62+
</build>
63+
64+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2025-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.fixtures;
6+
7+
import java.time.Duration;
8+
import java.util.Locale;
9+
import java.util.Map;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
13+
import com.agentclientprotocol.sdk.agent.AcpAgent;
14+
import com.agentclientprotocol.sdk.agent.AcpAgentFactory;
15+
import com.agentclientprotocol.sdk.agent.transport.StreamableHttpAcpAgentTransport;
16+
import com.agentclientprotocol.sdk.json.AcpJsonMapper;
17+
import com.agentclientprotocol.sdk.spec.AcpSchema;
18+
import reactor.core.publisher.Mono;
19+
20+
/**
21+
* Small runnable ACP agent server for manually exercising the Streamable HTTP transport.
22+
*
23+
* @author Kaiser Dandangi
24+
*/
25+
public final class StreamableHttpAgentDemoServer {
26+
27+
private static final Duration START_TIMEOUT = Duration.ofSeconds(30);
28+
29+
private static final Duration STOP_TIMEOUT = Duration.ofSeconds(5);
30+
31+
private StreamableHttpAgentDemoServer() {
32+
}
33+
34+
public static void main(String[] args) {
35+
Options options;
36+
try {
37+
options = Options.parse(args);
38+
}
39+
catch (IllegalArgumentException e) {
40+
System.err.println(e.getMessage());
41+
printUsage();
42+
System.exit(2);
43+
return;
44+
}
45+
46+
if (options.help()) {
47+
printUsage();
48+
return;
49+
}
50+
51+
Map<String, String> sessionCwds = new ConcurrentHashMap<>();
52+
AtomicInteger sessionCounter = new AtomicInteger();
53+
54+
AcpAgentFactory agentFactory = AcpAgentFactory.async(transport -> AcpAgent.async(transport)
55+
.requestTimeout(Duration.ofMinutes(2))
56+
.initializeHandler(request -> Mono.just(AcpSchema.InitializeResponse.ok(
57+
new AcpSchema.AgentCapabilities(true, new AcpSchema.McpCapabilities(),
58+
new AcpSchema.PromptCapabilities()))))
59+
.newSessionHandler(request -> {
60+
String sessionId = "demo-session-" + sessionCounter.incrementAndGet();
61+
sessionCwds.put(sessionId, request.cwd());
62+
return Mono.just(new AcpSchema.NewSessionResponse(sessionId, null, null));
63+
})
64+
.loadSessionHandler(request -> {
65+
sessionCwds.put(request.sessionId(), request.cwd());
66+
return Mono.just(new AcpSchema.LoadSessionResponse(null, null));
67+
})
68+
.promptHandler((request, context) -> {
69+
String text = request.text();
70+
String normalized = text == null || text.isBlank() ? "(empty prompt)" : text;
71+
String cwd = sessionCwds.getOrDefault(request.sessionId(), "(unknown cwd)");
72+
Mono<Void> response = normalized.toLowerCase(Locale.ROOT).contains("permission")
73+
? context.askPermission("Demo agent permission check for session " + request.sessionId())
74+
.flatMap(allowed -> context.sendMessage(
75+
"Permission " + (allowed ? "granted" : "denied") + ". Prompt: " + normalized))
76+
.onErrorResume(error -> context.sendMessage(
77+
"Permission request failed in demo server: " + error.getMessage()))
78+
: context.sendMessage("Demo agent received: " + normalized + " [cwd=" + cwd + "]");
79+
return response.thenReturn(AcpSchema.PromptResponse.endTurn());
80+
})
81+
.cancelHandler(notification -> {
82+
System.out.println("Received cancel for session " + notification.sessionId());
83+
return Mono.empty();
84+
})
85+
.build());
86+
87+
StreamableHttpAcpAgentTransport server = new StreamableHttpAcpAgentTransport(options.port(), options.path(),
88+
AcpJsonMapper.createDefault(), agentFactory)
89+
.routingMode(options.routingMode());
90+
91+
Runtime.getRuntime().addShutdownHook(new Thread(() -> server.closeGracefully().block(STOP_TIMEOUT),
92+
"acp-demo-shutdown"));
93+
94+
server.start().block(START_TIMEOUT);
95+
System.out.println("ACP Streamable HTTP demo agent listening at http://127.0.0.1:" + server.getPort()
96+
+ options.path());
97+
System.out.println("Press Ctrl-C to stop.");
98+
server.awaitTermination().block();
99+
}
100+
101+
private static void printUsage() {
102+
System.out.println("""
103+
Usage: java -jar acp-streamable-http-agent-server.jar [options]
104+
105+
Options:
106+
--port <port> Port to listen on. Defaults to 8080.
107+
--path <path> ACP endpoint path. Defaults to /acp.
108+
--strict Use strict transport routing.
109+
--compatible Use compatible transport routing. This is the default.
110+
-h, --help Show this help.
111+
""");
112+
}
113+
114+
private record Options(int port, String path, StreamableHttpAcpAgentTransport.RoutingMode routingMode,
115+
boolean help) {
116+
117+
static Options parse(String[] args) {
118+
int port = 8080;
119+
String path = StreamableHttpAcpAgentTransport.DEFAULT_ACP_PATH;
120+
StreamableHttpAcpAgentTransport.RoutingMode routingMode =
121+
StreamableHttpAcpAgentTransport.RoutingMode.COMPATIBLE;
122+
boolean help = false;
123+
124+
for (int i = 0; i < args.length; i++) {
125+
String arg = args[i];
126+
switch (arg) {
127+
case "--port" -> port = parsePort(requireValue(args, ++i, "--port"));
128+
case "--path" -> path = requireValue(args, ++i, "--path");
129+
case "--strict" -> routingMode = StreamableHttpAcpAgentTransport.RoutingMode.STRICT;
130+
case "--compatible" -> routingMode = StreamableHttpAcpAgentTransport.RoutingMode.COMPATIBLE;
131+
case "-h", "--help" -> help = true;
132+
default -> throw new IllegalArgumentException("Unknown option: " + arg);
133+
}
134+
}
135+
136+
if (!path.startsWith("/")) {
137+
throw new IllegalArgumentException("--path must start with /");
138+
}
139+
return new Options(port, path, routingMode, help);
140+
}
141+
142+
private static String requireValue(String[] args, int index, String option) {
143+
if (index >= args.length || args[index].startsWith("--")) {
144+
throw new IllegalArgumentException(option + " requires a value");
145+
}
146+
return args[index];
147+
}
148+
149+
private static int parsePort(String value) {
150+
try {
151+
int port = Integer.parseInt(value);
152+
if (port <= 0) {
153+
throw new IllegalArgumentException("--port must be positive");
154+
}
155+
return port;
156+
}
157+
catch (NumberFormatException e) {
158+
throw new IllegalArgumentException("--port must be a number", e);
159+
}
160+
}
161+
162+
}
163+
164+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
<root level="INFO">
8+
<appender-ref ref="STDOUT"/>
9+
</root>
10+
</configuration>

0 commit comments

Comments
 (0)