Skip to content

Commit d3725cf

Browse files
committed
Review feedback
1 parent f46dcb1 commit d3725cf

File tree

6 files changed

+28
-70
lines changed

6 files changed

+28
-70
lines changed

src/mcp/server/auth/errors.py

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,4 @@
1-
from typing import Literal
2-
3-
from pydantic import BaseModel, ValidationError
4-
5-
ErrorCode = Literal["invalid_request", "invalid_client"]
6-
7-
8-
class ErrorResponse(BaseModel):
9-
error: ErrorCode
10-
error_description: str
11-
12-
13-
class OAuthError(Exception):
14-
"""
15-
Base class for all OAuth errors.
16-
"""
17-
18-
error_code: ErrorCode
19-
20-
def __init__(self, error_description: str):
21-
super().__init__(error_description)
22-
self.error_description = error_description
23-
24-
def error_response(self) -> ErrorResponse:
25-
return ErrorResponse(
26-
error=self.error_code,
27-
error_description=self.error_description,
28-
)
1+
from pydantic import ValidationError
292

303

314
def stringify_pydantic_error(validation_error: ValidationError) -> str:

src/mcp/server/auth/handlers/authorize.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import logging
22
from dataclasses import dataclass
33
from typing import Any, Literal
4-
from urllib.parse import urlencode, urlparse, urlunparse
54

65
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel, ValidationError
76
from starlette.datastructures import FormData, QueryParams
87
from starlette.requests import Request
98
from starlette.responses import RedirectResponse, Response
109

1110
from mcp.server.auth.errors import (
12-
OAuthError,
1311
stringify_pydantic_error,
1412
)
1513
from mcp.server.auth.json_response import PydanticJSONResponse
@@ -225,34 +223,3 @@ async def error_response(
225223
return await error_response(
226224
error="server_error", error_description="An unexpected error occurred"
227225
)
228-
229-
230-
def create_error_redirect(
231-
redirect_uri: AnyUrl, error: Exception | AuthorizationErrorResponse
232-
) -> str:
233-
parsed_uri = urlparse(str(redirect_uri))
234-
235-
if isinstance(error, AuthorizationErrorResponse):
236-
# Convert ErrorResponse to dict
237-
error_dict = error.model_dump(exclude_none=True)
238-
query_params = {}
239-
for key, value in error_dict.items():
240-
if value is not None:
241-
if key == "error_uri" and hasattr(value, "__str__"):
242-
query_params[key] = str(value)
243-
else:
244-
query_params[key] = value
245-
246-
elif isinstance(error, OAuthError):
247-
query_params = {"error": error.error_code, "error_description": str(error)}
248-
else:
249-
query_params = {
250-
"error": "server_error",
251-
"error_description": "An unknown error occurred",
252-
}
253-
254-
new_query = urlencode(query_params)
255-
if parsed_uri.query:
256-
new_query = f"{parsed_uri.query}&{new_query}"
257-
258-
return urlunparse(parsed_uri._replace(query=new_query))

src/mcp/server/auth/handlers/token.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from starlette.requests import Request
99

1010
from mcp.server.auth.errors import (
11-
ErrorResponse,
1211
stringify_pydantic_error,
1312
)
1413
from mcp.server.auth.json_response import PydanticJSONResponse
@@ -80,7 +79,7 @@ class TokenHandler:
8079
provider: OAuthServerProvider[Any, Any, Any]
8180
client_authenticator: ClientAuthenticator
8281

83-
def response(self, obj: TokenSuccessResponse | TokenErrorResponse | ErrorResponse):
82+
def response(self, obj: TokenSuccessResponse | TokenErrorResponse):
8483
status_code = 200
8584
if isinstance(obj, TokenErrorResponse):
8685
status_code = 400

src/mcp/server/auth/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def authorize(
154154
+------------+
155155
156156
Implementations will need to define another handler on the MCP server return
157-
flow to perform the second redirect, and generates and stores an authorization
157+
flow to perform the second redirect, and generate and store an authorization
158158
code as part of completing the OAuth authorization step.
159159
160160
Implementations SHOULD generate an authorization code with at least 160 bits of

src/mcp/server/fastmcp/server.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,27 @@ def custom_route(
489489
name: str | None = None,
490490
include_in_schema: bool = True,
491491
):
492+
"""
493+
Decorator to register a custom HTTP route on the FastMCP server.
494+
495+
Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
496+
which can be useful for OAuth callbacks, health checks, or admin APIs.
497+
The handler function must be an async function that accepts a Starlette
498+
Request and returns a Response.
499+
500+
Args:
501+
path: URL path for the route (e.g., "/oauth/callback")
502+
methods: List of HTTP methods to support (e.g., ["GET", "POST"])
503+
name: Optional name for the route (to reference this route with
504+
Starlette's reverse URL lookup feature)
505+
include_in_schema: Whether to include in OpenAPI schema, defaults to True
506+
507+
Example:
508+
@server.custom_route("/health", methods=["GET"])
509+
async def health_check(request: Request) -> Response:
510+
return JSONResponse({"status": "ok"})
511+
"""
512+
492513
def decorator(
493514
func: Callable[[Request], Awaitable[Response]],
494515
) -> Callable[[Request], Awaitable[Response]]:
@@ -517,6 +538,7 @@ async def run_stdio_async(self) -> None:
517538
async def run_sse_async(self) -> None:
518539
"""Run the server using SSE transport."""
519540
import uvicorn
541+
520542
starlette_app = self.sse_app()
521543

522544
config = uvicorn.Config(
@@ -529,6 +551,7 @@ async def run_sse_async(self) -> None:
529551
await server.serve()
530552

531553
def sse_app(self) -> Starlette:
554+
"""Return an instance of the SSE server app."""
532555
from starlette.middleware import Middleware
533556
from starlette.routing import Mount, Route
534557

tests/server/fastmcp/servers/test_file_server.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,13 @@ async def test_read_resource_file(mcp: FastMCP):
114114

115115
@pytest.mark.anyio
116116
async def test_delete_file(mcp: FastMCP, test_dir: Path):
117-
await mcp.call_tool(
118-
"delete_file", arguments={"path": str(test_dir / "example.py")}
119-
)
117+
await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")})
120118
assert not (test_dir / "example.py").exists()
121119

122120

123121
@pytest.mark.anyio
124122
async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path):
125-
await mcp.call_tool(
126-
"delete_file", arguments={"path": str(test_dir / "example.py")}
127-
)
123+
await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")})
128124
res_iter = await mcp.read_resource("file://test_dir/example.py")
129125
res_list = list(res_iter)
130126
assert len(res_list) == 1

0 commit comments

Comments
 (0)