Implement SSE and Streamable HTTP protocols, fix CLI streaming
Summary
Makes callTool and callToolStreaming work for every protocol in the repo. Companion to universal-tool-calling-protocol/python-utcp#100.
CLI
callToolStreamingthrew on every call. It now runs the command and yields the full result as a single chunk, like the HTTP protocol. README limitation note updated.
Streamable HTTP and SSE were placeholder stubs. Both were registered in the package index but yielded a fake string such as "SSE event for tool: name" and never contacted the server. They are now real implementations ported from python-utcp:
- Path parameter substitution,
header_fields,body_field, remaining args as query params. - API key (header/query/cookie), Basic, OAuth2 client credentials with token caching, and OAuth2 user auth. Cookies are now actually sent; the old stubs computed them and dropped them.
- URL re-validation before each call,
redirect: 'error', timeout via AbortController (Streamable HTTP), and reader cancellation when the consumer stops early.
Streamable HTTP yields by response content type: NDJSON as one parsed object per line, JSON as a single value, everything else as Buffer chunks re-sliced to chunk_size. callTool concatenates binary chunks into one Buffer or returns parsed chunks as an array, matching Python.
SSE parses the wire format incrementally (comments, multi-line data, id, retry, CRLF, trailing unterminated event), JSON-parses each payload when possible, filters by event_type, and implements the reconnect / retry_timeout fields:
- When an established stream drops, wait
retry_timeoutms (overridden by a serverretry:field), reconnect withLast-Event-ID, give up afterMAX_RECONNECT_ATTEMPTS(5) per call. - A clean end of stream completes the call and never reconnects. Connection or HTTP errors on the initial request fail immediately.
Runtime note. Streaming fetches pass keepalive: false. Bun's fetch transparently re-issues a request when a reused pooled socket dies mid-response, even after part of the body arrived. That bypassed the reconnect logic (no Last-Event-ID, reconnect: false ignored) and could hand consumers partial chunks followed by a full replay. Opting streams out of the pool fixes it; Node ignores the option. Connection: close was considered and rejected because under Node it makes an abrupt disconnect look like a clean end of stream.
Test plan
- New
sse_communication_protocol.test.tsandstreamable_http_communication_protocol.test.tsagainst real express servers: discovery, NDJSON/JSON/binary re-chunking, POST bodies, header fields, path params, auth, error statuses, event filtering, CRLF, reconnect resume withLast-Event-ID,reconnect: false, attempt cap - New CLI single-chunk streaming test
-
bun test packages/http113 pass (3 runs),bun test packages/cli7 pass -
bun run buildin packages/http succeeds including DTS - Built package exercised under Node 22.17 and Bun 1.2.22 with keep-alive sockets in the pool: all reconnect scenarios pass on both
š¤ Generated with Claude Code
Summary by cubic
Makes callTool and callToolStreaming work for every protocol: CLI streaming previously threw and now runs the command to completion and yields the full result as a single chunk, while the SSE and Streamable HTTP protocols replace placeholder stubs that never contacted the server with real implementations. Both handle path/header/body/query argument mapping, API key/Basic/OAuth2/OAuth2-user auth with real cookie sending, and URL re-validation with redirect: 'error'; streaming fetches pass keepalive: false because Bun's fetch would otherwise transparently re-issue a request when a pooled socket dies mid-response, bypassing reconnect logic and replaying partial chunks.
SSE
- Parses the wire format incrementally (comments, multi-line data, id, retry, CRLF, trailing events), filters by
event_type, and JSON-parses payloads when possible. - Reconnects a dropped established stream with
Last-Event-IDafterretry_timeout(or the server'sretry:field), capped at 5 attempts per call; a clean end of stream completes the call and initial connection or HTTP errors fail immediately. - Bounds the handshake and OAuth2 token fetch to 30 s, caps the reconnect delay at 60 s, counts failed reconnect handshakes as attempts, and treats an over-buffered stream without a delimiter as a non-retryable protocol error; a CRLF split across reads no longer ends an event early.
Streamable HTTP
- Yields one parsed object per NDJSON line, a single parsed value for JSON, or Buffers re-sliced to
chunk_size;callToolconcatenates binary chunks or returns parsed chunks as an array. - One
AbortControllerdeadline covers the OAuth2 token fetch, request, and stream, and its timer is cleared even when URL/body construction throws; the reader is cancelled when the consumer stops early, andbody_fieldon GET is rejected up front even when supplied asundefined. - Caches OAuth2 tokens per token endpoint (not per
client_id) and validates the token URL before consulting it. - Substitutes every occurrence of
{param}and${param}path parameters.