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 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