Commit Graph

133 Commits

Author SHA1 Message Date
Max Isbey 34bcf2d415 refactor: extract auth components and streamable HTTP app helpers
- Add build_auth_components() in mcp.server.auth.components for reusable
  auth setup (middleware, endpoint wrapper, routes)
- Refactor create_streamable_http_app() to take session_manager as first
  arg with keyword args for app config (removed StreamableHTTPAppConfig)
- FastMCP now uses _build_auth_components() helper, reducing duplication
  between sse_app() and streamable_http_app()
- Session manager is now created/owned by caller, passed to app creator
- Add unit tests for build_auth_components()
- Export AuthComponents, build_auth_components from mcp.server.auth
- Export StreamableHTTPSessionManager, create_streamable_http_app from
  mcp.server

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 14
Claude-Permission-Prompts: 13
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# Plan: Extract Auth Helper and Make FastMCP a Thin Wrapper

## Summary

Create a shared `build_auth_components()` helper in the auth module that both `sse_app()` and `streamable_http_app()` can use. This removes ~60 lines of duplicated auth logic from FastMCP and makes it a much thinner wrapper.

## Files to Modify

1. **`src/mcp/server/auth/routes.py`** - Add `AuthConfig` dataclass and `build_auth_components()` function
2. **`src/mcp/server/fastmcp/server.py`** - Refactor both `sse_app()` and `streamable_http_app()` to use the helper
3. **`src/mcp/server/__init__.py`** - Export new auth helper for low-level users

## Implementation

### Step 1: Add to `src/mcp/server/auth/routes.py`

**Add new dataclass and helper function:**

```python
from dataclasses import dataclass, field
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.types import ASGIApp

@dataclass
class AuthConfig:
    """Configuration for auth components in Starlette apps."""

    # Token verification (required)
    token_verifier: TokenVerifier

    # Auth settings
    issuer_url: AnyHttpUrl
    required_scopes: list[str] = field(default_factory=list)
    resource_server_url: AnyHttpUrl | None = None

    # Optional: Full OAuth AS provider for serving auth endpoints
    auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None
    service_documentation_url: AnyHttpUrl | None = None
    client_registration_options: ClientRegistrationOptions | None = None
    revocation_options: RevocationOptions | None = None

@dataclass
class AuthComponents:
    """Auth components ready to be used in a Starlette app."""

    routes: list[Route]
    middleware: list[Middleware]
    endpoint_wrapper: Callable[[ASGIApp], ASGIApp]

def build_auth_components(config: AuthConfig) -> AuthComponents:
    """
    Build auth routes, middleware, and endpoint wrapper from config.

    Returns an AuthComponents with:
    - routes: OAuth AS routes (if provider set) + protected resource metadata
    - middleware: AuthenticationMiddleware + AuthContextMiddleware
    - endpoint_wrapper: RequireAuthMiddleware wrapper function
    """
    routes: list[Route] = []

    # Build middleware
    middleware = [
        Middleware(
            AuthenticationMiddleware,
            backend=BearerAuthBackend(config.token_verifier),
        ),
        Middleware(AuthContextMiddleware),
    ]

    # Add OAuth AS routes if provider is configured
    if config.auth_server_provider:
        routes.extend(
            create_auth_routes(
                provider=config.auth_server_provider,
                issuer_url=config.issuer_url,
                service_documentation_url=config.service_documentation_url,
                client_registration_options=config.client_registration_options,
                revocation_options=config.revocation_options,
            )
        )

    # Add protected resource metadata routes if resource_server_url is set
    if config.resource_server_url:
        routes.extend(
            create_protected_resource_routes(
                resource_url=config.resource_server_url,
                authorization_servers=[config.issuer_url],
                scopes_supported=config.required_scopes or None,
            )
        )

    # Build endpoint wrapper
    resource_metadata_url = None
    if config.resource_server_url:
        resource_metadata_url = build_resource_metadata_url(config.resource_server_url)

    def endpoint_wrapper(app: ASGIApp) -> ASGIApp:
        return RequireAuthMiddleware(app, config.required_scopes, resource_metadata_url)

    return AuthComponents(
        routes=routes,
        middleware=middleware,
        endpoint_wrapper=endpoint_wrapper,
    )
```

### Step 2: Refactor `FastMCP.streamable_http_app()`

Replace the ~50 lines of auth logic with:

