-
Notifications
You must be signed in to change notification settings - Fork 427
mcp: HTTP Header Standardization for x-mcp-header #915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
maciej-kisiel
merged 33 commits into
main
from
guglielmoc/SEP-2243_http_standardization_2
May 8, 2026
Merged
Changes from 22 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
f00d0f4
refactor: centralize MCP headers and add support for validating stand…
guglielmo-san d00288d
fix
guglielmo-san 57659c0
formatter
guglielmo-san 017e0fc
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization
guglielmo-san 604f2d4
fix after merge
guglielmo-san 7df5ab6
refactor: decouple standard header population from setMCPHeaders in s…
guglielmo-san 9de3bec
feat: implement SEP-2243 header encoding and support Mcp-Param- heade…
guglielmo-san f429bc5
feat: implement validation for tool-specific parameter headers in MCP…
guglielmo-san ad17562
feat: implement base64 header decoding and standardize primitive valu…
guglielmo-san a4e1e23
refactor: simplify MCP header logic and remove redundant tool cache a…
guglielmo-san aeada36
refactor: prohibit x-mcp-header annotations on nested object properti…
guglielmo-san b223143
refactor: rename mcp_http_headers to streamable_headers
guglielmo-san 005d33d
Merge remote-tracking branch 'origin/main' into guglielmoc/SEP-2243_h…
guglielmo-san 24cc607
feat: enable MCP parameter headers and add validation tests using int…
guglielmo-san 9dd6907
fix: add mutex protection to toolCache and provide thread-safe access…
guglielmo-san 8d4e94d
test: remove assertion for non-annotated query parameter headers in s…
guglielmo-san d8c5a76
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization_2
guglielmo-san e754ff7
refactor: remove redundant blank line in streamable_headers.go
guglielmo-san 87093cb
Merge remote-tracking branch 'refs/remotes/origin/guglielmoc/SEP-2243…
guglielmo-san 04652ee
merge tests
guglielmo-san d7ef2c1
minor fix
guglielmo-san c218557
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization_2
guglielmo-san e83292b
refactor: inline header encoding logic and update tool parameter head…
guglielmo-san 9bdd1df
refactor: remove unused mcpHeaderExtension constant from streamable h…
guglielmo-san 2faa9a9
refactor: expose ToolLookup method on Server and simplify connection …
guglielmo-san 8d68d57
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization_2
guglielmo-san bc2124a
refactor: update tool lookup mechanism to include header validation a…
guglielmo-san aefecc0
refactor: simplify encodeHeaderValue interface and update AddTool to …
guglielmo-san 9d69fe1
refactor: update encodeHeaderValue and primitiveToString to return su…
guglielmo-san 7fe797d
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization_2
guglielmo-san f51b46c
Update mcp/streamable_headers.go
guglielmo-san 0544a2c
Merge branch 'main' into guglielmoc/SEP-2243_http_standardization_2
guglielmo-san 40d050b
test: update streamable headers tests to expect nil instead of empty …
guglielmo-san File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
maciej-kisiel marked this conversation as resolved.
Outdated
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // Copyright 2025 The Go MCP SDK Authors. All rights reserved. | ||
| // Use of this source code is governed by the license | ||
| // that can be found in the LICENSE file. | ||
|
|
||
| package mcp | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "fmt" | ||
| "strings" | ||
| ) | ||
|
|
||
| const ( | ||
| base64Prefix = "=?base64?" | ||
| base64Suffix = "?=" | ||
| ) | ||
|
|
||
| // encodeHeaderValue converts a parameter value to an HTTP header-safe string | ||
| // per the SEP-2243 encoding rules: | ||
| // - string: used as-is if safe ASCII, otherwise Base64 encoded | ||
| // - number (float64): decimal string representation | ||
| // - bool: lowercase "true" or "false" | ||
| // - nil: returns "", false | ||
| // | ||
| // Values that contain non-ASCII characters, control characters, or | ||
| // leading/trailing whitespace are Base64-encoded with the =?base64?...?= wrapper. | ||
| func encodeHeaderValue(value any) (string, bool) { | ||
|
guglielmo-san marked this conversation as resolved.
Outdated
|
||
| var s string | ||
| switch v := value.(type) { | ||
| case string: | ||
| s = v | ||
| case float64: | ||
| s = fmt.Sprintf("%g", v) | ||
| case bool: | ||
| if v { | ||
|
guglielmo-san marked this conversation as resolved.
Outdated
|
||
| s = "true" | ||
| } else { | ||
| s = "false" | ||
| } | ||
| default: | ||
| return "", false | ||
| } | ||
|
|
||
| if requiresBase64Encoding(s) { | ||
| return encodeBase64(s), true | ||
| } | ||
| return s, true | ||
| } | ||
|
|
||
| // decodeHeaderValue decodes a header value that may be Base64-encoded | ||
| // with the =?base64?...?= wrapper. | ||
| func decodeHeaderValue(headerValue string) (string, bool) { | ||
|
guglielmo-san marked this conversation as resolved.
Outdated
|
||
| if len(headerValue) == 0 { | ||
| return headerValue, true | ||
| } | ||
|
|
||
| if strings.HasPrefix(strings.ToLower(headerValue), strings.ToLower(base64Prefix)) && | ||
|
guglielmo-san marked this conversation as resolved.
Outdated
|
||
| strings.HasSuffix(headerValue, base64Suffix) { | ||
| encoded := headerValue[len(base64Prefix) : len(headerValue)-len(base64Suffix)] | ||
| decoded, err := base64.StdEncoding.DecodeString(encoded) | ||
| if err != nil { | ||
| return "", false | ||
| } | ||
| return string(decoded), true | ||
| } | ||
| return headerValue, true | ||
| } | ||
|
|
||
| func requiresBase64Encoding(s string) bool { | ||
| if len(s) == 0 { | ||
| return false | ||
| } | ||
| if s[0] == ' ' || s[0] == '\t' || s[len(s)-1] == ' ' || s[len(s)-1] == '\t' { | ||
| return true | ||
| } | ||
| for _, c := range s { | ||
| if c < 0x20 || c > 0x7E { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func encodeBase64(s string) string { | ||
| return base64Prefix + base64.StdEncoding.EncodeToString([]byte(s)) + base64Suffix | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // Copyright 2025 The Go MCP SDK Authors. All rights reserved. | ||
| // Use of this source code is governed by the license | ||
| // that can be found in the LICENSE file. | ||
|
|
||
| package mcp | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestEncodeHeaderValue(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| value any | ||
| want string | ||
| wantOK bool | ||
| }{ | ||
| // Strings | ||
| {"plain ASCII", "us-west1", "us-west1", true}, | ||
| {"empty string", "", "", true}, | ||
| {"string with internal spaces", "us west 1", "us west 1", true}, | ||
| {"string with leading space", " us-west1", "=?base64?IHVzLXdlc3Qx?=", true}, | ||
| {"string with trailing space", "us-west1 ", "=?base64?dXMtd2VzdDEg?=", true}, | ||
| {"string with both spaces", " us-west1 ", "=?base64?IHVzLXdlc3QxIA==?=", true}, | ||
| {"non-ASCII", "日本語", "=?base64?5pel5pys6Kqe?=", true}, | ||
| {"mixed ASCII and non-ASCII", "Hello, 世界", "=?base64?SGVsbG8sIOS4lueVjA==?=", true}, | ||
| {"string with newline", "line1\nline2", "=?base64?bGluZTEKbGluZTI=?=", true}, | ||
| {"string with carriage return", "line1\r\nline2", "=?base64?bGluZTENCmxpbmUy?=", true}, | ||
| {"string with leading tab", "\tindented", "=?base64?CWluZGVudGVk?=", true}, | ||
|
|
||
| // Numbers | ||
| {"integer", float64(42), "42", true}, | ||
| {"float", float64(3.14159), "3.14159", true}, | ||
|
|
||
| // Booleans | ||
| {"true", true, "true", true}, | ||
| {"false", false, "false", true}, | ||
|
|
||
| // Unsupported types | ||
| {"nil", nil, "", false}, | ||
| {"slice", []string{"a"}, "", false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, ok := encodeHeaderValue(tt.value) | ||
| if ok != tt.wantOK { | ||
| t.Fatalf("encodeHeaderValue(%v) ok = %v, want %v", tt.value, ok, tt.wantOK) | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("encodeHeaderValue(%v) = %q, want %q", tt.value, got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestDecodeHeaderValue(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| want string | ||
| wantOK bool | ||
| }{ | ||
| {"plain value", "us-west1", "us-west1", true}, | ||
| {"empty value", "", "", true}, | ||
| {"valid base64", "=?base64?SGVsbG8=?=", "Hello", true}, | ||
| {"non-ASCII decoded", "=?base64?5pel5pys6Kqe?=", "日本語", true}, | ||
| {"leading space decoded", "=?base64?IHVzLXdlc3Qx?=", " us-west1", true}, | ||
| {"case-insensitive prefix", "=?BASE64?SGVsbG8=?=", "Hello", true}, | ||
| {"invalid base64 chars", "=?base64?SGVs!!!bG8=?=", "", false}, | ||
| // Missing prefix or suffix: treated as literal values, not base64 | ||
| {"missing prefix", "SGVsbG8=", "SGVsbG8=", true}, | ||
| {"missing suffix", "=?base64?SGVsbG8=", "=?base64?SGVsbG8=", true}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, ok := decodeHeaderValue(tt.input) | ||
| if ok != tt.wantOK { | ||
| t.Fatalf("decodeHeaderValue(%q) ok = %v, want %v", tt.input, ok, tt.wantOK) | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("decodeHeaderValue(%q) = %q, want %q", tt.input, got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestEncodeDecodeRoundTrip(t *testing.T) { | ||
| values := []string{ | ||
| "us-west1", | ||
| "", | ||
| " leading", | ||
| "trailing ", | ||
| "Hello, 世界", | ||
| "line1\nline2", | ||
| "\ttab", | ||
| } | ||
| for _, v := range values { | ||
| encoded, ok := encodeHeaderValue(v) | ||
| if !ok { | ||
| t.Fatalf("encodeHeaderValue(%q) failed", v) | ||
| } | ||
| decoded, ok := decodeHeaderValue(encoded) | ||
| if !ok { | ||
| t.Fatalf("decodeHeaderValue(%q) failed", encoded) | ||
| } | ||
| if decoded != v { | ||
| t.Errorf("round-trip failed: %q -> %q -> %q", v, encoded, decoded) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.