Skip to content

feat: client-side streamable-http transport supports continuously listening #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 28, 2025

Conversation

leavez
Copy link
Contributor

@leavez leavez commented May 21, 2025

Description

Implemented for client-side streamable-http transport

  • option to listen continuously for server notifications with a long-live GET connection (missing feature from spec)
  • and a logger option

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

MCP Spec Compliance

  • This PR implements a feature defined in the MCP specification
  • Link to relevant spec section: Link text
  • Implementation follows the specification exactly

Summary by CodeRabbit

  • New Features

    • Introduced support for continuous server-to-client notifications using a persistent connection, enabling real-time updates without repeated polling.
    • Added options for enabling continuous listening and enhanced logging.
  • Bug Fixes

    • Improved error handling for session termination and unsupported server features.
  • Tests

    • Added comprehensive tests to verify continuous listening functionality and error scenarios.
    • Enhanced concurrency safety and coverage for notification handling in HTTP client tests.
  • Documentation

    • Clarified example client usage with comments on enabling continuous listening to receive global notifications.

Copy link
Contributor

coderabbitai bot commented May 21, 2025

## Walkthrough

This update enhances the StreamableHTTP transport by introducing continuous server-to-client notifications using a persistent HTTP GET connection, controlled via a new option. The HTTP request logic is unified, error handling is improved, and new logger support is added. Comprehensive tests and a mock server verify the new listening feature and error scenarios.

## Changes

| File(s)                                   | Change Summary                                                                                                                                     |
|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| client/transport/streamable_http.go      | Added continuous listening via persistent GET connection, unified HTTP request sending logic, improved error handling, added logger support, and updated struct and methods accordingly. |
| client/transport/streamable_http_test.go | Added mock HTTP server with GET support, new tests for continuous listening and handling of unsupported GET method, and a test logger for capturing logs. |
| client/http_test.go                       | Added thread-safe SafeMap for notification counting, refactored tests to verify global notification receipt with continuous listening, and improved concurrency safety in tests. |
| examples/simple_client/main.go            | Added explanatory comments about the default transport behavior and how to enable continuous listening to receive server notifications.             |

## Possibly related PRs

