Skip to content

Commit 5203ef1

Browse files
committed
Add examples and update imports
1 parent a311d34 commit 5203ef1

5 files changed

Lines changed: 292 additions & 0 deletions

File tree

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,27 @@
2727
"exports": {
2828
".": {
2929
"types": "./dist/acp.d.ts",
30+
"import": "./dist/acp.js",
3031
"default": "./dist/acp.js"
3132
},
3233
"./http-client": {
3334
"types": "./dist/http-stream.d.ts",
35+
"import": "./dist/http-stream.js",
3436
"default": "./dist/http-stream.js"
3537
},
3638
"./ws-client": {
3739
"types": "./dist/ws-stream.d.ts",
40+
"import": "./dist/ws-stream.js",
3841
"default": "./dist/ws-stream.js"
3942
},
4043
"./server": {
4144
"types": "./dist/server.d.ts",
45+
"import": "./dist/server.js",
4246
"default": "./dist/server.js"
4347
},
4448
"./node": {
4549
"types": "./dist/node-adapter.d.ts",
50+
"import": "./dist/node-adapter.js",
4651
"default": "./dist/node-adapter.js"
4752
},
4853
"./schema/schema.json": "./schema/schema.json"
@@ -67,8 +72,14 @@
6772
"docs:ts:verify": "cd src && typedoc --emit none && echo 'TypeDoc verification passed'"
6873
},
6974
"peerDependencies": {
75+
"ws": ">=8.0.0",
7076
"zod": "^3.25.0 || ^4.0.0"
7177
},
78+
"peerDependenciesMeta": {
79+
"ws": {
80+
"optional": true
81+
}
82+
},
7283
"devDependencies": {
7384
"@eslint/js": "^10.0.1",
7485
"@hey-api/openapi-ts": "^0.97.0",

src/examples/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This directory contains examples using the [ACP](https://agentclientprotocol.com
44

55
- [`agent.ts`](./agent.ts) - A minimal agent implementation that simulates LLM interaction
66
- [`client.ts`](./client.ts) - A minimal client implementation that spawns the [`agent.ts`](./agent.ts) as a subprocess
7+
- [`http-server.ts`](./http-server.ts) - A minimal ACP Streamable HTTP server with WebSocket upgrade support
8+
- [`http-client.ts`](./http-client.ts) - A minimal client using `createHttpStream`
9+
- [`ws-client.ts`](./ws-client.ts) - A minimal client using `createWebSocketStream`
710

811
## Running the Agent
912

@@ -75,3 +78,25 @@ npx tsx src/examples/client.ts
7578
```
7679

7780
This client will spawn the example agent as a subprocess, send a message, and print the content it receives from it.
81+
82+
## Running the HTTP and WebSocket Examples
83+
84+
Start the Streamable HTTP server with WebSocket upgrade support:
85+
86+
```bash
87+
npx tsx src/examples/http-server.ts
88+
```
89+
90+
In another terminal, run the HTTP client:
91+
92+
```bash
93+
npx tsx src/examples/http-client.ts
94+
```
95+
96+
Or run the WebSocket client:
97+
98+
```bash
99+
npx tsx src/examples/ws-client.ts
100+
```
101+
102+
The HTTP example sends a bearer token through custom request headers. The WebSocket example passes the Node `ws` constructor so custom headers can be sent during the WebSocket handshake. Browser WebSocket clients can use `createWebSocketStream` too, but browsers do not allow custom WebSocket headers.

src/examples/http-client.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env node
2+
3+
import * as acp from "@agentclientprotocol/sdk";
4+
import { createHttpStream } from "@agentclientprotocol/sdk/http-client";
5+
6+
class HttpExampleClient implements acp.Client {
7+
async requestPermission(
8+
params: acp.RequestPermissionRequest,
9+
): Promise<acp.RequestPermissionResponse> {
10+
return {
11+
outcome: {
12+
outcome: "selected",
13+
optionId: params.options[0]?.optionId ?? "allow",
14+
},
15+
};
16+
}
17+
18+
async sessionUpdate(params: acp.SessionNotification): Promise<void> {
19+
const update = params.update;
20+
21+
if (update.sessionUpdate === "agent_message_chunk") {
22+
process.stdout.write(
23+
update.content.type === "text" ? update.content.text : "",
24+
);
25+
return;
26+
}
27+
28+
console.log(`[${update.sessionUpdate}]`);
29+
}
30+
}
31+
32+
const serverUrl = process.env.ACP_HTTP_URL ?? "http://127.0.0.1:7331/acp";
33+
const stream = createHttpStream(serverUrl, {
34+
headers: {
35+
Authorization: "Bearer example-token",
36+
},
37+
// To use cookies, pass a cookie-aware fetch implementation here instead of relying on a built-in cookie jar.
38+
// fetch: cookieAwareFetch,
39+
});
40+
const connection = new acp.ClientSideConnection(
41+
(_agent) => new HttpExampleClient(),
42+
stream,
43+
);
44+
45+
try {
46+
await connection.initialize({
47+
protocolVersion: acp.PROTOCOL_VERSION,
48+
clientCapabilities: {},
49+
});
50+
51+
const session = await connection.newSession({
52+
cwd: process.cwd(),
53+
mcpServers: [],
54+
});
55+
56+
const result = await connection.prompt({
57+
sessionId: session.sessionId,
58+
prompt: [
59+
{
60+
type: "text",
61+
text: "Hello over Streamable HTTP",
62+
},
63+
],
64+
});
65+
66+
console.log(`\nDone: ${result.stopReason}`);
67+
} finally {
68+
await stream.writable.close();
69+
}

src/examples/http-server.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env node
2+
3+
import { createServer } from "node:http";
4+
5+
import { WebSocketServer } from "ws";
6+
7+
import * as acp from "@agentclientprotocol/sdk";
8+
import { createNodeHttpHandler } from "@agentclientprotocol/sdk/node";
9+
import { AcpServer } from "@agentclientprotocol/sdk/server";
10+
11+
class HttpExampleAgent implements acp.Agent {
12+
private readonly connection: acp.AgentSideConnection;
13+
private readonly sessions = new Set<string>();
14+
15+
constructor(connection: acp.AgentSideConnection) {
16+
this.connection = connection;
17+
}
18+
19+
async initialize(
20+
_params: acp.InitializeRequest,
21+
): Promise<acp.InitializeResponse> {
22+
return {
23+
protocolVersion: acp.PROTOCOL_VERSION,
24+
agentCapabilities: {
25+
loadSession: false,
26+
},
27+
};
28+
}
29+
30+
async newSession(
31+
_params: acp.NewSessionRequest,
32+
): Promise<acp.NewSessionResponse> {
33+
const sessionId = crypto.randomUUID();
34+
this.sessions.add(sessionId);
35+
return { sessionId };
36+
}
37+
38+
async authenticate(
39+
_params: acp.AuthenticateRequest,
40+
): Promise<acp.AuthenticateResponse> {
41+
return {};
42+
}
43+
44+
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
45+
if (!this.sessions.has(params.sessionId)) {
46+
throw new Error(`Session ${params.sessionId} not found`);
47+
}
48+
49+
await this.connection.sessionUpdate({
50+
sessionId: params.sessionId,
51+
update: {
52+
sessionUpdate: "agent_message_chunk",
53+
content: {
54+
type: "text",
55+
text: "Hello from the ACP HTTP/WebSocket example server.",
56+
},
57+
},
58+
});
59+
60+
return { stopReason: "end_turn" };
61+
}
62+
63+
async cancel(_params: acp.CancelNotification): Promise<void> {}
64+
}
65+
66+
const acpServer = new AcpServer({
67+
createAgent: (connection) => new HttpExampleAgent(connection),
68+
});
69+
const acpHttpHandler = createNodeHttpHandler(acpServer);
70+
const webSocketServer = new WebSocketServer({ noServer: true });
71+
const port = Number.parseInt(process.env.PORT ?? "7331", 10);
72+
73+
const httpServer = createServer((req, res) => {
74+
if (!isAcpPath(req.url)) {
75+
res.writeHead(404, { "Content-Type": "text/plain" });
76+
res.end("Not Found");
77+
return;
78+
}
79+
80+
// Put authentication or tenant-selection middleware here before routing to AcpServer.
81+
// For example, validate `req.headers.authorization` and reject unauthorized requests.
82+
if (!isAuthorized(req.headers.authorization)) {
83+
res.writeHead(401, { "Content-Type": "text/plain" });
84+
res.end("Unauthorized");
85+
return;
86+
}
87+
88+
acpHttpHandler(req, res);
89+
});
90+
91+
httpServer.on("upgrade", (req, socket, head) => {
92+
if (!isAcpPath(req.url) || !isAuthorized(req.headers.authorization)) {
93+
socket.destroy();
94+
return;
95+
}
96+
97+
webSocketServer.handleUpgrade(req, socket, head, (ws) => {
98+
acpServer.handleWebSocket(ws);
99+
});
100+
});
101+
102+
httpServer.listen(port, () => {
103+
console.log(`ACP HTTP endpoint listening at http://127.0.0.1:${port}/acp`);
104+
console.log(`ACP WebSocket endpoint listening at ws://127.0.0.1:${port}/acp`);
105+
});
106+
107+
function isAcpPath(url: string | undefined): boolean {
108+
return new URL(url ?? "/", "http://127.0.0.1").pathname === "/acp";
109+
}
110+
111+
function isAuthorized(authorization: string | undefined): boolean {
112+
return (
113+
authorization === undefined || authorization === "Bearer example-token"
114+
);
115+
}

