Skip to content

Commit e01639a

Browse files
feat: HTTP Header Standardization for method and name (#907)
## Description Implements SEP-2243 (https://modelcontextprotocol.io/seps/2243-http-standardization) (HTTP Header Standardization) for Mcp-Method and Mcp-Name standard headers (x-mcp-param custom headers will be covvered in separate PR). Fixes #905
1 parent 93a41b2 commit e01639a

6 files changed

Lines changed: 798 additions & 10 deletions

File tree

mcp/shared.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
// It is the version that the client sends in the initialization request, and
3737
// the default version used by the server.
3838
latestProtocolVersion = protocolVersion20251125
39+
protocolVersion20260630 = "2026-06-30"
3940
protocolVersion20251125 = "2025-11-25"
4041
protocolVersion20250618 = "2025-06-18"
4142
protocolVersion20250326 = "2025-03-26"
@@ -343,6 +344,9 @@ func clientSessionMethod[P Params, R Result](f func(*ClientSession, context.Cont
343344

344345
// MCP-specific error codes.
345346
const (
347+
// CodeHeaderMismatch indicates that HTTP headers do not match the corresponding values
348+
// in the request body, or that required headers are missing or malformed.
349+
CodeHeaderMismatch = -32001
346350
// CodeResourceNotFound indicates that a requested resource could not be found.
347351
CodeResourceNotFound = -32002
348352
// CodeURLElicitationRequired indicates that the server requires URL elicitation

mcp/streamable.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,6 @@ import (
3939
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
4040
)
4141

42-
const (
43-
protocolVersionHeader = "Mcp-Protocol-Version"
44-
sessionIDHeader = "Mcp-Session-Id"
45-
lastEventIDHeader = "Last-Event-ID"
46-
)
47-
4842
// A StreamableHTTPHandler is an http.Handler that serves streamable MCP
4943
// sessions, as defined by the [MCP spec].
5044
//
@@ -1191,6 +1185,24 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques
11911185
}
11921186
}
11931187

1188+
// Validate MCP standard headers (Mcp-Method, Mcp-Name)
1189+
if !isBatch && len(incoming) == 1 {
1190+
if err := validateMcpHeaders(req.Header, incoming[0]); err != nil {
1191+
resp := &jsonrpc.Response{
1192+
Error: jsonrpc2.NewError(CodeHeaderMismatch, err.Error()),
1193+
}
1194+
if jreq, ok := incoming[0].(*jsonrpc.Request); ok {
1195+
resp.ID = jreq.ID
1196+
}
1197+
w.Header().Set("Content-Type", "application/json")
1198+
w.WriteHeader(http.StatusBadRequest)
1199+
if data, err := jsonrpc2.EncodeMessage(resp); err == nil {
1200+
w.Write(data)
1201+
}
1202+
return
1203+
}
1204+
}
1205+
11941206
// The prime and close events were added in protocol version 2025-11-25 (SEP-1699).
11951207
// Use the version from InitializeParams if this is an initialize request,
11961208
// otherwise use the protocol version header.
@@ -1797,6 +1809,9 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
17971809
// and permanently break the connection.
17981810
return nil, nil, fmt.Errorf("%s: %w: %w", requestSummary, jsonrpc2.ErrRejected, err)
17991811
}
1812+
// Keep this after the setMCPHeaders call to ensure that the
1813+
// protocol version header is set.
1814+
setStandardHeaders(req.Header, msg)
18001815
resp, err := c.client.Do(req)
18011816
if err != nil {
18021817
// Any error from client.Do means the request didn't reach the server.
@@ -1932,6 +1947,7 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
19321947
if c.sessionID != "" {
19331948
req.Header.Set(sessionIDHeader, c.sessionID)
19341949
}
1950+
19351951
return nil
19361952
}
19371953

mcp/streamable_client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (s *fakeStreamableServer) ServeHTTP(w http.ResponseWriter, req *http.Reques
7070
key := streamableRequestKey{
7171
httpMethod: req.Method,
7272
sessionID: req.Header.Get(sessionIDHeader),
73-
lastEventID: req.Header.Get("Last-Event-ID"), // TODO: extract this to a constant, like sessionIDHeader
73+
lastEventID: req.Header.Get(lastEventIDHeader),
7474
}
7575
var jsonrpcReq *jsonrpc.Request
7676
if req.Method == http.MethodPost {

mcp/streamable_headers.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by the license
3+
// that can be found in the LICENSE file.
4+
5+
package mcp
6+
7+
import (
8+
"encoding/json"
9+
"errors"
10+
"fmt"
11+
"net/http"
12+
13+
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
14+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
15+
)
16+
17+
const (
18+
protocolVersionHeader = "Mcp-Protocol-Version"
19+
sessionIDHeader = "Mcp-Session-Id"
20+
lastEventIDHeader = "Last-Event-ID"
21+
methodHeader = "Mcp-Method"
22+
nameHeader = "Mcp-Name"
23+
minVersionForStandardHeaders = protocolVersion20260630
24+
)
25+
26+
func extractName(method string, params json.RawMessage) (string, bool) {
27+
switch method {
28+
case "tools/call":
29+
var p CallToolParams
30+
if err := internaljson.Unmarshal(params, &p); err == nil {
31+
return p.Name, true
32+
}
33+
case "prompts/get":
34+
var p GetPromptParams
35+
if err := internaljson.Unmarshal(params, &p); err == nil {
36+
return p.Name, true
37+
}
38+
case "resources/read":
39+
var p ReadResourceParams
40+
if err := internaljson.Unmarshal(params, &p); err == nil {
41+
return p.URI, true
42+
}
43+
}
44+
45+
return "", false
46+
}
47+
48+
// setStandardHeaders populates standard MCP headers.
49+
// It requires the protocol version header to be set.
50+
func setStandardHeaders(header http.Header, msg jsonrpc.Message) {
51+
if msg == nil {
52+
return
53+
}
54+
if header.Get(protocolVersionHeader) == "" || header.Get(protocolVersionHeader) < minVersionForStandardHeaders {
55+
return
56+
}
57+
58+
switch msg := msg.(type) {
59+
case *jsonrpc.Request:
60+
header.Set(methodHeader, msg.Method)
61+
if name, ok := extractName(msg.Method, msg.Params); ok {
62+
header.Set(nameHeader, name)
63+
}
64+
}
65+
}
66+
67+
func validateMcpHeaders(header http.Header, msg jsonrpc.Message) error {
68+
protocolVersion := header.Get(protocolVersionHeader)
69+
if protocolVersion == "" || protocolVersion < minVersionForStandardHeaders {
70+
return nil
71+
}
72+
73+
switch msg := msg.(type) {
74+
case *jsonrpc.Request:
75+
methodInHeader := header.Get(methodHeader)
76+
if methodInHeader == "" {
77+
return errors.New("missing required Mcp-Method header")
78+
}
79+
if methodInHeader != msg.Method {
80+
return fmt.Errorf("header mismatch: Mcp-Method header value '%s' does not match body value '%s'", methodInHeader, msg.Method)
81+
}
82+
83+
if msg.Method == "tools/call" || msg.Method == "resources/read" || msg.Method == "prompts/get" {
84+
nameInHeader := header.Get(nameHeader)
85+
if nameInHeader == "" {
86+
return fmt.Errorf("missing required Mcp-Name header for method %q", msg.Method)
87+
}
88+
nameInBody, ok := extractName(msg.Method, msg.Params)
89+
if !ok {
90+
return fmt.Errorf("failed to extract name from parameters for method %q", msg.Method)
91+
}
92+
if nameInHeader != nameInBody {
93+
return fmt.Errorf("header mismatch: Mcp-Name header value '%s' does not match body value '%s'", nameInHeader, nameInBody)
94+
}
95+
}
96+
}
97+
return nil
98+
}

0 commit comments

Comments
 (0)