- [mark3labs/mcp-go#168](https://github.com/mark3labs/mcp-go/pull/168): Introduced the initial StreamableHTTP client implementation, which is directly extended and refactored in this PR to support continuous server-to-client notifications.

## Suggested labels

`type: enhancement`

## Suggested reviewers

- dugenkui03  
- pottekkat

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (1.64.8)

Error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2
Failed executing command with error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dc57e6 and 4e9411d.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
client/transport/streamable_http.go (2)
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
🔇 Additional comments (17)
client/transport/streamable_http.go (17)

20-20: LGTM: Import addition for logger support

The util import is appropriately added to support the new logging functionality.


25-36: LGTM: Well-documented continuous listening option

The function provides clear documentation, appropriate warnings about server compatibility, and correctly implements the feature flag.


71-75: LGTM: Standard option function implementation

The logger option follows the established pattern and is correctly implemented.


91-110: LGTM: Appropriate struct field additions

The new fields support the continuous listening feature properly. Good use of sync.Once for the initialized channel to prevent race conditions.


120-127: LGTM: Proper initialization of new fields

The constructor correctly initializes the logger with a default and creates the initialized channel.


147-158: LGTM: Proper goroutine lifecycle management

The implementation correctly addresses potential goroutine leaks by using select to handle both initialization completion and client closure. The context-aware approach ensures proper cleanup.


183-183: LGTM: Enhanced error logging for debugging

The addition of error logging improves debugging capabilities without affecting the core functionality.


232-244: LGTM: Improved request handling with proper error management

The refactoring to use sendHTTP improves code organization. The special handling for initialize requests with session termination errors is appropriate to distinguish genuine routing issues.


273-275: LGTM: Proper channel lifecycle management

The use of sync.Once ensures the initialized channel is closed exactly once, addressing the concern from previous reviews about potential deadlocks.


367-367: LGTM: Clear parameter addition for continuous listening

The ignoreResponse parameter is well-documented and enables the method to support both request-response and continuous listening scenarios.


386-386: LGTM: Enhanced error logging throughout SSE handling

The addition of structured error logging improves debugging capabilities for SSE stream processing issues.


405-407: LGTM: Appropriate response handling for continuous listening

The conditional response handling correctly supports continuous listening by only processing responses when required.


484-487: LGTM: Consistent refactoring to use centralized HTTP logic

The refactoring improves consistency and benefits from the centralized error handling and request management in sendHTTP.


532-553: LGTM: Robust continuous listening implementation

The method handles various error scenarios appropriately:

  • Gracefully exits when server doesn't support GET listening
  • Implements retry logic with proper context cancellation handling
  • Provides informative logging for debugging

The retry interval being a variable is convenient for testing.


555-560: LGTM: Well-defined error constants and configurable retry

The specific error types improve error handling clarity, and making the retry interval a variable is good for testability.


562-597: LGTM: Well-implemented GET connection establishment

The method properly handles the GET request setup and validation. The detailed comments about response handling limitations (lines 586-590) honestly acknowledge potential compatibility issues with other SDKs while explaining the current design constraints.

The graceful handling of 405 Method Not Allowed is appropriate for servers that don't support the listening feature.


599-611: LGTM: Useful context management helper

The helper properly creates a context that respects both the original context cancellation and client closure. The goroutine will terminate appropriately when either condition occurs.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
client/transport/streamable_http.go (1)

202-210: ⚠️ Potential issue

Guard against double-closing initialized channel

close(c.initialized) is executed each time an initialize request succeeds.
A second initialize (or a retry after a transient 50x) will panic because a closed
channel cannot be closed again.

-       close(c.initialized)
+       // close only once
+       select {
+       case <-c.initialized: // already closed – nothing to do
+       default:
+               close(c.initialized)
+       }
🧹 Nitpick comments (3)
client/transport/streamable_http.go (3)

258-266: Avoid setting Content-Type on GET requests

The Content-Type header is meaningless (and sometimes confusing for proxies)
when the request has no body. It is safer to omit it for GET.

- req.Header.Set("Content-Type", "application/json")
+ if method != http.MethodGet {
+     req.Header.Set("Content-Type", "application/json")
+ }

469-476: resp.Body is closed twice

createGETConnectionToServer defers resp.Body.Close() and passes the same
reader to handleSSEResponse, which in turn closes it in readSSE. A double
close is harmless but unnecessary and can mislead future maintainers.

- defer resp.Body.Close()
+ // readSSE will close resp.Body for us

57-61: Nil-logger safety

WithLogger directly assigns the provided logger. If a caller passes nil
the transport will panic on first call. Either document the contract or add a
nil-fallback:

return func(sc *StreamableHTTP) {
-       sc.logger = logger
+       if logger != nil {
+               sc.logger = logger
+       }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between d37791c and 6435882.

📒 Files selected for processing (2)
  • client/transport/streamable_http.go (11 hunks)
  • client/transport/streamable_http_test.go (2 hunks)
🔇 Additional comments (1)
client/transport/streamable_http_test.go (1)

563-569: Dead code: returned notificationCount is never used

The helper returns notificationCount but none of the tests read the value.
Drop the return or use it; otherwise it misleads readers and linters.

[ suggest_nitpick ]

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
client/transport/streamable_http.go (1)

292-295: Differentiate 404 "session terminated" from generic 404

Blindly treating every 404 as "session terminated" can hide genuine endpoint-routing issues. Consider checking a sentinel header or body message returned by the server before wiping the session id.

🧹 Nitpick comments (4)
client/transport/streamable_http.go (4)

445-466: Consider specific handling for session termination in listenForever

The listenForever method doesn't specifically check for the errSessionTerminated error, which might lead to unnecessary retries when the session is actually terminated.

func (c *StreamableHTTP) listenForever() {
	c.logger.Infof("listening to server forever")
	for {
		err := c.createGETConnectionToServer()
		if errors.Is(err, errGetMethodNotAllowed) {
			// server does not support listening
			c.logger.Errorf("server does not support listening")
			return
		}
+		if errors.Is(err, errSessionTerminated) {
+			// Session was terminated, no need to retry until reinitialization
+			c.logger.Errorf("session terminated, waiting for reinitialization")
+			// Wait for reinitialization signal
+			select {
+			case <-c.closed:
+				return
+			case <-c.initialized: // This would require resetting and recreating this channel on reinitialization
+				c.logger.Infof("session reinitialized, resuming listening")
+				continue
+			}
+		}

		select {
		case <-c.closed:
			return
		default:
		}

		if err != nil {
			c.logger.Errorf("failed to listen to server. retry in 1 second: %v", err)
		}
		time.Sleep(retryInterval)
	}
}

472-473: Consider making retryInterval configurable

The retryInterval variable is used for testing convenience, but it's not exposed for configuration. Consider making it configurable via an option function to allow users to adjust the retry behavior.

+// WithRetryInterval sets the interval between retries for continuous listening.
+func WithRetryInterval(interval time.Duration) StreamableHTTPCOption {
+	return func(sc *StreamableHTTP) {
+		sc.retryInterval = interval
+	}
+}

type StreamableHTTP struct {
	baseURL             *url.URL
	httpClient          *http.Client
	headers             map[string]string
	headerFunc          HTTPHeaderFunc
	logger              util.Logger
	getListeningEnabled bool
+	retryInterval       time.Duration

	initialized chan struct{}
	sessionID   atomic.Value // string

	notificationHandler func(mcp.JSONRPCNotification)
	notifyMu            sync.RWMutex

	closed chan struct{}
}

// In NewStreamableHTTP
smc := &StreamableHTTP{
	baseURL:      parsedURL,
	httpClient:   &http.Client{},
	headers:      make(map[string]string),
	closed:       make(chan struct{}),
	logger:       util.DefaultLogger(),
	initialized:  make(chan struct{}),
+	retryInterval: 1 * time.Second,
}

// In listenForever
time.Sleep(c.retryInterval)

477-478: Consider using a context with timeout for GET requests

The code creates a background context without timeout for GET requests. Consider using a context with timeout to prevent overly long-running requests if there are network issues.

-ctx := context.Background() // the sendHTTP will be automatically canceled when the client is closed
+ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // adjust timeout as needed
+defer cancel() // ensure resources are cleaned up
resp, err := c.sendHTTP(ctx, http.MethodGet, nil, "text/event-stream")

151-158: Use logger instead of fmt.Printf for error messages

There are several instances of fmt.Printf in the code that should be replaced with the logger for consistency. This includes lines 151, 157, 321, 329, and 382.

-fmt.Printf("failed to create close request\n: %v", err)
+c.logger.Errorf("failed to create close request: %v", err)

-fmt.Printf("failed to send close request\n: %v", err)
+c.logger.Errorf("failed to send close request: %v", err)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3f236 and c0f4403.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (11 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
client/transport/streamable_http.go (4)
server/streamable_http.go (1)
  • WithLogger (77-81)
util/logger.go (2)
  • Logger (8-11)
  • DefaultLogger (16-20)
client/transport/interface.go (1)
  • HTTPHeaderFunc (13-13)
mcp/types.go (1)
  • MethodInitialize (19-19)
🔇 Additional comments (7)
client/transport/streamable_http.go (7)

25-36: Well-documented option for continuous listening

The WithContinuousListening option is clearly documented with references to the MCP specification. This makes it easy for users to understand the purpose and behavior of this feature.


57-61: Good addition of logger support

Adding logger support is a good practice for this type of component, especially for a feature that involves long-lived connections and retry logic.


120-126: Clean implementation of continuous listening initialization

The goroutine that waits for initialization before starting the continuous listening process is a good approach. This ensures the session ID is established before attempting to listen for server notifications.


244-298: Good refactoring of HTTP request handling

The sendHTTP method nicely centralizes the common HTTP request creation and sending logic that was previously duplicated in SendRequest and SendNotification. This improves maintainability and ensures consistent behavior.


186-192: Good handling of initialize request failures

The special handling for 404 errors during initialization is a good addition. It correctly distinguishes between genuine routing issues and session termination errors.


468-473: Clear error definitions

The error definitions are clear and help distinguish between different error conditions. This makes the error handling logic more readable and maintainable.


1-507: Overall excellent implementation of continuous listening feature

The implementation of the continuous listening feature is well-designed and follows good practices:

  1. It correctly implements the MCP specification for listening for server messages
  2. It handles errors and retries appropriately
  3. It provides clear configuration options
  4. It includes proper logging
  5. It refactors common code to reduce duplication

There are a few minor improvements suggested, but overall this is a solid implementation of the feature.

@leavez leavez changed the title Feat: client-side streamable-http transport support continuously listening Feat: client-side streamable-http transport supports continuously listening May 21, 2025
@leavez leavez changed the title Feat: client-side streamable-http transport supports continuously listening feat: client-side streamable-http transport supports continuously listening May 22, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
client/transport/streamable_http.go (1)

284-354: Addresses previous 404 handling concern with improved logic

The sendHTTP method centralizes request handling and addresses the previous review comment about 404 handling. The universal 404 handling clears session ID, but the caller (SendRequest) has special logic for initialization requests to distinguish routing issues from session termination.

🧹 Nitpick comments (2)
client/transport/streamable_http.go (2)

347-351: Session ID race condition in CompareAndSwap

The CompareAndSwap operation could fail if the session ID changed between reading it (line 312) and this comparison, potentially leaving a stale session ID. Consider using a more robust approach.

 	// universal handling for session terminated
 	if resp.StatusCode == http.StatusNotFound {
-		c.sessionID.CompareAndSwap(sessionID, "")
+		c.sessionID.Store("")
 		return nil, errSessionTerminated
 	}

548-579: Consider context handling in long-lived connection

Using context.Background() means the GET connection won't be canceled by request-specific contexts, which is appropriate for a long-lived listening connection. However, consider if this could lead to resource leaks.

The comment on line 550 could be more specific:

-	ctx := context.Background() // the sendHTTP will be automatically canceled when the client is closed
+	ctx := context.Background() // sendHTTP will cancel when client.closed is triggered
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc540bb and d7ddec1.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (11 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
client/transport/streamable_http.go (6)
server/streamable_http.go (1)
  • WithLogger (77-81)
util/logger.go (2)
  • Logger (8-11)
  • DefaultLogger (16-20)
client/client.go (1)
  • Client (16-25)
client/transport/interface.go (1)
  • HTTPHeaderFunc (13-13)
mcp/types.go (1)
  • MethodInitialize (19-19)
client/oauth.go (1)
  • OAuthAuthorizationRequiredError (48-48)
🔇 Additional comments (7)
client/transport/streamable_http.go (7)

20-20: LGTM: Appropriate import addition

The util package import is correctly added to support the new logger functionality.


25-36: Well-documented continuous listening option

The option is properly documented with clear explanation of functionality and links to the MCP specification. The warning about server support is helpful.


57-61: Standard logger option implementation

Follows the same pattern as the server-side WithLogger option and properly integrates with the struct.


116-117: Good default logger initialization

Properly initializes the logger with a default and creates the coordination channel.


473-473: Good refactoring to use centralized HTTP handling

The SendNotification method correctly uses the new sendHTTP method for consistency.


518-539: Robust retry logic with proper error handling

The continuous listening implementation properly handles different error scenarios and includes appropriate logging. The check for method not allowed prevents infinite retries when server doesn't support GET.


541-546: Well-defined error constants

Clear error definitions with a configurable retry interval for testing flexibility.

@dugenkui03 dugenkui03 added the area: mcp spec Issues related to MCP specification compliance label May 29, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
client/transport/streamable_http.go (1)

385-386: 🛠️ Refactor suggestion

Inconsistent error logging - use structured logger.

Several locations use fmt.Printf for error logging instead of the configured logger, which bypasses the logging infrastructure.

Replace fmt.Printf calls with the structured logger:

-			fmt.Printf("failed to unmarshal message: %v\n", err)
+			c.logger.Errorf("failed to unmarshal message: %v", err)
-				fmt.Printf("failed to unmarshal notification: %v\n", err)
+				c.logger.Errorf("failed to unmarshal notification: %v", err)
-					fmt.Printf("SSE stream error: %v\n", err)
+					c.logger.Errorf("SSE stream error: %v", err)

Also applies to: 393-394, 446-447

🧹 Nitpick comments (2)
client/transport/streamable_http.go (2)

292-362: Excellent refactoring with minor context leak concern.

The sendHTTP method effectively consolidates HTTP request logic, eliminating duplication. However, there's a potential goroutine leak in the context cancellation logic.

In lines 301-308, the goroutine for context cancellation may not be cleaned up if the HTTP request completes quickly. Consider this pattern instead:

-	go func() {
-		select {
-		case <-c.closed:
-			cancel()
-		case <-newCtx.Done():
-			// The original context was canceled, no need to do anything
-		}
-	}()
+	go func() {
+		defer cancel()
+		select {
+		case <-c.closed:
+		case <-newCtx.Done():
+		}
+	}()

This ensures the cancel function is always called, cleaning up resources properly.


549-554: Consider making retry interval configurable.

The hardcoded 1-second retry interval may not be suitable for all deployment scenarios.

Consider adding a configuration option for the retry interval:

+func WithRetryInterval(interval time.Duration) StreamableHTTPCOption {
+	return func(sc *StreamableHTTP) {
+		sc.retryInterval = interval
+	}
+}

And add a retryInterval field to the struct with a default value.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42ba0ff and 928d9ea.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (10 hunks)
🔇 Additional comments (11)
client/transport/streamable_http.go (11)

20-20: Import addition looks good.

The addition of the util package import supports the new logger functionality.


25-36: Well-documented continuous listening option.

The function documentation clearly explains the feature and includes relevant specification links. Good defensive notice about server support variability.


57-61: Logger option implementation is correct.

Simple and straightforward option function for logger configuration.


88-94: Good synchronization design for initialization coordination.

The combination of initialized channel and initializedOnce provides proper one-time signaling without risk of double-close panics.


118-119: Proper initialization of new fields.

Default logger and initialized channel are correctly set up in the constructor.


139-149: Excellent fix for goroutine leak prevention.

The select statement properly handles both initialization completion and client closure, addressing the previous review concern about potential goroutine leaks.


223-232: Good differentiation of 404 errors during initialization.

This addresses the previous review feedback by treating 404 errors during initialization as routing issues rather than session termination, which is the correct behavior.


261-263: Proper use of sync.Once for channel coordination.

The initializedOnce.Do() ensures the channel is closed exactly once, preventing panics and addressing previous review concerns about missing channel closure on error paths.


481-481: Good refactoring to use common HTTP method.

The use of sendHTTP eliminates code duplication and ensures consistent behavior across request types.


526-547: Robust listening implementation with proper error handling.

The retry logic and graceful handling of unsupported servers (405 Method Not Allowed) is well-implemented. The infinite loop with proper exit conditions is correct.


556-587: Well-structured GET connection handling.

The method properly handles different response scenarios including method not allowed, status code validation, and content type verification. The error wrapping provides good debugging context.

}

// Start the transport - this will launch listenForever in a goroutine
if err := trans.Start(context.Background()); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is the SendRequest#initialize method called before Start()?

Copy link
Contributor Author

@leavez leavez May 29, 2025

Choose a reason for hiding this comment

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

I've fixed this.

It will not affect the test results, as start in http transport is doing nothing for sending

@dugenkui03
Copy link
Collaborator

@leavez Please let me know when this PR is ready, and I'd like to review it.

Also, it will be great If you can provide some examples in /examples or create a http_test.go in client, to demonstrate how to use this capability with client.Client

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
client/transport/streamable_http.go (2)

230-241: ⚠️ Potential issue

Ensure initialized channel is closed on all initialization paths

While sync.Once prevents double-closing, the channel is only closed on successful initialization. If the initialize request fails (e.g., error from sendHTTP or non-OK status), the listening goroutine will block indefinitely.

Consider closing the channel in a defer or ensuring it's closed on error paths:

 func (c *StreamableHTTP) SendRequest(
 	ctx context.Context,
 	request JSONRPCRequest,
 ) (*JSONRPCResponse, error) {
+	// Ensure initialized is closed for initialize requests
+	if request.Method == string(mcp.MethodInitialize) {
+		defer c.initializedOnce.Do(func() {
+			close(c.initialized)
+		})
+	}
 
 	// Marshal request
 	requestBody, err := json.Marshal(request)

Also applies to: 264-274


354-357: 🛠️ Refactor suggestion

Differentiate session termination from routing errors

The code still treats every 404 as session termination, which could mask genuine endpoint routing issues. Consider checking for a sentinel header or specific response body to confirm session termination.

 // universal handling for session terminated
 if resp.StatusCode == http.StatusNotFound {
-    c.sessionID.CompareAndSwap(sessionID, "")
-    return nil, errSessionTerminated
+    // Check if this is actually a session termination
+    if resp.Header.Get("X-Session-Terminated") == "true" {
+        c.sessionID.CompareAndSwap(sessionID, "")
+        return nil, errSessionTerminated
+    }
+    // Otherwise, let the caller handle the 404
 }
🧹 Nitpick comments (3)
client/transport/streamable_http.go (1)

592-604: Consider goroutine lifecycle for frequent calls

While this helper ensures proper cancellation, each call spawns a goroutine that lives until context cancellation. For high-frequency operations, consider reusing a single monitoring goroutine or using context.WithCancelCause.

client/http_test.go (2)

85-192: Comprehensive test coverage for continuous listening

The tests effectively verify both the default behavior and the continuous listening feature. The subtest structure improves readability and test isolation.

Consider using a helper function to wait for notifications with timeout instead of fixed sleep durations:

func waitForNotifications(t *testing.T, notificationNum *SafeMap, method string, expected int, timeout time.Duration) {
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        if notificationNum.Get(method) == expected {
            return
        }
        time.Sleep(10 * time.Millisecond)
    }
    t.Errorf("Timeout waiting for %d %s notifications, got %d", expected, method, notificationNum.Get(method))
}

15-16: Remove or complete the incomplete comment

The comment for SafeMap is incomplete and should be either finished or removed.

-// SafeMap is a thread-safe map wrapper
-
+// SafeMap is a thread-safe map wrapper for counting notifications
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f5efb5 and f8b7dce.

📒 Files selected for processing (2)
  • client/http_test.go (3 hunks)
  • client/transport/streamable_http.go (11 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
client/transport/streamable_http.go (6)
server/streamable_http.go (1)
  • WithLogger (87-91)
util/logger.go (2)
  • Logger (8-11)
  • DefaultLogger (16-20)
client/client.go (1)
  • Client (16-25)
client/transport/interface.go (1)
  • HTTPHeaderFunc (13-13)
mcp/types.go (1)
  • MethodInitialize (19-19)
client/oauth.go (1)
  • OAuthAuthorizationRequiredError (61-61)
🔇 Additional comments (6)
client/transport/streamable_http.go (5)

25-36: Well-documented continuous listening option

The implementation follows the established option pattern and includes clear documentation with specification references. The notice about server support is particularly helpful.


71-75: Clean logger integration

Good use of the default logger pattern to ensure the logger is always initialized.

Also applies to: 91-96, 125-125


146-156: Goroutine leak prevention properly implemented

The select statement correctly handles both initialization success and client closure, addressing the previous concern about potential goroutine leaks.


527-548: Robust continuous listening implementation

Good error handling with graceful degradation when servers don't support GET method. The configurable retry interval is helpful for testing.


557-590: Clean SSE connection implementation

Well-structured GET connection handling with proper content-type validation and error responses.

client/http_test.go (1)

196-223: Well-implemented thread-safe map

Good use of RWMutex for efficient concurrent access with proper separation of read and write operations.

@leavez
Copy link
Contributor Author

leavez commented Jun 5, 2025

@dugenkui03 I've updated test for demonstrating.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
client/http_test.go (1)

122-122: Fix typo in error messages.

There's a typo "progross" that should be "progress" in the error messages.

-				t.Errorf("Expected 1 progross notification item, got %d", n)
+				t.Errorf("Expected 1 progress notification item, got %d", n)

And similarly on line 176:

-				t.Errorf("Expected 1 progross notification item, got %d", n)
+				t.Errorf("Expected 1 progress notification item, got %d", n)

Also applies to: 176-176

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8b7dce and c706c93.

📒 Files selected for processing (1)
  • client/http_test.go (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
client/http_test.go (7)
mcp/utils.go (1)
  • NewToolResultText (234-243)
mcp/tools.go (3)
  • NewTool (569-591)
  • CallToolRequest (46-49)
  • CallToolResult (36-43)
server/server.go (1)
  • ServerFromContext (71-76)
server/streamable_http.go (1)
  • NewTestStreamableHTTPServer (645-649)
mcp/types.go (7)
  • InitializeRequest (399-402)
  • Params (162-162)
  • InitializeParams (404-410)
  • LATEST_PROTOCOL_VERSION (99-99)
  • Implementation (480-483)
  • JSONRPCNotification (318-321)
  • Content (827-829)
client/http.go (1)
  • NewStreamableHttpClient (11-17)
client/transport/streamable_http.go (1)
  • WithContinuousListening (32-36)
🔇 Additional comments (10)
client/http_test.go (10)

6-6: LGTM! Imports correctly updated for new functionality.

The addition of the sync package and reorganization of imports properly supports the new thread-safe SafeMap implementation.

Also applies to: 10-12


54-54: Good refactoring to use helper function.

Using mcp.NewToolResultText instead of manually constructing the result improves code readability and maintainability.


58-69: Helper function implementation looks good.

The addServerToolfunc correctly creates tools that send global notifications to all clients, which is essential for testing the continuous listening feature.


74-82: Good refactoring to move initialization outside subtests.

Moving the initialization request definition outside and reusing it follows the DRY principle and improves test maintainability.


91-91: Improved thread-safety with SafeMap.

The change from a simple integer counter to a thread-safe map properly handles concurrent notification callbacks.


109-127: Well-structured subtest for basic notification functionality.

The subtest properly verifies that notifications related to tool requests are received correctly.


129-137: Good test coverage for default behavior.

This subtest correctly verifies that global notifications are NOT received by default, which is important for ensuring backward compatibility.


139-190: Comprehensive test for continuous listening feature.

The subtest properly:

  • Creates a new client with WithContinuousListening() option
  • Verifies both normal and global notification receipt
  • Includes proper cleanup with defer client.Close()

The test logic correctly validates the new continuous listening functionality.


195-222: Well-implemented thread-safe map.

The SafeMap implementation correctly uses:

  • sync.RWMutex for optimal read/write performance
  • Proper locking patterns in all methods
  • Clear and simple API for the test's needs

This is a solid implementation for handling concurrent notification callbacks.


186-189:

✅ Verification successful

Verify notification type being tested.

The test checks for "notifications/tools/list_changed" notification, which appears to be automatically triggered when a tool is added to the server, rather than the "helloToEveryone" notification sent within the tool function. This seems correct as it tests the actual server behavior.

Can you confirm that adding a tool to the server automatically triggers a "notifications/tools/list_changed" notification? If so, this test logic is correct.


🏁 Script executed:

#!/bin/bash
# Search for where "notifications/tools/list_changed" is sent in the codebase
rg -A 3 -B 3 "notifications/tools/list_changed"

Length of output: 7625


Test correctly verifies “notifications/tools/list_changed”

The search shows that the server’s session logic (server/session.go) and its own tests (server/server_test.go, server/session_test.go) emit a "notifications/tools/list_changed" notification whenever tools are added or removed. The client/http_test.go assertion on lines 186–189 is therefore valid and needs no change.

  • File: client/http_test.go
    Lines 186–189 – correctly checks for exactly one "notifications/tools/list_changed" notification.


func (c *StreamableHTTP) createGETConnectionToServer() error {

ctx, cancel := c.contextAwareOfClientClose(context.Background())
Copy link
Collaborator

Choose a reason for hiding this comment

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

why not pass ctx from Start() to createGETConnectionToServer(), then use ctx from Start()

Copy link
Contributor Author

@leavez leavez Jun 26, 2025

Choose a reason for hiding this comment

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

fixed in c222bab

return fmt.Errorf("unexpected content type: %s", contentType)
}

_, err = c.handleSSEResponse(ctx, resp.Body)

Choose a reason for hiding this comment

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

We currently ignore the response here.
However, if you refer to the Python SDK implementation, you'll see that server responses are actively written to a read_stream—a memory stream used for receiving messages.

This read_stream is initialized during client setup, and it's the same stream shared by both the GET and POST handlers.

That said, the intended behavior here is still somewhat unclear—the MCP spec doesn't explicitly define whether GET responses must be surfaced to the client, so it's possible the current handling is valid, but worth clarifying.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

7dc57e6

The spec is unclear, and to be easier to use, we should be more compatible, however, currently the transport layer is message based, so is no easy way to handle the response messages.


return resp, nil
}

// handleSSEResponse processes an SSE stream for a specific request.
// It returns the final result for the request once received, or an error.
func (c *StreamableHTTP) handleSSEResponse(ctx context.Context, reader io.ReadCloser) (*JSONRPCResponse, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When readSSE#ReadString occur error, reader will be closed and won't receive notification anymore, but handleSSEResponse will wait ctx.Done(). Now, client will ignore all the notification, am I right?
image

image

Copy link
Contributor Author

@leavez leavez Jun 26, 2025

Choose a reason for hiding this comment

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

image when readSSE is done, the responseChan will be closed, and goes image

then the method returns

@leavez
Copy link
Contributor Author

leavez commented Jun 12, 2025

I’m currently on vacation and will take care of it when back.

Meanwhile, feel free to fork this branch and open a new PR directly if anyone like to improve it

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
client/transport/streamable_http.go (1)

385-385: Replace print statements with structured logging

These print statements should use the logger for consistency with the rest of the codebase.

-				fmt.Printf("failed to unmarshal message: %v\n", err)
+				c.logger.Errorf("failed to unmarshal message: %v", err)
-					fmt.Printf("failed to unmarshal notification: %v\n", err)
+					c.logger.Errorf("failed to unmarshal notification: %v", err)
-					fmt.Printf("SSE stream error: %v\n", err)
+					c.logger.Errorf("SSE stream error: %v", err)

Also applies to: 393-393, 446-446

♻️ Duplicate comments (1)
client/transport/streamable_http.go (1)

583-583: Response from handleSSEResponse is ignored

The return value from handleSSEResponse is ignored, but this may be intentional for the continuous listening use case where we don't expect a specific response.

However, consider logging if an unexpected response is received to aid in debugging:

-	_, err = c.handleSSEResponse(ctx, resp.Body)
+	response, err := c.handleSSEResponse(ctx, resp.Body)
+	if response != nil {
+		c.logger.Warnf("unexpected response received during continuous listening: %v", response)
+	}
🧹 Nitpick comments (1)
client/transport/streamable_http.go (1)

529-550: Robust continuous listening implementation

The listenForever method handles server capabilities properly by detecting when GET method is not allowed. The retry logic with exponential backoff would be more robust than a fixed 1-second interval.

Consider implementing exponential backoff for retries:

+var maxRetryInterval = 30 * time.Second
+
 func (c *StreamableHTTP) listenForever(ctx context.Context) {
 	c.logger.Infof("listening to server forever")
+	currentRetryInterval := retryInterval
 	for {
 		err := c.createGETConnectionToServer(ctx)
 		if errors.Is(err, errGetMethodNotAllowed) {
 			c.logger.Errorf("server does not support listening")
 			return
 		}
 
 		select {
 		case <-ctx.Done():
 			return
 		default:
 		}
 
 		if err != nil {
 			c.logger.Errorf("failed to listen to server. retry in %v: %v", currentRetryInterval, err)
+			time.Sleep(currentRetryInterval)
+			currentRetryInterval = min(currentRetryInterval*2, maxRetryInterval)
+		} else {
+			currentRetryInterval = retryInterval // reset on success
-		}
-		time.Sleep(retryInterval)
+		}
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c706c93 and c222bab.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (11 hunks)
🧰 Additional context used
🧠 Learnings (1)
client/transport/streamable_http.go (2)
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
🔇 Additional comments (8)
client/transport/streamable_http.go (8)

25-36: Well-documented continuous listening feature

The new WithContinuousListening option is well-documented with clear explanations of its purpose and server requirements. The implementation correctly sets the flag to enable the feature.


71-75: Logger option properly implemented

The WithLogger option follows the established pattern and correctly sets the logger field.


100-101: Proper synchronization for initialization coordination

The addition of sync.Once ensures the initialized channel is closed exactly once, addressing previous concerns about channel coordination and potential deadlocks.


147-158: Goroutine leak prevention implemented correctly

The select statement in the goroutine properly handles both initialization completion and client closure, preventing the goroutine leak that was flagged in previous reviews.


183-183: Improved error reporting with structured logging

Good improvement replacing print statements with proper structured logging for better observability.

Also applies to: 189-189


273-275: Initialization channel properly coordinated

Using sync.Once ensures the initialization channel is closed exactly once, resolving the previous concern about potential deadlocks when initialization fails.


304-362: Well-structured HTTP request unification

The new sendHTTP method effectively consolidates HTTP request logic, improving maintainability. The universal 404 handling for session termination is a good approach.


591-603: Context management utility well-implemented

The contextAwareOfClientClose function properly combines client closure with context cancellation, ensuring clean shutdown behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
client/transport/streamable_http.go (1)

356-359: Generic 404 handling may mask routing issues

The current implementation treats all 404 responses as session termination, which could hide genuine endpoint routing problems. Consider checking for specific headers or response body content that confirms session termination.

🧹 Nitpick comments (1)
client/transport/streamable_http.go (1)

586-591: Address specification ambiguity with GET responses

The comment explains a potential compatibility issue where other SDKs might send response messages via the GET connection. Consider implementing a mechanism to handle these responses or provide a configuration option for different compatibility modes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c222bab and 7dc57e6.

📒 Files selected for processing (1)
  • client/transport/streamable_http.go (12 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
client/transport/streamable_http.go (2)
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
🔇 Additional comments (12)
client/transport/streamable_http.go (12)

20-20: New utility import looks good

The addition of the util package import is appropriate for the new logger functionality.


25-36: Well-documented continuous listening option

The WithContinuousListening function is well-documented with clear explanation of its purpose and MCP specification reference. The notice about server support is helpful for users.


71-75: Logger option follows consistent pattern

The WithLogger function follows the same pattern as other option functions and provides necessary logging support for the continuous listening feature.


96-101: Proper synchronization primitives for initialization

The addition of initialized channel and initializedOnce sync.Once addresses the previous review concerns about ensuring the initialization channel is closed exactly once across all code paths.


148-158: Goroutine leak prevention implemented correctly

The select statement properly handles both the initialized channel and closed channel, preventing goroutine leaks if initialization fails. This addresses the previous review concern about potential deadlocks.


235-244: Improved error handling for initialization requests

The special handling for session termination errors during initialization requests is a good improvement. However, the error handling could be more explicit about what constitutes a routing issue vs. session termination.


273-275: Proper channel closing with sync.Once

Using sync.Once to close the initialized channel ensures it's closed exactly once, addressing previous review concerns about channel coordination and potential deadlocks.


304-362: Good refactoring of common HTTP logic

The extraction of HTTP request logic into sendHTTP method reduces code duplication and centralizes request handling. The OAuth and header handling is comprehensive.


367-367: Clear parameter documentation

The ignoreResponse parameter is well-documented, making it clear when responses should be ignored for continuous listening scenarios.


532-553: Robust continuous listening implementation

The listenForever method includes proper error handling, retry logic, and graceful handling of servers that don't support continuous listening. The retry interval being a variable is good for testing.


562-597: Comprehensive GET connection handling

The createGETConnectionToServer method properly handles different HTTP status codes and validates content types. The comment about specification ambiguity and SDK compatibility is valuable context.


599-611: Well-designed context coordination

The contextAwareOfClientClose method properly coordinates context cancellation between the client's close state and the original context, preventing resource leaks.

@leavez
Copy link
Contributor Author

leavez commented Jun 26, 2025

@dugenkui03 All comments are fixed

@dugenkui03 dugenkui03 merged commit 1eddde7 into mark3labs:main Jun 28, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: mcp spec Issues related to MCP specification compliance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy