Skip to content

Conversation

@alexbumbacea
Copy link

We have a use case for messages larger than 1MB, hence making this configurable would help.
In order to discover the source of our problems we had to spend some time investigating, as errors were silently discarded. So we also included changes to improving surfacing errors .

Copy link
Contributor

@findleyr findleyr left a comment

Choose a reason for hiding this comment

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

Let's discuss this change in #726. I'm not sure we shouldn't just remove the buffering.

@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from 91f93af to ee3d418 Compare December 24, 2025 21:59
@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from ee3d418 to 85a4340 Compare January 3, 2026 09:48
@alexbumbacea alexbumbacea changed the title mcp: improve http transports error handling and make buffer size configurable mcp: improve http transports error handling and make transport work with any size message Jan 3, 2026
@alexbumbacea
Copy link
Author

@findleyr current proposal (no fixed buffer size) approach is inline with most sdks. Can we get this merged?

@findleyr
Copy link
Contributor

findleyr commented Jan 9, 2026

@alexbumbacea sorry for the delay, will review tomorrow.

mcp/event.go Outdated
yield(Event{}, fmt.Errorf("context done: %w", ctx.Err()))
return
} else {
yield(Event{}, fmt.Errorf("error reading event: %w", err))
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to expose these errors in the API: we can just format them with %v.

Copy link
Author

Choose a reason for hiding this comment

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

Not clear why we don't wat to wrap it and instead we just want to print it.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you wrap an error, then callers can extract the error. That means the error type becomes part of your effective API. Which means you can't change it.

If you just include the error text, you don't have that problem. If later, someone needs to distinguish particular errors, we can design the proper API for them.

mcp/event.go Outdated
const maxTokenSize = 1 * 1024 * 1024 // 1 MiB max line size
scanner.Buffer(nil, maxTokenSize)
func scanEvents(ctx context.Context, r io.Reader) iter.Seq2[Event, error] {
scanner := bufio.NewReader(r)
Copy link
Contributor

Choose a reason for hiding this comment

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

'reader'

mcp/event.go Outdated
scanner := bufio.NewScanner(r)
const maxTokenSize = 1 * 1024 * 1024 // 1 MiB max line size
scanner.Buffer(nil, maxTokenSize)
func scanEvents(ctx context.Context, r io.Reader) iter.Seq2[Event, error] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we adding a ctx parameter? Does not appear to be used?

Copy link
Contributor

Choose a reason for hiding this comment

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

Used on L117 to stop the iteration on cancellation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ack, ok... but is that necessary? Wouldn't a context cancellation close the response body?

mcp/event.go Outdated
yield(Event{}, fmt.Errorf("context done: %w", ctx.Err()))
return
} else {
yield(Event{}, fmt.Errorf("error reading event: %w", err))
Copy link
Contributor

Choose a reason for hiding this comment

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

If you wrap an error, then callers can extract the error. That means the error type becomes part of your effective API. Which means you can't change it.

If you just include the error text, you don't have that problem. If later, someone needs to distinguish particular errors, we can design the proper API for them.

mcp/event.go Outdated
return true
}
for {
line, err := scanner.ReadBytes('\n')
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we think this is a potential DOS attack vector? True, the client is reading from the server, which it presumably trusts in many other ways. But a malicious server could make the client crash by feeding it a large event.

What about a high and configurable upper limit?

Copy link
Author

@alexbumbacea alexbumbacea Jan 12, 2026

Choose a reason for hiding this comment

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

it was already discussed in #726 that having this kind of limit would not protect against this in the event the server would present that as a multi "data" event.

Looking into other implementations of the MCP client (other languages and even other written in go) they do not impose such a limitation.

mcp/event.go Outdated
scanner := bufio.NewScanner(r)
const maxTokenSize = 1 * 1024 * 1024 // 1 MiB max line size
scanner.Buffer(nil, maxTokenSize)
func scanEvents(ctx context.Context, r io.Reader) iter.Seq2[Event, error] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Used on L117 to stop the iteration on cancellation.

@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from 85a4340 to 7ba1c9a Compare January 12, 2026 10:44
@alexbumbacea
Copy link
Author

@findleyr all comments have been either replied to or addressed. are there any other concerns?

mcp/event.go Outdated
if !bytes.Equal(before, dataKey) {
flushData()
// If we're starting a new event field and we already have an event, emit it first
if bytes.Equal(before, eventKey) && !evt.Empty() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't look right: the event: field does not need to be the first field in the event. Maybe add a failing test for this.

Copy link
Author

Choose a reason for hiding this comment

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

removed and added tests the scenario you mentioned

mcp/event.go Outdated
dataBuf *bytes.Buffer // if non-nil, preceding field was also data
)
flushData := func() {
emitEvent := func() bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

s/emitEvent/yieldEvent

mcp/event.go Outdated
scanner := bufio.NewScanner(r)
const maxTokenSize = 1 * 1024 * 1024 // 1 MiB max line size
scanner.Buffer(nil, maxTokenSize)
func scanEvents(ctx context.Context, r io.Reader) iter.Seq2[Event, error] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ack, ok... but is that necessary? Wouldn't a context cancellation close the response body?

@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch 2 times, most recently from 3577e96 to 377a108 Compare January 14, 2026 07:19
@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from 377a108 to 8f634ce Compare January 15, 2026 10:28
@alexbumbacea
Copy link
Author

@findleyr is there anything else?

@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from 8f634ce to 9fc710b Compare January 20, 2026 13:42
@alexbumbacea
Copy link
Author

@findleyr can we get this merged?

@findleyr
Copy link
Contributor

@alexbumbacea very sorry, I thought I had merged this. Total oversight on my part.

@maciej-kisiel we may want to cherry-pick this into the release, or do a second prerelease.


case data := <-c.incoming:
case m := <-c.incoming:
if m.err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, but returning (nil, nil) here is not correct. Previously the code would simply not receive the message: now it's reading a nil message, which is almost certain to cause a nil pointer exception.

I think we should simply not surface this error to the JSON-RPC layer.

Copy link
Author

@alexbumbacea alexbumbacea Jan 28, 2026

Choose a reason for hiding this comment

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

removed the return here, just a TODO comment to have track of the idea that it needs to be handled

Copy link
Contributor

Choose a reason for hiding this comment

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

This is still not right: we'd proceed to process the nil message.

Copy link
Author

Choose a reason for hiding this comment

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

not surfacing the error at all, would require even more extensive refactoring. the best I can think of it's to wrap the error in a jsonrpc.Response

// Network errors during reading should trigger reconnection, not permanent failure.
// Return from processStream so handleSSE can attempt to reconnect.
c.logger.Debug(fmt.Sprintf("%s: stream read error (will attempt reconnect): %v", requestSummary, err))
return lastEventID, reconnectDelay, false
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, I think we actually need to be more subtle than this: a malformed event is a hard error, and different from a reader error.

Copy link
Author

Choose a reason for hiding this comment

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

did some adaptaptation. let me know WDYT

@findleyr
Copy link
Contributor

Apologies again for this getting lost in the weeds: I think there are a few things that need to be addressed before merging.


case data := <-c.incoming:
case m := <-c.incoming:
if m.err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is still not right: we'd proceed to process the nil message.

@alexbumbacea alexbumbacea force-pushed the handling-large-messages-and-surface-errors branch from da1ac52 to d534f57 Compare January 28, 2026 17:04
@alexbumbacea alexbumbacea requested a review from findleyr January 28, 2026 17:06
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.

3 participants