Skip to content

HTTP transport: bare close(timer)/flush(sse_stream) resolve to the package's Transport-only methods, breaking SSE delivery #71

Description

@kalidke

Two calls in src/transports/http.jl invoke close/flush unqualified, so inside the module they resolve to the package's own Transport-only methods rather than Base, and throw MethodError at runtime. Both are swallowed by an enclosing catch, so they are invisible in normal operation.

The two sites

src/transports/http.jl:242 — in the SSE loop:

timer = Timer(0.1)  # 100ms timeout
while !istaskdone(t) && isopen(timer)
    ...
end
close(timer)        # <-- Timer, not a Transport

MethodError(ModelContextProtocol.close, (Timer(closed, timeout: 0.1 s),), ...)

src/transports/http.jl:546 — writing a response to an SSE stream:

write(sse_stream, event)
flush(sse_stream)   # <-- HTTP.Streams.Stream, not a Transport

MethodError(ModelContextProtocol.flush, (HTTP.Streams.Stream{...},), ...)

The shadowing methods are flush(transport::Transport) (src/transports/base.jl:201), close(transport::HttpTransport) (http.jl:679), and the stdio equivalents.

Why it matters

The close(timer) throw happens inside the SSE stream loop, so it propagates to the outer handler, which logs "SSE stream closed" and runs a finally that deletes the stream from transport.sse_streams. That suggests server→client notification delivery over HTTP SSE is broken — progress notifications, notifications/tasks/status, and logging notifications all ride that path, and all are listed as implemented with "transport-correct delivery (stdio / SSE)".

Caveat on scope of evidence: I have verified that both MethodErrors occur and traced the control flow, but I have not proven end-to-end that no SSE notification is ever delivered. That should be confirmed as part of the fix.

How it surfaced

Observed while dry-running the rebuilt Inspector workflow (#70) against examples/simple_http_server.jl — the errors appear as level: info MCP log notifications on stderr:

"message":"Failed to write to SSE stream","error":"MethodError(ModelContextProtocol.flush, (HTTP.Streams.Stream{...
"message":"SSE stream closed","error":"MethodError(ModelContextProtocol.close, (Timer (closed, timeout: 0.1 s)...

They reproduce on main (a2508c9) and are unrelated to the Inspector change.

Fix

Qualify both as Base.close(timer) / Base.flush(sse_stream). Note http.jl:264 in the same file already does this correctly (Base.flush(stream)), and CLAUDE.md documents the footgun:

Use Base.flush() explicitly for HTTP streams to avoid naming conflicts

Worth adding a regression test that asserts an SSE notification actually arrives at a connected client, since the suite is currently green (935/935) with these paths throwing — and worth grepping for any other unqualified Base calls shadowed by the package's exports.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions