-
Notifications
You must be signed in to change notification settings - Fork 301
Add CORS support for SSE transport #137
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
Open
jssmith
wants to merge
5
commits into
main
Choose a base branch
from
cors-support-revised
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9b5157b
add CORS support for local integration with browser clients
pbeast 67afab5
Fix CORS support: correct docs and clean up imports
jssmith 046dc63
Add pytest tests for CORS middleware
jssmith fbc8939
Use single quotes in f-string join for clarity
jssmith 6b5a122
Add end-to-end tests for CORS server startup
jssmith 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
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,136 @@ | ||
| """Tests for CORS support in SSE transport.""" | ||
|
|
||
| import pytest | ||
| from starlette.middleware.cors import CORSMiddleware | ||
| from starlette.testclient import TestClient | ||
|
|
||
| from postgres_mcp.server import mcp | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def app_with_cors(): | ||
| """Create an SSE app with CORS middleware configured.""" | ||
| app = mcp.sse_app() | ||
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=["https://claude.ai", "https://example.com"], | ||
| allow_methods=["GET", "POST", "OPTIONS"], | ||
| allow_headers=["*"], | ||
| ) | ||
| return app | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def app_without_cors(): | ||
| """Create an SSE app without CORS middleware.""" | ||
| return mcp.sse_app() | ||
|
|
||
|
|
||
| class TestCorsPreflightRequests: | ||
| """Test CORS preflight (OPTIONS) requests.""" | ||
|
|
||
| def test_preflight_allowed_origin_returns_cors_headers(self, app_with_cors): | ||
| """OPTIONS preflight from allowed origin should return CORS headers.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| response = client.options( | ||
| "/sse", | ||
| headers={ | ||
| "Origin": "https://claude.ai", | ||
| "Access-Control-Request-Method": "GET", | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| assert response.headers.get("access-control-allow-origin") == "https://claude.ai" | ||
| assert "GET" in response.headers.get("access-control-allow-methods", "") | ||
|
|
||
| def test_preflight_second_allowed_origin(self, app_with_cors): | ||
| """OPTIONS preflight from second allowed origin should also work.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| response = client.options( | ||
| "/sse", | ||
| headers={ | ||
| "Origin": "https://example.com", | ||
| "Access-Control-Request-Method": "GET", | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| assert response.headers.get("access-control-allow-origin") == "https://example.com" | ||
|
|
||
| def test_preflight_disallowed_origin_no_cors_header(self, app_with_cors): | ||
| """OPTIONS preflight from non-allowed origin should not return CORS header.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| response = client.options( | ||
| "/sse", | ||
| headers={ | ||
| "Origin": "https://malicious.com", | ||
| "Access-Control-Request-Method": "GET", | ||
| }, | ||
| ) | ||
| # The response may be 200 or 400, but should NOT have the allow-origin header | ||
| assert response.headers.get("access-control-allow-origin") is None | ||
|
|
||
| def test_preflight_messages_endpoint(self, app_with_cors): | ||
| """OPTIONS preflight on /messages/ endpoint should also work.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| response = client.options( | ||
| "/messages/", | ||
| headers={ | ||
| "Origin": "https://claude.ai", | ||
| "Access-Control-Request-Method": "POST", | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| assert response.headers.get("access-control-allow-origin") == "https://claude.ai" | ||
| assert "POST" in response.headers.get("access-control-allow-methods", "") | ||
|
|
||
|
|
||
| class TestCorsOnActualRequests: | ||
| """Test CORS headers on actual (non-preflight) requests.""" | ||
|
|
||
| def test_post_request_with_allowed_origin(self, app_with_cors): | ||
| """POST request from allowed origin should include CORS header in response.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| # Send a POST to /messages/ - it will fail (no valid session) but CORS headers should be present | ||
| response = client.post( | ||
| "/messages/", | ||
| headers={"Origin": "https://claude.ai"}, | ||
| content="test", | ||
| ) | ||
| # Even if the request fails, CORS headers should be present | ||
| assert response.headers.get("access-control-allow-origin") == "https://claude.ai" | ||
|
|
||
| def test_post_request_with_disallowed_origin(self, app_with_cors): | ||
| """POST request from non-allowed origin should not have CORS header.""" | ||
| client = TestClient(app_with_cors, raise_server_exceptions=False) | ||
| response = client.post( | ||
| "/messages/", | ||
| headers={"Origin": "https://malicious.com"}, | ||
| content="test", | ||
| ) | ||
| assert response.headers.get("access-control-allow-origin") is None | ||
|
|
||
|
|
||
| class TestCorsDisabled: | ||
| """Test behavior when CORS middleware is not configured.""" | ||
|
|
||
| def test_preflight_without_cors_middleware(self, app_without_cors): | ||
| """App without CORS middleware should not handle preflight specially.""" | ||
| client = TestClient(app_without_cors, raise_server_exceptions=False) | ||
| response = client.options( | ||
| "/sse", | ||
| headers={ | ||
| "Origin": "https://claude.ai", | ||
| "Access-Control-Request-Method": "GET", | ||
| }, | ||
| ) | ||
| assert response.headers.get("access-control-allow-origin") is None | ||
|
|
||
| def test_request_without_cors_middleware(self, app_without_cors): | ||
| """App without CORS middleware should not return CORS headers.""" | ||
| client = TestClient(app_without_cors, raise_server_exceptions=False) | ||
| response = client.post( | ||
| "/messages/", | ||
| headers={"Origin": "https://claude.ai"}, | ||
| content="test", | ||
| ) | ||
| assert response.headers.get("access-control-allow-origin") is None |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
logger.infoline has an invalid f-string because the inner", "string terminates the outer double-quoted f-string, which makes the module fail to parse at import time. In any environment that executes this code path (includingpostgres-mcp --transport=sse), Python will raise aSyntaxErrorbefore the server can start. Use single quotes inside the join or escape the quotes to keep the f-string valid.Useful? React with 👍 / 👎.