Skip to content

Commit 71285cd

Browse files
committed
feat: refactor rest express server
1 parent ba84d86 commit 71285cd

File tree

4 files changed

+80
-38
lines changed

4 files changed

+80
-38
lines changed

.github/workflows/run-tck.yaml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ env:
1313
TCK_VERSION: 0.3.0.beta3
1414
# Tells uv to not need a venv, and instead use system
1515
UV_SYSTEM_PYTHON: 1
16-
# SUT_JSONRPC_URL to use for the TCK and the server agent
17-
SUT_JSONRPC_URL: http://localhost:41241
16+
# Base URL for the SUT agent
17+
SUT_BASE_URL: http://localhost:41241
18+
# SUT_JSONRPC_URL to use for the TCK (JSON-RPC transport)
19+
SUT_JSONRPC_URL: http://localhost:41241/a2a/jsonrpc
20+
# SUT_REST_URL to use for the TCK (HTTP+REST transport)
21+
SUT_REST_URL: http://localhost:41241/a2a/api
1822
# Slow system on CI
1923
TCK_STREAMING_TIMEOUT: 5.0
2024

@@ -58,7 +62,7 @@ jobs:
5862
npm run tck:sut-agent &
5963
- name: Wait for SUT to start
6064
run: |
61-
URL="${{ env.SUT_JSONRPC_URL }}/.well-known/agent-card.json"
65+
URL="${{ env.SUT_BASE_URL }}/.well-known/agent-card.json"
6266
EXPECTED_STATUS=200
6367
TIMEOUT=120
6468
RETRY_INTERVAL=2
@@ -90,11 +94,17 @@ jobs:
9094
sleep "$RETRY_INTERVAL"
9195
done
9296
93-
- name: Run TCK
94-
id: run-tck
97+
- name: Run TCK (JSON-RPC Transport)
98+
id: run-tck-jsonrpc
9599
timeout-minutes: 5
96100
run: |
97-
./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory
101+
./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc --transport-strategy prefer_jsonrpc
102+
working-directory: tck/a2a-tck
103+
- name: Run TCK (HTTP+REST Transport)
104+
id: run-tck-rest
105+
timeout-minutes: 5
106+
run: |
107+
./run_tck.py --sut-url ${{ env.SUT_REST_URL }} --category mandatory --transports rest --transport-strategy prefer_rest
98108
working-directory: tck/a2a-tck
99109
- name: Stop SUT
100110
if: always()

src/server/express/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33
* This module provides Express.js specific functionality.
44
*/
55

6+
// Legacy A2AExpressApp class (for backward compatibility)
67
export { A2AExpressApp } from "./a2a_express_app.js";
8+
79
export { jsonRpcHandler, jsonErrorHandler } from "./json_rpc_handler.js";
810
export type { JsonRpcHandlerOptions } from "./json_rpc_handler.js";
11+
912
export { agentCardHandler } from "./agent_card_handler.js";
1013
export type { AgentCardHandlerOptions, AgentCardProvider } from "./agent_card_handler.js";
14+
1115
export { httpRestHandler } from "./http_rest_routes.js";
1216
export type { HttpRestHandlerOptions } from "./http_rest_routes.js";
17+
18+
// Deprecated exports (for backward compatibility)
19+
export { createHttpRestRouter } from "./http_rest_routes.js";

tck/agent/index.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ExecutionEventBus,
1616
DefaultRequestHandler
1717
} from "../../src/server/index.js";
18-
import { A2AExpressApp, jsonRpcHandler, agentCardHandler, httpRestHandler } from "../../src/server/express/index.js";
18+
import { jsonRpcHandler, agentCardHandler, httpRestHandler } from "../../src/server/express/index.js";
1919