```python
def streamable_http_app(self) -> Starlette:
    """Return an instance of the StreamableHTTP server app."""
    additional_routes: list[Route | Mount] = []
    middleware: list[Middleware] = []
    endpoint_wrapper: Callable[[ASGIApp], ASGIApp] | None = None

    # Build auth components if auth is configured
    if self.settings.auth and self._token_verifier:
        from mcp.server.auth.routes import AuthConfig, build_auth_components

        auth_config = AuthConfig(
            token_verifier=self._token_verifier,
            issuer_url=self.settings.auth.issuer_url,
            required_scopes=self.settings.auth.required_scopes or [],
            resource_server_url=self.settings.auth.resource_server_url,
            auth_server_provider=self._auth_server_provider,
            service_documentation_url=self.settings.auth.service_documentation_url,
            client_registration_options=self.settings.auth.client_registration_options,
            revocation_options=self.settings.auth.revocation_options,
        )
        auth_components = build_auth_components(auth_config)

        additional_routes.extend(auth_components.routes)
        middleware = auth_components.middleware
        endpoint_wrapper = auth_components.endpoint_wrapper

    # Add custom routes last
    additional_routes.extend(self._custom_starlette_routes)

    # Create config and call low-level function
    config = StreamableHTTPAppConfig(
        mcp_server=self._mcp_server,
        event_store=self._event_store,
        retry_interval=self._retry_interval,
        json_response=self.settings.json_response,
        stateless=self.settings.stateless_http,
        security_settings=self.settings.transport_security,
        endpoint_path=self.settings.streamable_http_path,
        debug=self.settings.debug,
        additional_routes=additional_routes,
        middleware=middleware,
        endpoint_wrapper=endpoint_wrapper,
    )

    starlette_app, session_manager = create_streamable_http_app(config)
    self._session_manager = session_manager
    return starlette_app
```

### Step 3: Refactor `FastMCP.sse_app()`

Similar refactor - replace the auth logic with `build_auth_components()`. The SSE app has slightly different route structure (two endpoints: SSE and messages), so the wrapper is applied to each endpoint individually rather than via config:

```python
def sse_app(self, mount_path: str | None = None) -> Starlette:
    """Return an instance of the SSE server app."""
    if mount_path is not None:
        self.settings.mount_path = mount_path

    normalized_message_endpoint = self._normalize_path(
        self.settings.mount_path, self.settings.message_path
    )

    sse = SseServerTransport(
        normalized_message_endpoint,
        security_settings=self.settings.transport_security,
    )

    async def handle_sse(scope: Scope, receive: Receive, send: Send):
        async with sse.connect_sse(scope, receive, send) as streams:
            await self._mcp_server.run(
                streams[0], streams[1],
                self._mcp_server.create_initialization_options(),
            )
        return Response()

    routes: list[Route | Mount] = []
    middleware: list[Middleware] = []

    # Build auth components if configured
    if self.settings.auth and self._token_verifier:
        from mcp.server.auth.routes import AuthConfig, build_auth_components

        auth_config = AuthConfig(
            token_verifier=self._token_verifier,
            issuer_url=self.settings.auth.issuer_url,
            required_scopes=self.settings.auth.required_scopes or [],
            resource_server_url=self.settings.auth.resource_server_url,
            auth_server_provider=self._auth_server_provider,
            service_documentation_url=self.settings.auth.service_documentation_url,
            client_registration_options=self.settings.auth.client_registration_options,
            revocation_options=self.settings.auth.revocation_options,
        )
        auth_components = build_auth_components(auth_config)

        routes.extend(auth_components.routes)
        middleware = auth_components.middleware

        # SSE has two endpoints that need wrapping
        routes.append(Route(
            self.settings.sse_path,
            endpoint=auth_components.endpoint_wrapper(handle_sse),
            methods=["GET"],
        ))
        routes.append(Mount(
            self.settings.message_path,
            app=auth_components.endpoint_wrapper(sse.handle_post_message),
        ))
    else:
        # No auth - add routes directly
        async def sse_endpoint(request: Request) -> Response:
            return await handle_sse(request.scope, request.receive, request._send)

        routes.append(Route(self.settings.sse_path, endpoint=sse_endpoint, methods=["GET"]))
        routes.append(Mount(self.settings.message_path, app=sse.handle_post_message))

    routes.extend(self._custom_starlette_routes)
    return Starlette(debug=self.settings.debug, routes=routes, middleware=middleware)
```

### Step 4: Update exports in `src/mcp/server/__init__.py`

Add the new auth helper to exports:

```python
from .auth.routes import AuthConfig, AuthComponents, build_auth_components
```

## Verification

1. Run existing tests:
   ```bash
   PYTEST_DISABLE_PLUGIN_AUTOLOAD="" uv run --frozen pytest tests/server/fastmcp/
   ```

2. Run auth tests specifically:
   ```bash
   PYTEST_DISABLE_PLUGIN_AUTOLOAD="" uv run --frozen pytest tests/server/fastmcp/auth/
   ```

3. Run type checking:
   ```bash
   uv run --frozen pyright
   ```

4. Test low-level usage with auth:
   ```python
   from mcp.server import Server, StreamableHTTPAppConfig, create_streamable_http_app
   from mcp.server.auth.routes import AuthConfig, build_auth_components

   server = Server('test')
   auth = build_auth_components(AuthConfig(...))

   config = StreamableHTTPAppConfig(
       mcp_server=server,
       additional_routes=auth.routes,
       middleware=auth.middleware,
       endpoint_wrapper=auth.endpoint_wrapper,
   )
   app, manager = create_streamable_http_app(config)
   ```

## Result

FastMCP's `streamable_http_app()` goes from ~90 lines to ~30 lines, and `sse_app()` similarly shrinks. The auth logic is now:
- Reusable by low-level Server users
- Testable in isolation
- Shared between SSE and StreamableHTTP transports
</claude-plan>
2026-01-16 15:32:33 +01:00
Felix Weinberger cfb2909631 fix: change Resource URI fields from AnyUrl to str (#1863) 2026-01-16 09:58:57 +01:00
Yann Jouanin 0da9a074d0 Support for Resource and ResourceTemplate metadata (#1840)
Co-authored-by: Jacem Elwaar <jacem@mcpappsbuilders.com>
2026-01-12 14:45:31 +00:00
Marcelo Trylesinski 3ffe142e9a Support Python 3.14 (#1834) 2026-01-07 16:28:23 +00:00
Yann Jouanin 3863f203e9 Server initialize response update to last spec (add title, description) (#1634) 2026-01-06 21:34:08 +00:00
Marcelo Trylesinski 6149b63a44 tests: add missing init files (#1831) 2026-01-06 19:52:09 +01:00
Max Isbey 37de50144f fix: add StatelessModeNotSupported exception and improve tests (#1828) 2026-01-06 11:26:26 +00:00
Max Isbey bb6cb029f0 fix: raise clear error for server-to-client requests in stateless mode (#1827) 2026-01-05 17:17:54 +00:00
gazzadownunder d52937b39c Make refresh_token grant type optional in DCR handler (#1651)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 13:41:45 +00:00
Maxime 78a9504ec1 fix: return HTTP 404 for unknown session IDs instead of 400 (#1808)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2025-12-31 14:06:12 +00:00
Ankesh Kumar Thakur a4bf947540 fix: Token endpoint response for invalid_client (#1481)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2025-12-19 18:19:00 +00:00
zenlytix 8ac0cab98c Fix for Url Elicitation issue 1768 (#1780) 2025-12-15 18:58:17 +01:00
Marcelo Trylesinski a3a4b8d11a Add streamable_http_client which accepts httpx.AsyncClient instead of httpx_client_factory (#1177)
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
2025-12-10 16:39:00 +00:00
Paul Carleton d3a184119e Merge commit from fork
Main branch checks / checks (push) Failing after 0s
* Auto-enable DNS rebinding protection for localhost servers

When a FastMCP server is created with host="127.0.0.1" or "localhost"
and no explicit transport_security is provided, automatically enable
DNS rebinding protection. Both 127.0.0.1 and localhost are allowed
as valid hosts/origins since clients may use either to connect.

* Add tests for auto DNS rebinding protection on localhost

Tests verify that:
- Protection auto-enables for host=127.0.0.1
- Protection auto-enables for host=localhost
- Both 127.0.0.1 and localhost are in allowed hosts/origins
- Protection does NOT auto-enable for other hosts (e.g., 0.0.0.0)
- Explicit transport_security settings are not overridden

* Add IPv6 localhost (::1) support for DNS rebinding protection

Extend auto-enable DNS rebinding protection to also cover IPv6
localhost. When host="::1", protection is now auto-enabled with
appropriate allowed hosts ([::1]:*) and origins (http://[::1]:*).

* Fix import ordering in test file
2025-12-02 13:23:55 +00:00
Max Isbey c92bb2f7ff SEP-1686: Tasks (#1645) 2025-11-28 18:51:58 +00:00
Chris Coutinho 02b7889929 Implement SEP-1036: URL mode elicitation for secure out-of-band interactions (#1580)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
2025-11-25 11:00:21 +00:00
Tapan Chugh b19fa6f279 SEP-1330: Elicitation Enum Schema Improvements and Standards Compliance (#1246)
Co-authored-by: Tapan Chugh <tapanc@cs.washington.edu>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-11-23 23:32:08 +00:00
Olivier Chafik 71c475588f Implement SEP-1577 - Sampling With Tools (#1594)
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 23:58:14 -05:00
Jon Shea c51936f61f Add client_secret_basic authentication support (#1334)
Co-authored-by: Paul Carleton <paulc@anthropic.com>
2025-11-20 20:53:37 +00:00
Victorien 116c13e2c6 Refactor func_metadata() implementation (#1496) 2025-11-13 20:21:15 +00:00
Max Isbey 89e9c43acf Get baseline 100% clean coverage (#1553) 2025-11-11 14:09:32 +01:00
Luca Chang f161149680 Implement RFC 7523 JWT flows (#1247)
Co-authored-by: Yann Jouanin <yann.jouanin@valueandco.com>
2025-10-29 16:48:08 +00:00
Max Isbey db9e451551 fix: Replace remaining manual server polling with wait_for_server helper (#1529) 2025-10-29 11:27:57 +00:00
Max Isbey 3e86edfb2f fix: Replace arbitrary sleeps with active server readiness checks in tests (#1527)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-28 21:42:33 +00:00
Max Isbey f97f7c4a7d fix: Replace fixed sleep with active server readiness check in SSE tests (#1526) 2025-10-28 19:49:13 +00:00
Brandon Shar 1200ba0082 Allow CallToolResult to be returned directly to support _meta field for OpenAI Apps (#1459)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2025-10-21 18:52:08 +00:00
Mat Leonard 98f82485bd feat: add tool metadata in FastMCP.tool decorator (#1463)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2025-10-17 11:18:34 +00:00
daamitt dcc68ce56b fix: Set the Server session initialization state immediately after respond… (#1478)
Main branch checks / checks (push) Failing after 0s
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2025-10-15 13:56:43 +01:00
Fenn Bailey cd7253c593 feat: add resource annotations support to FastMCP (#1468) 2025-10-14 11:07:37 +01:00
AishwaryaKalloli b4e50aa9f9 Handles message type Exception in lowlevel/server.py _handle_message function. Mentioned as TODO on line 528. (#786)
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-10-13 14:29:03 +01:00
Brandon Wu b8e758b02b feat: add ability to remove tools (#1322)
Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-06 14:16:50 +01:00
Marcus Shu df3e428ee8 Improve OAuth protected resource metadata URL construction per RFC 9728 (#1407) 2025-10-06 13:52:44 +01:00
automaton82 9323efad99 Issue 1379 patch - Fix MCP server OAuth not working with Visual Studio Code and others with extra grant_types (#1380) 2025-09-29 11:13:16 +01:00
Jon Shea 1940040ac3 Accept additional response_types values from OAuth servers (#1323) 2025-09-26 20:29:06 +01:00
Tapan Chugh b85e7bd1a9 feat: Add SDK support for SEP-1034 default values in elicitation schemas (#1337)
Co-authored-by: Tapan Chugh <tapanc@cs.washington.edu>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-09-24 16:33:45 +01:00
Max Isbey 4fb975c6dc feat: add paginated list decorators for prompts, resources, and tools (#1286)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 14:58:06 +01:00
Yann Jouanin 20596e5f41 Add test for ProtectedResourceMetadataParsing (#1236)
Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-09-23 12:48:21 +01:00
Peter Alexander 7e93a9fc19 Return HTTP 403 for invalid Origin headers (#1353) 2025-09-22 16:44:51 +01:00
pchoudhury22 ca5cb4cb67 fix(fastmcp): propagate mimeType in resource template list (#1186)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-09-17 14:50:41 +01:00
David Soria Parra c3717e7ad3 Fix context injection for resources and prompts (#1336)
Main branch checks / checks (push) Failing after 0s
2025-09-11 14:30:06 +01:00
Eleftheria Stein-Kousathana 47d35f0b3c Allow ping requests before initialization (#1312) 2025-09-01 22:37:36 +01:00
jess 1644b822b3 changes structured temperature to not deadly (#1328) 2025-08-31 23:09:49 +00:00
Sreenath Somarajapuram 07ae8c0d4e types: Setting default value for method: Literal (#1292) 2025-08-26 16:22:56 +01:00
San Nguyen eaf7cf41d5 fix: error too many values to unpack (expected 2) (#1279)
Signed-off-by: San Nguyen <vinhsannguyen91@gmail.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
2025-08-23 21:36:39 +01:00
xavier f4b2957a20 Added Audio to FastMCP (#1130) 2025-08-22 11:45:38 +01:00
David Soria Parra 0926613714 Update dependencies and fix type issues (#1268)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2025-08-14 10:40:47 +01:00
Marcelo Trylesinski c7671e470c Add pyright strict mode on the whole project (#1254) 2025-08-11 18:56:37 +01:00
lukacf b34e7207ad server: skip duplicate response on CancelledError (#1153)
Co-authored-by: ihrpr <inna@anthropic.com>
2025-07-25 10:16:13 +01:00
Felix Weinberger bd9885feda Add regression test for stateless request memory cleanup (#1140) 2025-07-15 09:29:29 +01:00
Inna Harper 9bf5160eea README - replace code snippets with examples (#1136) 2025-07-14 13:57:47 +01:00