parent
59185e6969
commit
2fdabde139
4 changed files with 133 additions and 2 deletions
|
|
@ -1962,7 +1962,7 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
|||
}
|
||||
}
|
||||
default:
|
||||
if !initialized && !validatedMeta.usesNewProtocol {
|
||||
if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() {
|
||||
ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method)
|
||||
return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -560,7 +560,15 @@ type validatedMeta struct {
|
|||
// a non-nil error. clientInfo is optional; if present but invalid, an error is
|
||||
// returned. Otherwise, it returns usesNewProtocol set to true and the populated
|
||||
// initializeParams.
|
||||
// Notifications always report usesNewProtocol false and never error.
|
||||
func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
||||
// SEP-2575 defines the `_meta` triple for calls only: NotificationParams
|
||||
// declares `_meta` optional with no protocolVersion, so a notification
|
||||
// neither selects the new protocol nor is discarded for carrying an
|
||||
// incomplete triple.
|
||||
if !req.IsCall() {
|
||||
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||
}
|
||||
meta := extractRequestMeta(req.Params)
|
||||
if meta == nil {
|
||||
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||
|
|
|
|||
|
|
@ -1530,12 +1530,15 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques
|
|||
// The new (>= 2026-07-28) protocol is supported on the HTTP transport
|
||||
// only when [StreamableHTTPOptions.Stateless] is true.
|
||||
//
|
||||
// The `_meta` triple is defined for calls only, notifications are
|
||||
// excluded.
|
||||
//
|
||||
// TODO: this validation can be moved within validateMcpHeaders.
|
||||
var metaVersion string
|
||||
if meta := extractRequestMeta(jreq.Params); meta != nil {
|
||||
metaVersion, _ = meta[MetaKeyProtocolVersion].(string)
|
||||
}
|
||||
if protocolVersion >= protocolVersion20260728 || metaVersion != "" {
|
||||
if (jreq.IsCall() && protocolVersion >= protocolVersion20260728) || metaVersion != "" {
|
||||
// Extract again the protocol version from the context to see what the client
|
||||
// is advertising in the Mcp-Protocol-Version HTTP header.
|
||||
headerVersion := protocolVersionFromContext(req.Context())
|
||||
|
|
|
|||
|
|
@ -3712,6 +3712,126 @@ func TestStreamableStateless_AcceptsNewProtocol(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestStreamableStateless_NotificationMetaValidation checks that the SEP-2575
|
||||
// per-request `_meta` triple is required of calls only. NotificationParams
|
||||
// declares `_meta` optional with no protocolVersion, so a notification that
|
||||
// omits it is well-formed and must be accepted with 202.
|
||||
func TestStreamableStateless_NotificationMetaValidation(t *testing.T) {
|
||||
newProtocolMeta := map[string]any{
|
||||
MetaKeyProtocolVersion: protocolVersion20260728,
|
||||
MetaKeyClientInfo: map[string]any{"name": "new-proto-client", "version": "9.9"},
|
||||
MetaKeyClientCapabilities: map[string]any{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message map[string]any
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "cancelled without meta",
|
||||
message: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": notificationCancelled,
|
||||
"params": map[string]any{"requestID": 1, "reason": "context canceled"},
|
||||
},
|
||||
wantStatus: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
name: "progress without meta",
|
||||
message: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": notificationProgress,
|
||||
"params": map[string]any{"progressToken": "t", "progress": 1},
|
||||
},
|
||||
wantStatus: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
name: "notification with matching meta",
|
||||
message: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": notificationCancelled,
|
||||
"params": map[string]any{
|
||||
"_meta": newProtocolMeta,
|
||||
"requestID": 1,
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
// A notification that volunteers a version is still held to the
|
||||
// header-match rule, so relaxing the requirement does not open a
|
||||
// hole for inconsistent messages.
|
||||
name: "notification with mismatched meta",
|
||||
message: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": notificationCancelled,
|
||||
"params": map[string]any{
|
||||
"_meta": map[string]any{
|
||||
MetaKeyProtocolVersion: "2025-06-18",
|
||||
MetaKeyClientCapabilities: map[string]any{},
|
||||
},
|
||||
"requestID": 1,
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
// Calls remain subject to the requirement.
|
||||
name: "call without meta",
|
||||
message: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": map[string]any{},
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
server := NewServer(testImpl, nil)
|
||||
AddTool(server, &Tool{Name: "noop"},
|
||||
func(ctx context.Context, req *CallToolRequest, args struct{}) (*CallToolResult, any, error) {
|
||||
return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil, nil
|
||||
})
|
||||
handler := NewStreamableHTTPHandler(
|
||||
func(*http.Request) *Server { return server },
|
||||
&StreamableHTTPOptions{Stateless: true},
|
||||
)
|
||||
httpServer := httptest.NewServer(handler)
|
||||
defer httpServer.Close()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, httpServer.URL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||
req.Header.Set(protocolVersionHeader, protocolVersion20260728)
|
||||
req.Header.Set(methodHeader, test.message["method"].(string))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != test.wantStatus {
|
||||
t.Fatalf("status = %d, want %d; body = %s", resp.StatusCode, test.wantStatus, respBody)
|
||||
}
|
||||
if test.wantStatus == http.StatusAccepted && len(respBody) > 0 {
|
||||
t.Errorf("accepted notification returned body %q, want empty", respBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamableClientUnsupportedVersionFallback exercises the full
|
||||
// SEP-2575 fallback. The client requests protocolVersion20260728, which the
|
||||
// server is configured to NOT advertise in supportedProtocolVersions for the
|
||||
|
|
|
|||
Loading…
Reference in a new issue