From 546b25be88811e6c7c240345d18cb4ce5ce0eb13 Mon Sep 17 00:00:00 2001 From: 4444jPPP Date: Fri, 27 Mar 2026 14:29:26 -0400 Subject: [PATCH 1/2] fix: accept single supported content type in SSE mode Accept header validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relax the Accept header validation for SSE-mode POST requests from requiring both application/json AND text/event-stream to requiring at least one. This restores compatibility with clients that send only Accept: text/event-stream (e.g. Anthropic's MCP proxy used by Claude.ai for remote MCP integrations). The MCP spec uses SHOULD (not MUST) for clients accepting both content types. The server already negotiates the response format based on the message type — notifications/responses get JSON 202s, requests get SSE streams — so requiring both types in the Accept header is stricter than necessary. Closes #2349 --- src/mcp/server/streamable_http.py | 6 ++--- ...est_1363_race_condition_streamable_http.py | 16 ++++++------ tests/shared/test_streamable_http.py | 25 +++++++++++++++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index aa99e7c88..43ec05fdd 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -426,10 +426,10 @@ async def _validate_accept_header(self, request: Request, scope: Scope, send: Se ) await response(scope, request.receive, send) return False - # For SSE responses, require both content types - elif not (has_json and has_sse): + # For SSE responses, require at least one supported content type + elif not (has_json or has_sse): response = self._create_error_response( - "Not Acceptable: Client must accept both application/json and text/event-stream", + "Not Acceptable: Client must accept application/json or text/event-stream", HTTPStatus.NOT_ACCEPTABLE, ) await response(scope, request.receive, send) diff --git a/tests/issues/test_1363_race_condition_streamable_http.py b/tests/issues/test_1363_race_condition_streamable_http.py index db2a82d07..4d2082ac4 100644 --- a/tests/issues/test_1363_race_condition_streamable_http.py +++ b/tests/issues/test_1363_race_condition_streamable_http.py @@ -137,7 +137,7 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi # Suppress WARNING logs (expected validation errors) and capture ERROR logs with caplog.at_level(logging.ERROR): - # Test with missing text/event-stream in Accept header + # Test with only application/json in Accept header (valid — single supported type) async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0 ) as client: @@ -145,14 +145,14 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi "/", json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}, headers={ - "Accept": "application/json", # Missing text/event-stream + "Accept": "application/json", "Content-Type": "application/json", }, ) - # Should get 406 Not Acceptable due to missing text/event-stream - assert response.status_code == 406 + # Single supported Accept type is sufficient + assert response.status_code == 200 - # Test with missing application/json in Accept header + # Test with only text/event-stream in Accept header (valid — single supported type) async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0 ) as client: @@ -160,12 +160,12 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi "/", json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}, headers={ - "Accept": "text/event-stream", # Missing application/json + "Accept": "text/event-stream", "Content-Type": "application/json", }, ) - # Should get 406 Not Acceptable due to missing application/json - assert response.status_code == 406 + # Single supported Accept type is sufficient + assert response.status_code == 200 # Test with completely invalid Accept header async with httpx.AsyncClient( diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index f8ca30441..a30d262c8 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -612,8 +612,7 @@ def test_accept_header_wildcard(basic_server: None, basic_server_url: str, accep "accept_header", [ "text/html", - "application/*", - "text/*", + "image/png", ], ) def test_accept_header_incompatible(basic_server: None, basic_server_url: str, accept_header: str): @@ -630,6 +629,28 @@ def test_accept_header_incompatible(basic_server: None, basic_server_url: str, a assert "Not Acceptable" in response.text +@pytest.mark.parametrize( + "accept_header", + [ + "text/event-stream", + "application/json", + "application/*", + "text/*", + ], +) +def test_accept_header_single_type(basic_server: None, basic_server_url: str, accept_header: str): + """Test that a single supported Accept type is sufficient for SSE mode.""" + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": accept_header, + "Content-Type": "application/json", + }, + json=INIT_REQUEST, + ) + assert response.status_code == 200 + + def test_content_type_validation(basic_server: None, basic_server_url: str): """Test that Content-Type header is properly validated.""" # Test with incorrect Content-Type From 9d4df6b86e94b0b74ce1b3d19466536c485f5c79 Mon Sep 17 00:00:00 2001 From: 4444jPPP Date: Thu, 23 Apr 2026 12:11:57 -0400 Subject: [PATCH 2/2] Add repository standards (editorconfig, gitattributes) Co-Authored-By: Claude Opus 4.6 (1M context) --- .editorconfig | 34 ++++++++++++++++++++++++ .gitattributes | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1ac860ea6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# EditorConfig — https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.{py,pyi}] +indent_size = 4 + +[*.{go}] +indent_style = tab + +[*.{rs}] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab + +[*.{sh,bash,zsh}] +indent_size = 2 + +[*.{yaml,yml}] +indent_size = 2 + +[*.{toml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..0f119ae0a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,71 @@ +# Auto-detect text files and normalize line endings +* text=auto + +# Source code +*.py text diff=python +*.ts text diff=typescript +*.tsx text diff=typescript +*.js text diff=javascript +*.jsx text diff=javascript +*.go text diff=go +*.rs text diff=rust +*.rb text diff=ruby +*.sh text eol=lf diff=bash +*.bash text eol=lf diff=bash +*.zsh text eol=lf diff=bash +*.lua text + +# Configuration +*.json text +*.yaml text +*.yml text +*.toml text +*.cfg text +*.ini text +*.conf text +*.env text + +# Templates +*.tmpl text +*.j2 text + +# Documentation +*.md text diff=markdown +*.txt text +*.rst text + +# Web +*.html text diff=html +*.css text diff=css +*.scss text diff=css +*.astro text +*.svg text + +# Build / lock files +package-lock.json text -diff +yarn.lock text -diff +Cargo.lock text -diff +poetry.lock text -diff + +# Binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.pdf binary +*.zip binary +*.gz binary +*.tar binary +*.db binary +*.sqlite binary + +# Force LF for scripts +Makefile text eol=lf +Dockerfile text eol=lf +justfile text eol=lf