Implement SSE and Streamable HTTP protocols, fix CLI streaming

Merged
H3
1mo ago
+1651-99

Summary

Makes callTool and callToolStreaming work for every protocol in the repo. Companion to universal-tool-calling-protocol/python-utcp#100.

CLI

  • callToolStreaming threw 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_timeout ms (overridden by a server retry: field), reconnect with Last-Event-ID, give up after MAX_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.ts and streamable_http_communication_protocol.test.ts against real express servers: discovery, NDJSON/JSON/binary re-chunking, POST bodies, header fields, path params, auth, error statuses, event filtering, CRLF, reconnect resume with Last-Event-ID, reconnect: false, attempt cap
  • New CLI single-chunk streaming test
  • bun test packages/http 113 pass (3 runs), bun test packages/cli 7 pass
  • bun run build in 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-ID after retry_timeout (or the server's retry: 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; callTool concatenates binary chunks or returns parsed chunks as an array.
  • One AbortController deadline 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, and body_field on GET is rejected up front even when supplied as undefined.
  • 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.