Skip to content

added the configurable items sse_read_timeout and headers to mcp-client #55

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 7 commits into from
Mar 25, 2025

Conversation

resurgence72
Copy link
Contributor

@resurgence72 resurgence72 commented Mar 18, 2025

Added the option design mode to mcp-client and added the configurable items sse_read_timeout and headers.
The readSSE() method and sendRequest() method take effect with the SSEMCPClient object, respectively.

When I was looking at Python-sdk, I found that the mcp-client can specify headers and sse_read_timeout configurations when initializing objects. In my usage scenario, it is necessary for the mcp-client object to carry a custom request header. I think this is a universal scenario.

Summary by CodeRabbit

  • New Features

    • Introduced configuration options to customize connection headers.
    • Added the ability to set a read timeout for Server-Sent Events (SSE) connections.
    • Enhanced error handling for scenarios where the SSE read exceeds the specified timeout duration.
  • Changes

    • Updated JSON serialization to ensure the Properties field is always included in the output for the ToolInputSchema.

Copy link
Contributor

coderabbitai bot commented Mar 18, 2025

Walkthrough

The pull request enhances the SSEMCPClient in the client/sse.go file by introducing functional options for configuration, including custom HTTP headers and read timeout settings. The constructor is updated to accept these options, and the SSEMCPClient struct is modified to include new fields. Methods for reading SSE events and sending requests are revised to handle timeout scenarios and integrate custom headers. Additionally, the Properties field in the ToolInputSchema struct in mcp/tools.go is modified to always be included in the JSON output.

Changes

File(s) Change Summary
client/sse.go - Added functional options: WithHeaders (to set custom HTTP headers) and WithSSEReadTimeout (to set a custom read timeout).
- Modified NewSSEMCPClient to accept variadic options.
- Updated SSEMCPClient struct with new fields: headers map[string]string and sseReadTimeout time.Duration.
- Updated readSSE to utilize a context with timeout and handle timeout errors.
- Enhanced sendRequest to use custom HTTP headers.
mcp/tools.go - Updated Properties field in ToolInputSchema to ensure it is always included in JSON output by changing the JSON tag from json:"properties,omitempty" to json:"properties".
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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 resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @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

🧹 Nitpick comments (2)
client/sse.go (2)

21-33: Document the new features in function comments

While the implementation is good, it would be helpful to document the new configuration options in the function comments, especially for NewSSEMCPClient, WithHeaders, and WithSSEReadTimeout.

+// Option defines a function type that modifies the SSEMCPClient configuration.
 type Option func(*SSEMCPClient)

+// WithHeaders sets custom HTTP headers that will be included in all requests sent by the client.
 func WithHeaders(headers map[string]string) Option {
 	return func(sc *SSEMCPClient) {
 		sc.headers = headers
 	}
 }

+// WithSSEReadTimeout sets the timeout duration for SSE read operations.
+// If not set, a default timeout will be used.
 func WithSSEReadTimeout(timeout time.Duration) Option {
 	return func(sc *SSEMCPClient) {
 		sc.sseReadTimeout = timeout
 	}
 }

 // SSEMCPClient implements the MCPClient interface using Server-Sent Events (SSE).
 // It maintains a persistent HTTP connection to receive server-pushed events
 // while sending requests over regular HTTP POST calls. The client handles
 // automatic reconnection and message routing between requests and responses.

And update the NewSSEMCPClient documentation:

