Skip to content

Commit 559201d

Browse files
committed
mcp: handle the Mcp-Protocol-Version header correctly
Handle the protocol version header according to section 2.7 of the spec (and the other SDKs). Fixes #198
1 parent 1a54234 commit 559201d

File tree

4 files changed

+192
-65
lines changed

4 files changed

+192
-65
lines changed

mcp/server.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -850,18 +850,11 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
850850
state.InitializeParams = params
851851
})
852852

853-
// If we support the client's version, reply with it. Otherwise, reply with our
854-
// latest version.
855-
version := params.ProtocolVersion
856-
if !slices.Contains(supportedProtocolVersions, params.ProtocolVersion) {
857-
version = latestProtocolVersion
858-
}
859-
860853
s := ss.server
861854
return &InitializeResult{
862855
// TODO(rfindley): alter behavior when falling back to an older version:
863856
// reject unsupported features.
864-
ProtocolVersion: version,
857+
ProtocolVersion: negotiatedVersion(params.ProtocolVersion),
865858
Capabilities: s.capabilities(),
866859
Instructions: s.opts.Instructions,
867860
ServerInfo: s.impl,

mcp/shared.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,36 @@ import (
2323
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2424
)
2525

26-
// latestProtocolVersion is the latest protocol version that this version of the SDK supports.
27-
// It is the version that the client sends in the initialization request.
28-
const latestProtocolVersion = "2025-06-18"
26+
const (
27+
// latestProtocolVersion is the latest protocol version that this version of
28+
// the SDK supports.
29+
//
30+
// It is the version that the client sends in the initialization request, and
31+
// the default version used by the server.
32+
latestProtocolVersion = protocolVersion20250618
33+
protocolVersion20250618 = "2025-06-18"
34+
protocolVersion20250326 = "2025-03-26"
35+
protocolVersion20251105 = "2024-11-05"
36+
)
2937

3038
var supportedProtocolVersions = []string{
31-
latestProtocolVersion,
32-
"2025-03-26",
33-
"2024-11-05",
39+
protocolVersion20250618,
40+
protocolVersion20250326,
41+
protocolVersion20251105,
42+
}
43+
44+
// negotiatedVersion returns the effective protocol version to use, given a
45+
// client version.
46+
func negotiatedVersion(clientVersion string) string {
47+
// In general, prefer to use the clientVersion, but if we don't support the
48+
// client's version, use the latest version.
49+
//
50+
// This handles the case where a new spec version is released, and the SDK
51+
// does not support it yet.
52+
if !slices.Contains(supportedProtocolVersions, clientVersion) {
53+
return latestProtocolVersion
54+
}
55+
return clientVersion
3456
}
3557

3658
// A MethodHandler handles MCP messages.

mcp/streamable.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"math"
1616
"math/rand/v2"
1717
"net/http"
18+
"slices"
1819
"strconv"
1920
"strings"
2021
"sync"
@@ -152,7 +153,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
152153

153154
if req.Method == http.MethodDelete {
154155
if sessionID == "" {
155-
http.Error(w, "DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
156+
http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
156157
return
157158
}
158159
if transport != nil { // transport may be nil in stateless mode
@@ -172,8 +173,45 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
172173
return
173174
}
174175
default:
175-
w.Header().Set("Allow", "GET, POST")
176-
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
176+
w.Header().Set("Allow", "GET, POST, DELETE")
177+
http.Error(w, "Method Not Allowed: streamable MCP servers support GET, POST, and DELETE requests", http.StatusMethodNotAllowed)
178+
return
179+
}
180+
181+
// Section 2.7 of the spec (2025-06-18) states:
182+
//
183+
// "If using HTTP, the client MUST include the MCP-Protocol-Version:
184+
// <protocol-version> HTTP header on all subsequent requests to the MCP
185+
// server, allowing the MCP server to respond based on the MCP protocol
186+
// version.
187+
//
188+
// For example: MCP-Protocol-Version: 2025-06-18
189+
// The protocol version sent by the client SHOULD be the one negotiated during
190+
// initialization.
191+
//
192+
// For backwards compatibility, if the server does not receive an
193+
// MCP-Protocol-Version header, and has no other way to identify the version -
194+
// for example, by relying on the protocol version negotiated during
195+
// initialization - the server SHOULD assume protocol version 2025-03-26.
196+
//
197+
// If the server receives a request with an invalid or unsupported
198+
// MCP-Protocol-Version, it MUST respond with 400 Bad Request."
199+
//
200+
// Since this wasn't present in the 2025-03-26 version of the spec, this
201+
// effectively means:
202+
// 1. IF the client provides a version header, it must be a supported
203+
// version.
204+
// 2. In stateless mode, where we've lost the state of the initialize
205+
// request, we assume that whatever the client tells us is the truth (or
206+
// assume 2025-03-26 if the client doesn't say anything).
207+
//
208+
// This logic matches the typescript SDK.
209+
protocolVersion := req.Header.Get(protocolVersionHeader)
210+
if protocolVersion == "" {
211+
protocolVersion = protocolVersion20250326
212+
}
213+
if !slices.Contains(supportedProtocolVersions, protocolVersion) {
214+
http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)
177215
return
178216
}
179217

@@ -234,7 +272,9 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
234272
// set the initial state to a default value.
235273
state := new(ServerSessionState)
236274
if !hasInitialize {
237-
state.InitializeParams = new(InitializeParams)
275+
state.InitializeParams = &InitializeParams{
276+
ProtocolVersion: protocolVersion,
277+
}
238278
}
239279
if !hasInitialized {
240280
state.InitializedParams = new(InitializedParams)
@@ -377,11 +417,12 @@ type streamableServerConn struct {
377417
eventStore EventStore
378418

379419
incoming chan jsonrpc.Message // messages from the client to the server
380-
done chan struct{}
381420

382-
mu sync.Mutex
421+
mu sync.Mutex // guards all fields below
422+
383423
// Sessions are closed exactly once.
384424
isDone bool
425+
done chan struct{}
385426

386427
// Sessions can have multiple logical connections (which we call streams),
387428
// corresponding to HTTP requests. Additionally, streams may be resumed by

0 commit comments

Comments
 (0)