Files
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2bd709db63 build(deps): bump github.com/mark3labs/mcp-go from 0.37.0 to 0.43.0 (#5844)
Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.37.0 to 0.43.0.
- [Release notes](https://github.com/mark3labs/mcp-go/releases)
- [Commits](https://github.com/mark3labs/mcp-go/compare/v0.37.0...v0.43.0)

---
updated-dependencies:
- dependency-name: github.com/mark3labs/mcp-go
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 08:17:30 +00:00

581 lines
15 KiB
Go

package transport
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/util"
)
// SSE implements the transport layer of the MCP protocol 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.
type SSE struct {
baseURL *url.URL
endpoint *url.URL
httpClient *http.Client
responses map[string]chan *JSONRPCResponse
mu sync.RWMutex
onNotification func(mcp.JSONRPCNotification)
notifyMu sync.RWMutex
endpointChan chan struct{}
headers map[string]string
headerFunc HTTPHeaderFunc
logger util.Logger
started atomic.Bool
closed atomic.Bool
cancelSSEStream context.CancelFunc
protocolVersion atomic.Value // string
onConnectionLost func(error)
connectionLostMu sync.RWMutex
// OAuth support
oauthHandler *OAuthHandler
}
type ClientOption func(*SSE)
// WithSSELogger sets a custom logger for the SSE client.
func WithSSELogger(logger util.Logger) ClientOption {
return func(sc *SSE) {
sc.logger = logger
}
}
func WithHeaders(headers map[string]string) ClientOption {
return func(sc *SSE) {
sc.headers = headers
}
}
func WithHeaderFunc(headerFunc HTTPHeaderFunc) ClientOption {
return func(sc *SSE) {
sc.headerFunc = headerFunc
}
}
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(sc *SSE) {
sc.httpClient = httpClient
}
}
func WithOAuth(config OAuthConfig) ClientOption {
return func(sc *SSE) {
sc.oauthHandler = NewOAuthHandler(config)
}
}
// NewSSE creates a new SSE-based MCP client with the given base URL.
// Returns an error if the URL is invalid.
func NewSSE(baseURL string, options ...ClientOption) (*SSE, error) {
parsedURL, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
smc := &SSE{
baseURL: parsedURL,
httpClient: &http.Client{},
responses: make(map[string]chan *JSONRPCResponse),
endpointChan: make(chan struct{}),
headers: make(map[string]string),
logger: util.DefaultLogger(),
}
for _, opt := range options {
opt(smc)
}
// If OAuth is configured, set the base URL for metadata discovery
if smc.oauthHandler != nil {
// Extract base URL from server URL for metadata discovery
baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
smc.oauthHandler.SetBaseURL(baseURL)
}
return smc, nil
}
// Start initiates the SSE connection to the server and waits for the endpoint information.
// Returns an error if the connection fails or times out waiting for the endpoint.
func (c *SSE) Start(ctx context.Context) error {
if c.started.Load() {
return nil
}
ctx, cancel := context.WithCancel(ctx)
c.cancelSSEStream = cancel
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL.String(), nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Connection", "keep-alive")
// set custom http headers
for k, v := range c.headers {
req.Header.Set(k, v)
}
if c.headerFunc != nil {
for k, v := range c.headerFunc(ctx) {
req.Header.Set(k, v)
}
}
// Add OAuth authorization if configured
if c.oauthHandler != nil {
authHeader, err := c.oauthHandler.GetAuthorizationHeader(ctx)
if err != nil {
// If we get an authorization error, return a specific error that can be handled by the client
if err.Error() == "no valid token available, authorization required" {
return &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
return fmt.Errorf("failed to get authorization header: %w", err)
}
req.Header.Set("Authorization", authHeader)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to connect to SSE stream: %w", err)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
// Handle OAuth unauthorized error
if resp.StatusCode == http.StatusUnauthorized && c.oauthHandler != nil {
return &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
go c.readSSE(resp.Body)
// Wait for the endpoint to be received
timeout := time.NewTimer(30 * time.Second)
defer timeout.Stop()
select {
case <-c.endpointChan:
// Endpoint received, proceed
case <-ctx.Done():
return fmt.Errorf("context cancelled while waiting for endpoint")
case <-timeout.C: // Add a timeout
cancel()
return fmt.Errorf("timeout waiting for endpoint")
}
c.started.Store(true)
return nil
}
// readSSE continuously reads the SSE stream and processes events.
// It runs until the connection is closed or an error occurs.
func (c *SSE) readSSE(reader io.ReadCloser) {
defer reader.Close()
br := bufio.NewReader(reader)
var event, data string
for {
// when close or start's ctx cancel, the reader will be closed
// and the for loop will break.
line, err := br.ReadString('\n')
if err != nil {
if err == io.EOF {
// Process any pending event before exit
if data != "" {
// If no event type is specified, use empty string (default event type)
if event == "" {
event = "message"
}
c.handleSSEEvent(event, data)
}
break
}
// Checking whether the connection was terminated due to NO_ERROR in HTTP2 based on RFC9113
// Only handle NO_ERROR specially if onConnectionLost handler is set to maintain backward compatibility
if strings.Contains(err.Error(), "NO_ERROR") {
c.connectionLostMu.RLock()
handler := c.onConnectionLost
c.connectionLostMu.RUnlock()
if handler != nil {
// This is not actually an error - HTTP2 idle timeout disconnection
handler(err)
return
}
}
if !c.closed.Load() {
c.logger.Errorf("SSE stream error: %v", err)
}
return
}
// Remove only newline markers
line = strings.TrimRight(line, "\r\n")
if line == "" {
// Empty line means end of event
if data != "" {
// If no event type is specified, use empty string (default event type)
if event == "" {
event = "message"
}
c.handleSSEEvent(event, data)
event = ""
data = ""
}
continue
}
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
}
}
}
// handleSSEEvent processes SSE events based on their type.
// Handles 'endpoint' events for connection setup and 'message' events for JSON-RPC communication.
func (c *SSE) handleSSEEvent(event, data string) {
switch event {
case "endpoint":
endpoint, err := c.baseURL.Parse(data)
if err != nil {
c.logger.Errorf("Error parsing endpoint URL: %v", err)
return
}
if endpoint.Host != c.baseURL.Host {
c.logger.Errorf("Endpoint origin does not match connection origin")
return
}
c.endpoint = endpoint
close(c.endpointChan)
case "message":
var baseMessage JSONRPCResponse
if err := json.Unmarshal([]byte(data), &baseMessage); err != nil {
c.logger.Errorf("Error unmarshaling message: %v", err)
return
}
// Handle notification
if baseMessage.ID.IsNil() {
var notification mcp.JSONRPCNotification
if err := json.Unmarshal([]byte(data), &notification); err != nil {
return
}
c.notifyMu.RLock()
if c.onNotification != nil {
c.onNotification(notification)
}
c.notifyMu.RUnlock()
return
}
// Create string key for map lookup
idKey := baseMessage.ID.String()
c.mu.RLock()
ch, exists := c.responses[idKey]
c.mu.RUnlock()
if exists {
ch <- &baseMessage
c.mu.Lock()
delete(c.responses, idKey)
c.mu.Unlock()
}
}
}
func (c *SSE) SetNotificationHandler(handler func(notification mcp.JSONRPCNotification)) {
c.notifyMu.Lock()
defer c.notifyMu.Unlock()
c.onNotification = handler
}
func (c *SSE) SetConnectionLostHandler(handler func(error)) {
c.connectionLostMu.Lock()
defer c.connectionLostMu.Unlock()
c.onConnectionLost = handler
}
// SendRequest sends a JSON-RPC request to the server and waits for a response.
// Returns the raw JSON response message or an error if the request fails.
func (c *SSE) SendRequest(
ctx context.Context,
request JSONRPCRequest,
) (*JSONRPCResponse, error) {
if !c.started.Load() {
return nil, fmt.Errorf("transport not started yet")
}
if c.closed.Load() {
return nil, fmt.Errorf("transport has been closed")
}
if c.endpoint == nil {
return nil, fmt.Errorf("endpoint not received")
}
// Marshal request
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint.String(), bytes.NewReader(requestBytes))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
// Set protocol version header if negotiated
if v := c.protocolVersion.Load(); v != nil {
if version, ok := v.(string); ok && version != "" {
req.Header.Set(HeaderKeyProtocolVersion, version)
}
}
for k, v := range c.headers {
req.Header.Set(k, v)
}
for k, v := range request.Header {
if _, ok := req.Header[k]; !ok {
req.Header[k] = v
}
}
// Add OAuth authorization if configured
if c.oauthHandler != nil {
authHeader, err := c.oauthHandler.GetAuthorizationHeader(ctx)
if err != nil {
// If we get an authorization error, return a specific error that can be handled by the client
if err.Error() == "no valid token available, authorization required" {
return nil, &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
return nil, fmt.Errorf("failed to get authorization header: %w", err)
}
req.Header.Set("Authorization", authHeader)
}
if c.headerFunc != nil {
for k, v := range c.headerFunc(ctx) {
req.Header.Set(k, v)
}
}
// Create string key for map lookup
idKey := request.ID.String()
// Register response channel
responseChan := make(chan *JSONRPCResponse, 1)
c.mu.Lock()
c.responses[idKey] = responseChan
c.mu.Unlock()
deleteResponseChan := func() {
c.mu.Lock()
delete(c.responses, idKey)
c.mu.Unlock()
}
// Send request
resp, err := c.httpClient.Do(req)
if err != nil {
deleteResponseChan()
return nil, fmt.Errorf("failed to send request: %w", err)
}
// Drain any outstanding io
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Check if we got an error response
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
deleteResponseChan()
// Handle OAuth unauthorized error
if resp.StatusCode == http.StatusUnauthorized && c.oauthHandler != nil {
return nil, &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, body)
}
select {
case <-ctx.Done():
deleteResponseChan()
return nil, ctx.Err()
case response, ok := <-responseChan:
if ok {
return response, nil
}
return nil, fmt.Errorf("connection has been closed")
}
}
// Close shuts down the SSE client connection and cleans up any pending responses.
// Returns an error if the shutdown process fails.
func (c *SSE) Close() error {
if !c.closed.CompareAndSwap(false, true) {
return nil // Already closed
}
if c.cancelSSEStream != nil {
// It could stop the sse stream body, to quit the readSSE loop immediately
// Also, it could quit start() immediately if not receiving the endpoint
c.cancelSSEStream()
}
// Clean up any pending responses
c.mu.Lock()
for _, ch := range c.responses {
close(ch)
}
c.responses = make(map[string]chan *JSONRPCResponse)
c.mu.Unlock()
return nil
}
// GetSessionId returns the session ID of the transport.
// Since SSE does not maintain a session ID, it returns an empty string.
func (c *SSE) GetSessionId() string {
return ""
}
// SetProtocolVersion sets the negotiated protocol version for this connection.
func (c *SSE) SetProtocolVersion(version string) {
c.protocolVersion.Store(version)
}
// SendNotification sends a JSON-RPC notification to the server without expecting a response.
func (c *SSE) SendNotification(ctx context.Context, notification mcp.JSONRPCNotification) error {
if c.endpoint == nil {
return fmt.Errorf("endpoint not received")
}
notificationBytes, err := json.Marshal(notification)
if err != nil {
return fmt.Errorf("failed to marshal notification: %w", err)
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
c.endpoint.String(),
bytes.NewReader(notificationBytes),
)
if err != nil {
return fmt.Errorf("failed to create notification request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
// Set protocol version header if negotiated
if v := c.protocolVersion.Load(); v != nil {
if version, ok := v.(string); ok && version != "" {
req.Header.Set(HeaderKeyProtocolVersion, version)
}
}
// Set custom HTTP headers
for k, v := range c.headers {
req.Header.Set(k, v)
}
// Add OAuth authorization if configured
if c.oauthHandler != nil {
authHeader, err := c.oauthHandler.GetAuthorizationHeader(ctx)
if err != nil {
// If we get an authorization error, return a specific error that can be handled by the client
if errors.Is(err, ErrOAuthAuthorizationRequired) {
return &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
return fmt.Errorf("failed to get authorization header: %w", err)
}
req.Header.Set("Authorization", authHeader)
}
if c.headerFunc != nil {
for k, v := range c.headerFunc(ctx) {
req.Header.Set(k, v)
}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send notification: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
// Handle OAuth unauthorized error
if resp.StatusCode == http.StatusUnauthorized && c.oauthHandler != nil {
return &OAuthAuthorizationRequiredError{
Handler: c.oauthHandler,
}
}
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf(
"notification failed with status %d: %s",
resp.StatusCode,
body,
)
}
return nil
}
// GetEndpoint returns the current endpoint URL for the SSE connection.
func (c *SSE) GetEndpoint() *url.URL {
return c.endpoint
}
// GetBaseURL returns the base URL set in the SSE constructor.
func (c *SSE) GetBaseURL() *url.URL {
return c.baseURL
}
// GetOAuthHandler returns the OAuth handler if configured
func (c *SSE) GetOAuthHandler() *OAuthHandler {
return c.oauthHandler
}
// IsOAuthEnabled returns true if OAuth is enabled
func (c *SSE) IsOAuthEnabled() bool {
return c.oauthHandler != nil
}