mcp: Implement support for SEP-2575 on client side (#975)
Implements the client half of the SEP-2575 / `2026-06-30` protocol. This CL is client-only; server-side `server/discover` is still a `MethodNotFound` stub. ### Changes - **`server/discover` probe in `Client.Connect()`** (`client.go`). When the caller opts into `protocolVersion >= 2026-06-30`, the client probes the server with `server/discover` before falling back to the legacy `initialize` handshake. - **Per-request `_meta` injection** (`shared.go`, `injectRequestMeta`). For`>= 2026-06-30` sessions every outgoing request gets the SEP-2575 triple (`protocolVersion`, `clientInfo`, `clientCapabilities`). - **Server-side fix**: `ServerSession.handle` no longer pre-populates `state.InitializeParams` from a `server/discover` request's `_meta`, so a subsequent fallback `initialize` is not rejected as a duplicate. ### Fallback rules Client falls back to the legacy `initialize` handshake when discover returns: - `MethodNotFound` (-32601), - `UnsupportedProtocolVersionError` (-32004), - an HTTP `Bad Request` (substring-matched on the error message, to interop with pre-2026-06-30 servers). Fixes #966
This commit is contained in:
parent
d54751a8ee
commit
c60a318d22
1 changed files with 1010 additions and 96 deletions
|
|
@ -1844,6 +1844,13 @@ func (c *streamableClientConn) sessionUpdated(state clientSessionState) {
|
|||
c.initializedResult = state.InitializeResult
|
||||
c.mu.Unlock()
|
||||
|
||||
// When the protocol version is >= 2026-06-30, the standalone HTTP GET
|
||||
// SSE stream is removed.
|
||||
if state.InitializeResult == nil ||
|
||||
state.InitializeResult.ProtocolVersion >= protocolVersion20260630 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start the standalone SSE stream as soon as we have the initialized
|
||||
// result, if continuous listening is enabled.
|
||||
//
|
||||
|
|
@ -1903,7 +1910,7 @@ func (c *streamableClientConn) connectStandaloneSSE() {
|
|||
return
|
||||
}
|
||||
summary := "standalone SSE stream"
|
||||
if err := c.checkResponse(summary, resp); err != nil {
|
||||
if err := c.checkResponse(c.ctx, summary, resp); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
|
|
@ -2046,10 +2053,11 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
|
|||
}
|
||||
}
|
||||
|
||||
if err := c.checkResponse(requestSummary, resp); err != nil {
|
||||
if err := c.checkResponse(ctx, requestSummary, resp); err != nil {
|
||||
// Only fail the connection for non-transient errors.
|
||||
// Transient errors (wrapped with ErrRejected) should not break the connection.
|
||||
if !errors.Is(err, jsonrpc2.ErrRejected) {
|
||||
// ErrMethodNotFound and ErrUnsupportedProtocolVersion should not break the connection as they trigger the initialize fallback.
|
||||
if !errors.Is(err, jsonrpc2.ErrRejected) && !errors.Is(err, jsonrpc2.ErrMethodNotFound) && !errors.Is(err, jsonrpc2.ErrUnsupportedProtocolVersion) {
|
||||
c.fail(err)
|
||||
}
|
||||
return err
|
||||
|
|
@ -2143,6 +2151,8 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
|
|||
}
|
||||
if c.initializedResult != nil {
|
||||
req.Header.Set(protocolVersionHeader, c.initializedResult.ProtocolVersion)
|
||||
} else if v := protocolVersionFromContext(req.Context()); v != "" {
|
||||
req.Header.Set(protocolVersionHeader, v)
|
||||
}
|
||||
if c.sessionID != "" {
|
||||
req.Header.Set(sessionIDHeader, c.sessionID)
|
||||
|
|
@ -2228,7 +2238,7 @@ func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary str
|
|||
}
|
||||
|
||||
resp = newResp
|
||||
if err := c.checkResponse(requestSummary, resp); err != nil {
|
||||
if err := c.checkResponse(ctx, requestSummary, resp); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
|
|
@ -2239,7 +2249,7 @@ func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary str
|
|||
// translates it into an error if the request was unsuccessful.
|
||||
//
|
||||
// The response body is close if a non-nil error is returned.
|
||||
func (c *streamableClientConn) checkResponse(requestSummary string, resp *http.Response) (err error) {
|
||||
func (c *streamableClientConn) checkResponse(ctx context.Context, requestSummary string, resp *http.Response) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
|
|
@ -2259,6 +2269,19 @@ func (c *streamableClientConn) checkResponse(requestSummary string, resp *http.R
|
|||
return fmt.Errorf("%w: %s: %v", jsonrpc2.ErrRejected, requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
// Read the body and if we can detect vPre servers that
|
||||
// reject "server/discover" as unsupported method with a plain HTTP 400,
|
||||
// then return jsonrpc2.ErrMethodNotFound or jsonrpc2.ErrUnsupportedProtocolVersion to trigger the fallback.
|
||||
protocolVersion := protocolVersionFromContext(ctx)
|
||||
if protocolVersion != "" && protocolVersion >= protocolVersion20260630 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(string(body), fmt.Sprintf("%s: %q unsupported", jsonrpc2.ErrNotHandled, methodDiscover)) {
|
||||
return fmt.Errorf("%s: %w: %v", requestSummary, jsonrpc2.ErrMethodNotFound, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
if strings.Contains(string(body), "Unsupported protocol version") {
|
||||
return fmt.Errorf("%s: %w: %v", requestSummary, jsonrpc2.ErrUnsupportedProtocolVersion, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Reference in a new issue