2020
/**
2121
* SUTAgentExecutor implements the agent's core logic.
@@ -157,8 +157,8 @@ class SUTAgentExecutor implements AgentExecutor {
157157
const SUTAgentCard: AgentCard = {
158158
name: 'SUT Agent',
159159
description: 'A sample agent to be used as SUT against tck tests.',
160-
// Adjust the base URL and port as needed. /a2a is the default base in A2AExpressApp
161-
url: 'http://localhost:41241/',
160+
// Main URL points to JSON-RPC endpoint (preferred transport)
161+
url: 'http://localhost:41241/a2a/jsonrpc',
162162
provider: {
163163
organization: 'A2A Samples',
164164
url: 'https://example.com/a2a-samples' // Added provider URL
@@ -185,9 +185,9 @@ const SUTAgentCard: AgentCard = {
185185
],
186186
supportsAuthenticatedExtendedCard: false,
187187
preferredTransport: 'JSONRPC',
188+
// Additional interface for HTTP+REST transport
188189
additionalInterfaces: [
189-
{url: 'http://localhost:41241', transport: 'JSONRPC'},
190-
{url: 'http://localhost:41241/v1', transport: 'HTTP+JSON'}
190+
{url: 'http://localhost:41241/a2a/api', transport: 'HTTP+JSON'}
191191
],
192192
};
193193

@@ -207,15 +207,15 @@ async function main() {
207207

208208
// 4. Setup Express app with modular handlers
209209
const expressApp = express();
210-
211-
// Register agent card handler
210+
211+
// Register agent card handler at well-known location (shared by all transports)
212212
expressApp.use('/.well-known/agent-card.json', agentCardHandler({ agentCardProvider: requestHandler }));
213213

214-
// Register JSON-RPC handler at root
215-
expressApp.use('/', jsonRpcHandler({ requestHandler }));
214+
// Register JSON-RPC handler (preferred transport, backward compatible)
215+
expressApp.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler }));
216216

217-
// Register HTTP+REST handler at /v1
218-
expressApp.use('/v1', httpRestHandler({ requestHandler }));
217+
// Register HTTP+REST handler (new feature - additional transport)
218+
expressApp.use('/a2a/api', httpRestHandler({ requestHandler }));
219219

220220
// 5. Start the server
221221
const PORT = process.env.PORT || 41241;
@@ -225,8 +225,6 @@ async function main() {
225225
}
226226
console.log(`[SUTAgent] Server using new framework started on http://localhost:${PORT}`);
227227
console.log(`[SUTAgent] Agent Card: http://localhost:${PORT}/.well-known/agent-card.json`);
228-
console.log(`[SUTAgent] JSON-RPC endpoint: http://localhost:${PORT}/`);
229-
console.log(`[SUTAgent] HTTP+REST endpoint: http://localhost:${PORT}/v1`);
230228
console.log('[SUTAgent] Press Ctrl+C to stop the server');
231229
});
232230
}

test/server/http_rest_handler.spec.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import { A2ARequestHandler } from '../../src/server/request_handler/a2a_request_
99
import { AgentCard, Task, Message } from '../../src/types.js';
1010
import { A2AError } from '../../src/server/error.js';
1111

12+
/**
13+
* Test suite for httpRestHandler - HTTP+REST transport implementation
14+
*
15+
* This suite tests the REST API endpoints following the A2A specification:
16+
* - GET /v1/card - Agent card retrieval
17+
* - POST /v1/message:send - Send message (non-streaming)
18+
* - POST /v1/message:stream - Send message with SSE streaming
19+
* - GET /v1/tasks/:taskId - Get task status
20+
* - POST /v1/tasks/:taskId:cancel - Cancel task
21+
* - POST /v1/tasks/:taskId:subscribe - Resubscribe to task updates
22+
* - Push notification config CRUD operations
23+
*/
1224
describe('httpRestHandler', () => {
1325
let mockRequestHandler: A2ARequestHandler;
1426
let app: Express;
@@ -312,26 +324,26 @@ describe('httpRestHandler', () => {
312324
assert.deepEqual(response.body, mockConfig);
313325
});
314326

315-
it('should return 501 if push notifications not supported', async () => {
316-
// Create new app with handler that has capabilities without push notifications
317-
const noPNRequestHandler = {
318-
...mockRequestHandler,
319-
getAgentCard: sinon.stub().resolves({
320-
...testAgentCard,
321-
capabilities: { streaming: false, pushNotifications: false }
322-
})
323-
};
324-
const noPNApp = express();
325-
noPNApp.use(httpRestHandler({ requestHandler: noPNRequestHandler as any }));
326-
327-
const response = await request(noPNApp)
328-
.post('/v1/tasks/task-1/pushNotificationConfigs')
329-
.send({ url: 'https://example.com/webhook', events: ['message'] })
330-
.expect(501);
327+
it('should return 501 if push notifications not supported', async () => {
328+
// Create new app with handler that has capabilities without push notifications
329+
const noPNRequestHandler = {
330+
...mockRequestHandler,
331+
getAgentCard: sinon.stub().resolves({
332+
...testAgentCard,
333+
capabilities: { streaming: false, pushNotifications: false }
334+
})
335+
};
336+
const noPNApp = express();
337+
noPNApp.use(httpRestHandler({ requestHandler: noPNRequestHandler as any }));
338+
339+
const response = await request(noPNApp)
340+
.post('/v1/tasks/task-1/pushNotificationConfigs')
341+
.send({ url: 'https://example.com/webhook', events: ['message'] })
342+
.expect(501);
331343

332-
assert.property(response.body, 'code');
333-
assert.property(response.body, 'message');
334-
});
344+
assert.property(response.body, 'code');
345+
assert.property(response.body, 'message');
346+
});
335347
});
336348

337349
describe('GET /v1/tasks/:taskId/pushNotificationConfigs', () => {
@@ -420,6 +432,21 @@ describe('httpRestHandler', () => {
420432
.post('/v1/tasks/task-1:unknown')
421433
.expect(404);
422434
});
435+
436+
it('should handle internal server errors gracefully', async () => {
437+
(mockRequestHandler.sendMessage as SinonStub).rejects(
438+
new Error('Unexpected internal error')
439+
);
440+
441+
const response = await request(app)
442+
.post('/v1/message:send')
443+
.send({ message: testMessage })
444+
.expect(500);
445+
446+
assert.property(response.body, 'code');
447+
assert.property(response.body, 'message');
448+
assert.equal(response.body.code, -32603); // Internal error code
449+
});
423450
});
424451
});
425452

0 commit comments

Comments
 (0)