-// NewSSEMCPClient creates a new SSE-based MCP client with the given base URL.
-// Returns an error if the URL is invalid.
+// NewSSEMCPClient creates a new SSE-based MCP client with the given base URL and options.
+// Options can include custom HTTP headers and SSE read timeout settings.
+// Returns an error if the URL is invalid.
 func NewSSEMCPClient(baseURL string, options ...Option) (*SSEMCPClient, error) {

Also applies to: 43-43, 53-53, 58-76, 126-166, 305-308


21-33: Consider adding validation for headers

The WithHeaders option accepts any map of headers without validation. While this provides flexibility, it could lead to overriding essential headers like "Content-Type" or "Accept".

Consider adding validation or merging logic to prevent overriding critical headers:

 func WithHeaders(headers map[string]string) Option {
 	return func(sc *SSEMCPClient) {
+		// Create a new map if headers is nil
+		if sc.headers == nil {
+			sc.headers = make(map[string]string)
+		}
+		
+		// Copy headers, skipping those that would override critical ones
+		for k, v := range headers {
+			// Skip critical headers that shouldn't be overridden
+			if k == "Content-Type" || k == "Accept" || k == "Connection" {
+				continue
+			}
+			sc.headers[k] = v
+		}
-		sc.headers = headers
 	}
 }

 // In sendRequest method
 req.Header.Set("Content-Type", "application/json")
 // set custom http headers
 for k, v := range c.headers {
-	req.Header.Set(k, v)
+	// Don't override headers already set
+	if v != "" && req.Header.Get(k) == "" {
+		req.Header.Set(k, v)
+	}
 }

Also applies to: 305-308

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a73d7cf and 6939c07.

📒 Files selected for processing (1)
  • client/sse.go (3 hunks)
🔇 Additional comments (4)
client/sse.go (4)

21-33: Well-implemented functional options pattern

The addition of the functional options pattern is a good approach for configuring the SSEMCPClient. This pattern provides flexibility for adding new configuration options in the future without breaking existing code.


43-43: Appropriate field additions for new configurations

The new fields headers and sseReadTimeout align well with the PR objective to support custom headers and read timeout configuration.

Also applies to: 53-53


58-76: Constructor properly updated to support options

The constructor has been correctly modified to accept and apply functional options, while maintaining backward compatibility by making the options parameter variadic.


305-308: Headers implementation looks good

The custom headers implementation in the sendRequest method is clean and straightforward.

According to the mcp's structural definition of the tool, inputSchema.properties cannot be null
@resurgence72
Copy link
Contributor Author

I introduced another commit in the current pr, sorry, I'm not familiar with the whole git process.

New submission to solve the problem (junjiem/dify-plugin-agent-mcp_sse#1).
in general, the current MCP - server InputSchema standards do not agree with MCP, Its InputSchema.properties cannot be empty in mcp.
image

but in mcp-go tool.go
image

As a result, when I used dify to interconnect with the parameterless tool written based on mcp-go, the mcp-client failed to pass the verification, resulting in an error, namely, the above issues

@resurgence72
Copy link
Contributor Author

hi @ezynda3 could you please help check this PR? I hope we can work together as soon as possible. At present, there are already some conflicts. Do you need me to solve them? Thank you very much!

@ezynda3
Copy link
Contributor

ezynda3 commented Mar 25, 2025

hi @ezynda3 could you please help check this PR? I hope we can work together as soon as possible. At present, there are already some conflicts. Do you need me to solve them? Thank you very much!

Yes please fix the merge conflicts and I can have a look at the PR

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: 1

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9747156 and 89bf4a2.

📒 Files selected for processing (1)
  • client/sse.go (3 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
client/sse.go (3)
client/types.go (1)
  • RPCResponse (5-8)
mcp/types.go (2)
  • JSONRPCNotification (192-195)
  • ServerCapabilities (319-342)
client/stdio.go (16)
  • c (86-92)
  • c (96-102)
  • c (107-170)
  • c (176-224)
  • c (226-229)
  • c (231-285)
  • c (287-307)
  • c (309-329)
  • c (331-342)
  • c (344-350)
  • c (352-358)
  • c (360-375)
  • c (377-387)
  • c (389-404)
  • c (406-416)
  • c (418-424)
🔇 Additional comments (5)
client/sse.go (5)

37-39: LGTM: New client configuration fields

The addition of headers and sseReadTimeout fields to the SSEMCPClient struct provides the necessary structure for the new configurable options.


42-54: Well-implemented functional options pattern

The implementation of the functional options pattern with WithHeaders and WithSSEReadTimeout is clean and follows Go best practices. This approach provides a flexible, extensible way to configure the client.


58-78: Good default values in constructor

The constructor properly initializes the new fields with sensible defaults (30-second timeout and empty headers map) before applying the provided options. This ensures the client behaves correctly even without explicit configuration.


134-175: Improve timeout handling in readSSE method

The current implementation creates a single timeout context at the beginning of the method, which means:

  1. The timeout applies to the entire SSE connection
  2. After the timeout period, the connection will be terminated regardless of activity

This might not be the expected behavior. Consider implementing a per-read timeout instead.

For a better implementation, consider:

  1. Using a parent context that's canceled when the client is closed
  2. Setting timeouts for individual read operations
 func (c *SSEMCPClient) readSSE(reader io.ReadCloser) {
 	defer reader.Close()
 
-	ctx, cancel := context.WithTimeout(context.Background(), c.sseReadTimeout)
+	// Create a parent context that can be canceled when the client is closed
+	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
+	
+	// Cancel context when client is closed
+	go func() {
+		select {
+		case <-c.done:
+			cancel()
+		}
+	}()
 
 	br := bufio.NewReader(reader)
 	var event, data string
 
 	for {
 		select {
 		case <-ctx.Done():
 			return
 		default:
+			// Set timeout for this specific read operation
+			readCtx, readCancel := context.WithTimeout(ctx, c.sseReadTimeout)
+			readDone := make(chan struct{})
+			
+			go func() {
+				defer close(readDone)
+				line, err := br.ReadString('\n')
+				// Process line here...
+			}()
+			
+			select {
+			case <-readCtx.Done():
+				if err := readCtx.Err(); err != context.Canceled {
+					fmt.Printf("SSE read timed out: %v\n", err)
+					return
+				}
+			case <-readDone:
+				// Read completed successfully
+			}
+			readCancel()

304-307: LGTM: Custom headers implementation

The implementation for setting custom HTTP headers is straightforward and effective.

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/sse.go (1)

131-138: 🛠️ Refactor suggestion

Consider implementing per-read timeouts instead of a single timeout.

The current implementation creates a single timeout context at the beginning of the method, which means:

  1. If the timeout is meant to apply to the entire SSE connection, it will terminate after sseReadTimeout regardless of activity
  2. If the timeout is very short, it might disconnect even during active communications

Consider implementing a per-read timeout that resets on successful reads:

-ctx, cancel := context.WithTimeout(context.Background(), c.sseReadTimeout)
-defer cancel()
+// Create a parent context that can be canceled when the client is closed
+ctx, cancel := context.WithCancel(context.Background())
+defer cancel()
+
+// Start a goroutine that will cancel the context when c.done is closed
+go func() {
+	select {
+	case <-c.done:
+		cancel()
+	}
+}()

for {
	select {
	case <-ctx.Done():
+		if errors.Is(ctx.Err(), context.Canceled) {
+			// Client was closed, normal termination
+			return
+		}
+		// Timeout occurred
+		fmt.Printf("SSE read timed out: %v\n", ctx.Err())
		return
	default:
+		// Use a timeout for this specific read
+		readCtx, readCancel := context.WithTimeout(ctx, c.sseReadTimeout)
+		lineCh := make(chan string, 1)
+		errCh := make(chan error, 1)
+		
+		go func() {
+			line, err := br.ReadString('\n')
+			if err != nil {
+				errCh <- err
+				return
+			}
+			lineCh <- line
+		}()
+		
+		select {
+		case <-readCtx.Done():
+			readCancel()
+			return
+		case err := <-errCh:
+			readCancel()
+			// Handle error as in the current implementation
+			// ...
+		case line := <-lineCh:
+			readCancel()
+			// Process line as in the current implementation
+			// ...
+		}
	}
}
🧹 Nitpick comments (3)
client/sse.go (3)

42-54: Make a defensive copy of headers in WithHeaders function.

The WithHeaders function directly assigns the provided map to the client's headers field. If the caller modifies the map after creating the option, it will affect the client's behavior.

Consider making a defensive copy:

func WithHeaders(headers map[string]string) ClientOption {
	return func(sc *SSEMCPClient) {
-		sc.headers = headers
+		// Make a defensive copy
+		for k, v := range headers {
+			sc.headers[k] = v
+		}
	}
}

70-70: Validate timeout value to prevent immediate timeouts.

The default timeout of 30 seconds is reasonable, but there's no validation to ensure that a user-provided timeout isn't too short or zero.

Consider adding validation in the WithSSEReadTimeout function:

func WithSSEReadTimeout(timeout time.Duration) ClientOption {
	return func(sc *SSEMCPClient) {
+		// Ensure timeout is at least 1 second to prevent immediate timeouts
+		if timeout < time.Second {
+			timeout = time.Second
+		}
		sc.sseReadTimeout = timeout
	}
}

304-307: Check for potential header conflicts.

The current implementation adds all headers from the map without checking for potential conflicts with headers set elsewhere in the code.

Consider adding a check for important headers:

// set custom http headers
for k, v := range c.headers {
+	// Skip Content-Type if it's already set to prevent overriding
+	if strings.ToLower(k) == "content-type" && req.Header.Get("Content-Type") != "" {
+		continue
+	}
	req.Header.Set(k, v)
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 89bf4a2 and eb64d3f.

📒 Files selected for processing (1)
  • client/sse.go (3 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
client/sse.go (2)
client/types.go (1)
  • RPCResponse (5-8)
mcp/types.go (2)
  • JSONRPCNotification (192-195)
  • ServerCapabilities (319-342)
🔇 Additional comments (2)
client/sse.go (2)

38-39: Good addition of configuration fields to the struct.

The addition of headers and sseReadTimeout fields to the SSEMCPClient struct is a good enhancement that allows for more flexibility in client configuration.


58-78: Good implementation of the functional options pattern.

The updated constructor correctly implements the functional options pattern, setting reasonable defaults and applying user-provided options.

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.

2 participants