Skip to content

Fix streaming mode for CLI, MCP, TCP protocols and add SSE reconnection - #100

Merged
h3xxit merged 4 commits into
devfrom
fix/streaming-single-chunk-and-sse-reconnect
Sep 4, 2026
Merged

Fix streaming mode for CLI, MCP, TCP protocols and add SSE reconnection#100
h3xxit merged 4 commits into
devfrom
fix/streaming-single-chunk-and-sse-reconnect

Conversation

@h3xxit

@h3xxit h3xxit commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

call_tool_streaming failed for several protocols instead of emitting the full result as a single chunk the way the HTTP protocol does. This PR fixes those and implements the SSE reconnection that the call template already advertised.

Streaming fixes

  • CLI: raised NotImplementedError on every streaming call. Now runs the command and yields the result once.
  • MCP: yielded the un-awaited coroutine instead of the result.
  • TCP: was a plain coroutine returning a generator, so the client's async for failed with a TypeError. UDP gets the same shape and return annotation.

SSE

  • reconnect / retry_timeout were accepted but never acted on. When an established stream drops, the client now waits retry_timeout ms (overridden by any retry: field from the server), reconnects with the Last-Event-ID header, and gives up after MAX_RECONNECT_ATTEMPTS (5) per call so a tool call can never hang forever. A clean end of stream completes the call and never reconnects. Connection or HTTP errors on the initial request still fail immediately, so a wrong URL does not trigger retries. The redirect refusal from the security hardening applies to every attempt.
  • The parser handles CRLF line endings and a trailing event without a terminating blank line.

Already correct and untouched: file, text, gql, http, streamable_http, websocket.

Test plan

  • New tests: single-chunk streaming for CLI, MCP, TCP, UDP
  • New tests: SSE reconnect resumes with Last-Event-ID; reconnect=False raises after partial events; attempt cap is honored
  • pytest over the cli, socket, mcp and http plugin test suites on top of current dev: 288 passed, 9 skipped

Companion PR: universal-tool-calling-protocol/typescript-utcp#41 brings the TypeScript implementation to parity.

🤖 Generated with Claude Code


Summary by cubic

Fixes streaming tool calls for CLI, MCP, TCP, and UDP, which previously failed or yielded the wrong object instead of emitting the full result as one chunk. Also implements and hardens SSE reconnection, which the call template advertised but never performed, and pins the MCP plugin to mcp 1.x.

Bug Fixes

  • CLI now runs the command and yields the result once instead of raising NotImplementedError.
  • MCP now awaits the tool call before yielding it.
  • TCP streaming is now an async generator; UDP gets the same shape and AsyncGenerator return annotation.
  • Pins mcp to <2; version 2.x removed FastMCP and McpError, which broke test collection in CI.

New Features

  • When an established SSE stream drops, the client waits retry_timeout ms (or the server's retry: value), reconnects with Last-Event-ID, and stops after 5 attempts.
  • A clean end of stream completes the call without reconnecting; initial connection and HTTP errors still fail immediately, and redirects remain refused.
  • Reconnect handshake failures are retried (only the initial handshake fails fast), and the reconnect delay is capped at 60 s.
  • The handshake is bounded by a 30-second timeout so a silent server cannot hang the call.
  • The parser handles CRLF splits across chunks, multi-byte UTF-8, and trailing unterminated events; events over 16 MiB without a delimiter raise SseProtocolError, which is never retried.

Written for commit d86d312. Summary will update on new commits.

Review in cubic

call_tool_streaming failed for several protocols instead of emitting the
full result as a single chunk like the HTTP protocol does:

- CLI raised NotImplementedError on every streaming call.
- MCP yielded the un-awaited coroutine instead of the result.
- TCP was a plain coroutine returning a generator, so the client's
  `async for` failed. UDP gets the same shape and type annotation.

SSE improvements:

- Implement `reconnect` / `retry_timeout`, which were accepted but never
  acted on. When an established stream drops, reconnect after
  `retry_timeout` (or the server's `retry:` value) with `Last-Event-ID`,
  capped at MAX_RECONNECT_ATTEMPTS per call. A clean end of stream
  completes the call; connection or HTTP errors on the initial request
  still fail immediately. Redirects stay refused on every attempt.
- Handle CRLF line endings and a trailing unterminated event.

Tests added for every fixed protocol and for the SSE reconnect paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py Outdated
Comment thread plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py Outdated
Comment thread plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py Outdated
mcp 2.x removed `mcp.server.fastmcp.FastMCP` and
`mcp.shared.exceptions.McpError`, which the MCP test mocks import. CI
installs the newest mcp, so every job failed at collection with mcp
2.1.1 (the last green run on dev predates the mcp 2 release). The plugin
targets the 1.x API; a fresh install with the pin resolves to mcp 1.29.1
and the MCP suite passes. Migrating to mcp 2 is a separate task.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@h3xxit

h3xxit commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

CI was failing at test collection, not on any test: the runner installed mcp 2.1.1, which removed mcp.server.fastmcp.FastMCP and mcp.shared.exceptions.McpError that the MCP test mocks import. The last green run on dev predates the mcp 2 release, so any PR would have hit this. Pushed a pin to mcp>=1.12,<2 in the plugin's pyproject; a fresh install resolves to mcp 1.29.1 and the MCP suite passes. Migrating the plugin to mcp 2 should be its own PR.

…ys, fix CRLF framing

Addresses cubic review on #100:

- The handshake (until response headers arrive) is bounded by
  HANDSHAKE_TIMEOUT_SECONDS (30 s) via asyncio.wait_for, so a server that
  accepts the connection but never answers cannot hang the call. Reading
  the body stays unbounded: an SSE stream may legitimately be quiet.
- A reconnect handshake that fails (refused, 503, timeout) now counts as
  one attempt and is retried; only the initial handshake fails fast.
- The reconnect delay is capped at MAX_RECONNECT_DELAY_MS (60 s) whatever
  retry_timeout or a server-sent retry: field asks for, so the attempt
  cap actually bounds the total wait.
- A CRLF split across two chunks no longer becomes two LFs and ends the
  event early: a trailing CR is held until the next chunk. Decoding is
  now incremental too, so a multi-byte UTF-8 character straddling chunks
  no longer raises.
- An event that exceeds MAX_EVENT_BUFFER_CHARS (16 Mi) without a
  blank-line delimiter raises SseProtocolError, which is never retried.

Tests for each of the above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Count connections in the no-delimiter handler and assert exactly one, so
the test actually verifies that SseProtocolError bypasses the reconnect
path instead of relying on the error being re-raised on a retry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@h3xxit
h3xxit merged commit 228d0a3 into dev Sep 4, 2026
10 checks passed
@h3xxit
h3xxit deleted the fix/streaming-single-chunk-and-sse-reconnect branch September 4, 2026 13:31
h3xxit added a commit that referenced this pull request Sep 4, 2026
Both #100 and this branch appended tests to test_mcp_transport.py; keep
both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant