Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions src/mcp/server/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 8 additions & 8 deletions tests/issues/test_1363_race_condition_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,35 +137,35 @@ 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:
response = await client.post(
"/",
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:
response = await client.post(
"/",
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(
Expand Down
25 changes: 23 additions & 2 deletions tests/shared/test_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
Loading