Skip to content

Commit 5a7b303

Browse files
Merge branch 'main' into fastmpc-logging-progress-example
2 parents 41d4ac7 + 20596e5 commit 5a7b303

File tree

9 files changed

+84
-15
lines changed

9 files changed

+84
-15
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ repos:
2525
hooks:
2626
- id: ruff-format
2727
name: Ruff Format
28-
entry: uv run ruff
28+
entry: uv run --frozen ruff
2929
args: [format]
3030
language: system
3131
types: [python]
3232
pass_filenames: false
3333
- id: ruff
3434
name: Ruff
35-
entry: uv run ruff
35+
entry: uv run --frozen ruff
3636
args: ["check", "--fix", "--exit-non-zero-on-fix"]
3737
types: [python]
3838
language: system
3939
pass_filenames: false
4040
exclude: ^README\.md$
4141
- id: pyright
4242
name: pyright
43-
entry: uv run pyright
43+
entry: uv run --frozen pyright
4444
language: system
4545
types: [python]
4646
pass_filenames: false
@@ -52,7 +52,7 @@ repos:
5252
pass_filenames: false
5353
- id: readme-snippets
5454
name: Check README snippets are up to date
55-
entry: uv run scripts/update_readme_snippets.py --check
55+
entry: uv run --frozen python scripts/update_readme_snippets.py --check
5656
language: system
5757
files: ^(README\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$
5858
pass_filenames: false

src/mcp/server/elicitation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async def elicit_with_validation(
9898
related_request_id=related_request_id,
9999
)
100100

101-
if result.action == "accept" and result.content:
101+
if result.action == "accept" and result.content is not None:
102102
# Validate and parse the content using the schema
103103
validated_data = schema.model_validate(result.content)
104104
return AcceptedElicitation(data=validated_data)

src/mcp/server/fastmcp/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]:
320320
name=template.name,
321321
title=template.title,
322322
description=template.description,
323+
mimeType=template.mime_type,
323324
)
324325
for template in templates
325326
]

src/mcp/server/transport_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,6 @@ async def validate_request(self, request: Request, is_post: bool = False) -> Res
122122
# Validate Origin header
123123
origin = request.headers.get("origin")
124124
if not self._validate_origin(origin):
125-
return Response("Invalid Origin header", status_code=400)
125+
return Response("Invalid Origin header", status_code=403)
126126

127127
return None
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Integration tests for MCP Oauth Protected Resource.
3+
"""
4+
5+
import httpx
6+
import pytest
7+
from inline_snapshot import snapshot
8+
from pydantic import AnyHttpUrl
9+
from starlette.applications import Starlette
10+
11+
from mcp.server.auth.routes import create_protected_resource_routes
12+
13+
14+
@pytest.fixture
15+
def test_app():
16+
"""Fixture to create protected resource routes for testing."""
17+
18+
# Create the protected resource routes
19+
protected_resource_routes = create_protected_resource_routes(
20+
resource_url=AnyHttpUrl("https://example.com/resource"),
21+
authorization_servers=[AnyHttpUrl("https://auth.example.com/authorization")],
22+
scopes_supported=["read", "write"],
23+
resource_name="Example Resource",
24+
resource_documentation=AnyHttpUrl("https://docs.example.com/resource"),
25+
)
26+
27+
app = Starlette(routes=protected_resource_routes)
28+
return app
29+
30+
31+
@pytest.fixture
32+
async def test_client(test_app: Starlette):
33+
"""Fixture to create an HTTP client for the protected resource app."""
34+
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=test_app), base_url="https://mcptest.com") as client:
35+
yield client
36+
37+
38+
@pytest.mark.anyio
39+
async def test_metadata_endpoint(test_client: httpx.AsyncClient):
40+
"""Test the OAuth 2.0 Protected Resource metadata endpoint."""
41+
42+
response = await test_client.get("/.well-known/oauth-protected-resource")
43+
assert response.json() == snapshot(
44+
{
45+
"resource": "https://example.com/resource",
46+
"authorization_servers": ["https://auth.example.com/authorization"],
47+
"scopes_supported": ["read", "write"],
48+
"resource_name": "Example Resource",
49+
"resource_documentation": "https://docs.example.com/resource",
50+
"bearer_methods_supported": ["header"],
51+
}
52+
)

tests/server/fastmcp/auth/test_auth_integration.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,8 @@ class TestAuthEndpoints:
342342
@pytest.mark.anyio
343343
async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):
344344
"""Test the OAuth 2.0 metadata endpoint."""
345-
print("Sending request to metadata endpoint")
345+
346346
response = await test_client.get("/.well-known/oauth-authorization-server")
347-
print(f"Got response: {response.status_code}")
348-
if response.status_code != 200:
349-
print(f"Response content: {response.content}")
350347
assert response.status_code == 200
351348

352349
metadata = response.json()
@@ -399,9 +396,7 @@ async def test_token_invalid_auth_code(
399396
"redirect_uri": "https://client.example.com/callback",
400397
},
401398
)
402-
print(f"Status code: {response.status_code}")
403-
print(f"Response body: {response.content}")
404-
print(f"Response JSON: {response.json()}")
399+
405400
assert response.status_code == 400
406401
error_response = response.json()
407402
assert error_response["error"] == "invalid_grant"

tests/server/fastmcp/test_server.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,27 @@ def get_data(name: str) -> str:
810810
result = await resource.read()
811811
assert result == "Data for test"
812812

813+
@pytest.mark.anyio
814+
async def test_resource_template_includes_mime_type(self):
815+
"""Test that list resource templates includes the correct mimeType."""
816+
mcp = FastMCP()
817+
818+
@mcp.resource("resource://{user}/csv", mime_type="text/csv")
819+
def get_csv(user: str) -> str:
820+
return f"csv for {user}"
821+
822+
templates = await mcp.list_resource_templates()
823+
assert len(templates) == 1
824+
template = templates[0]
825+
826+
assert hasattr(template, "mimeType")
827+
assert template.mimeType == "text/csv"
828+
829+
async with client_session(mcp._mcp_server) as client:
830+
result = await client.read_resource(AnyUrl("resource://bob/csv"))
831+
assert isinstance(result.contents[0], TextResourceContents)
832+
assert result.contents[0].text == "csv for bob"
833+
813834

814835
class TestContextInjection:
815836
"""Test context injection in tools, resources, and prompts."""

tests/server/test_sse_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def test_sse_security_invalid_origin_header(server_port: int):
127127

128128
async with httpx.AsyncClient() as client:
129129
response = await client.get(f"http://127.0.0.1:{server_port}/sse", headers=headers)
130-
assert response.status_code == 400
130+
assert response.status_code == 403
131131
assert response.text == "Invalid Origin header"
132132

133133
finally:

tests/server/test_streamable_http_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ async def test_streamable_http_security_invalid_origin_header(server_port: int):
155155
json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}},
156156
headers=headers,
157157
)
158-
assert response.status_code == 400
158+
assert response.status_code == 403
159159
assert response.text == "Invalid Origin header"
160160

161161
finally:

0 commit comments

Comments
 (0)