src/examples/ws-client.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env node
2+
3+
import { WebSocket } from "ws";
4+
5+
import * as acp from "@agentclientprotocol/sdk";
6+
import { createWebSocketStream } from "@agentclientprotocol/sdk/ws-client";
7+
import type { WebSocketConstructor } from "@agentclientprotocol/sdk/ws-client";
8+
9+
class WebSocketExampleClient implements acp.Client {
10+
async requestPermission(
11+
params: acp.RequestPermissionRequest,
12+
): Promise<acp.RequestPermissionResponse> {
13+
return {
14+
outcome: {
15+
outcome: "selected",
16+
optionId: params.options[0]?.optionId ?? "allow",
17+
},
18+
};
19+
}
20+
21+
async sessionUpdate(params: acp.SessionNotification): Promise<void> {
22+
const update = params.update;
23+
24+
if (update.sessionUpdate === "agent_message_chunk") {
25+
process.stdout.write(
26+
update.content.type === "text" ? update.content.text : "",
27+
);
28+
return;
29+
}
30+
31+
console.log(`[${update.sessionUpdate}]`);
32+
}
33+
}
34+
35+
const serverUrl = process.env.ACP_WS_URL ?? "ws://127.0.0.1:7331/acp";
36+
const stream = createWebSocketStream(serverUrl, {
37+
WebSocket: WebSocket satisfies WebSocketConstructor,
38+
// Custom headers work with Node's `ws` constructor. Browser WebSocket does not support custom headers.
39+
headers: {
40+
Authorization: "Bearer example-token",
41+
},
42+
});
43+
const connection = new acp.ClientSideConnection(
44+
(_agent) => new WebSocketExampleClient(),
45+
stream,
46+
);
47+
48+
try {
49+
await connection.initialize({
50+
protocolVersion: acp.PROTOCOL_VERSION,
51+
clientCapabilities: {},
52+
});
53+
54+
const session = await connection.newSession({
55+
cwd: process.cwd(),
56+
mcpServers: [],
57+
});
58+
59+
const result = await connection.prompt({
60+
sessionId: session.sessionId,
61+
prompt: [
62+
{
63+
type: "text",
64+
text: "Hello over WebSocket",
65+
},
66+
],
67+
});
68+
69+
console.log(`\nDone: ${result.stopReason}`);
70+
} finally {
71+
await stream.writable.close();
72+
}

0 commit comments

Comments
 (0)