Skip to content

Commit 79f063b

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 3f10c19 commit 79f063b

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
@@ -859,18 +859,11 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
859859
state.InitializeParams = params
860860
})
861861

862-
// If we support the client's version, reply with it. Otherwise, reply with our
863-
// latest version.
864-
version := params.ProtocolVersion
865-
if !slices.Contains(supportedProtocolVersions, params.ProtocolVersion) {
866-
version = latestProtocolVersion
867-
}
868-
869862
s := ss.server
870863
return &InitializeResult{
871864
// TODO(rfindley): alter behavior when falling back to an older version:
872865
// reject unsupported features.
873-
ProtocolVersion: version,
866+
ProtocolVersion: negotiatedVersion(params.ProtocolVersion),
874867
Capabilities: s.capabilities(),
875868
Instructions: s.opts.Instructions,
876869
ServerInfo: s.impl,

mcp/shared.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,36 @@ import (
2727
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2828
)
2929

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

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

4062
// 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"
@@ -153,7 +154,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
153154

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

@@ -235,7 +273,9 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
235273
// set the initial state to a default value.
236274
state := new(ServerSessionState)
237275
if !hasInitialize {
238-
state.InitializeParams = new(InitializeParams)
276+
state.InitializeParams = &InitializeParams{
277+
ProtocolVersion: protocolVersion,
278+
}
239279
}
240280
if !hasInitialized {
241281
state.InitializedParams = new(InitializedParams)
@@ -378,11 +418,12 @@ type streamableServerConn struct {
378418
eventStore EventStore
379419

380420
incoming chan jsonrpc.Message // messages from the client to the server
381-
done chan struct{}
382421

383-
mu sync.Mutex
422+
mu sync.Mutex // guards all fields below
423+
384424
// Sessions are closed exactly once.
385425
isDone bool
426+
done chan struct{}
386427

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

0 commit comments

Comments
 (0)