From 2b85dbd284873604d220624c8e83a0ffb1297f0d Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Fri, 26 Sep 2025 23:39:08 +0800 Subject: [PATCH 1/8] refactor: improve OAuth protected resource metadata URL construction per RFC 9728 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace string manipulation with proper URL parsing using urllib.parse.urlparse - Handle edge case where resource server path is "/" by treating as empty string - Ensure consistent URL construction for both WWW-Authenticate header and metadata endpoint - Add RFC 9728 §3.1 compliance comments for better code documentation --- src/mcp/server/fastmcp/server.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 839632930..5014dce77 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -938,9 +938,15 @@ def streamable_http_app(self) -> Starlette: resource_metadata_url = None if self.settings.auth and self.settings.auth.resource_server_url: from pydantic import AnyHttpUrl + from urllib.parse import urlparse + # RFC 9728 §3.1: Insert /.well-known/oauth-protected-resource between host and resource path + # This URL will be used in WWW-Authenticate header for client discovery + parsed = urlparse(str(self.settings.auth.resource_server_url)) + # Handle trailing slash: if path is just "/", treat as empty + resource_path = parsed.path if parsed.path != "/" else "" resource_metadata_url = AnyHttpUrl( - str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource" + f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}" ) routes.append( @@ -963,15 +969,23 @@ def streamable_http_app(self) -> Starlette: from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler from mcp.server.auth.routes import cors_middleware from mcp.shared.auth import ProtectedResourceMetadata + from urllib.parse import urlparse protected_resource_metadata = ProtectedResourceMetadata( resource=self.settings.auth.resource_server_url, authorization_servers=[self.settings.auth.issuer_url], scopes_supported=self.settings.auth.required_scopes, ) + + # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path + parsed = urlparse(str(self.settings.auth.resource_server_url)) + # Handle trailing slash: if path is just "/", treat as empty + resource_path = parsed.path if parsed.path != "/" else "" + well_known_path = f"/.well-known/oauth-protected-resource{resource_path}" + routes.append( Route( - "/.well-known/oauth-protected-resource", + well_known_path, endpoint=cors_middleware( ProtectedResourceMetadataHandler(protected_resource_metadata).handle, ["GET", "OPTIONS"], From 2023262a5c365ffd02d4df15d719c7729c01c767 Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Sat, 27 Sep 2025 00:15:00 +0800 Subject: [PATCH 2/8] refactor: reorganize imports and clean up whitespace in server.py - Moved import of AnyHttpUrl to the appropriate section for clarity - Removed unnecessary duplicate import of urlparse - Cleaned up whitespace for improved code readability --- src/mcp/server/fastmcp/server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 5014dce77..6a70a89ea 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -937,9 +937,10 @@ def streamable_http_app(self) -> Starlette: # Determine resource metadata URL resource_metadata_url = None if self.settings.auth and self.settings.auth.resource_server_url: - from pydantic import AnyHttpUrl from urllib.parse import urlparse + from pydantic import AnyHttpUrl + # RFC 9728 §3.1: Insert /.well-known/oauth-protected-resource between host and resource path # This URL will be used in WWW-Authenticate header for client discovery parsed = urlparse(str(self.settings.auth.resource_server_url)) @@ -966,23 +967,24 @@ def streamable_http_app(self) -> Starlette: # Add protected resource metadata endpoint if configured as RS if self.settings.auth and self.settings.auth.resource_server_url: + from urllib.parse import urlparse + from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler from mcp.server.auth.routes import cors_middleware from mcp.shared.auth import ProtectedResourceMetadata - from urllib.parse import urlparse protected_resource_metadata = ProtectedResourceMetadata( resource=self.settings.auth.resource_server_url, authorization_servers=[self.settings.auth.issuer_url], scopes_supported=self.settings.auth.required_scopes, ) - + # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path parsed = urlparse(str(self.settings.auth.resource_server_url)) # Handle trailing slash: if path is just "/", treat as empty resource_path = parsed.path if parsed.path != "/" else "" well_known_path = f"/.well-known/oauth-protected-resource{resource_path}" - + routes.append( Route( well_known_path, From d3f856441a775abc25c5a3dd3bea784abd0fff30 Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Sat, 27 Sep 2025 01:09:41 +0800 Subject: [PATCH 3/8] refactor: encapsulate resource metadata URL construction in a dedicated function - Introduced `build_resource_metadata_url` to create RFC 9728 compliant metadata URLs. - Updated `FastMCP` to utilize the new function for constructing resource metadata URLs. - Improved code readability by removing redundant URL parsing logic from `server.py`. --- src/mcp/server/auth/routes.py | 28 ++++++++++++++++- src/mcp/server/fastmcp/server.py | 52 ++++++++------------------------ 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index bce32df52..862b9a2d9 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -1,5 +1,6 @@ from collections.abc import Awaitable, Callable from typing import Any +from urllib.parse import urlparse from pydantic import AnyHttpUrl from starlette.middleware.cors import CORSMiddleware @@ -186,6 +187,25 @@ def build_metadata( return metadata +def build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl: + """ + Build RFC 9728 compliant protected resource metadata URL. + + Inserts /.well-known/oauth-protected-resource between host and resource path + as specified in RFC 9728 §3.1. + + Args: + resource_server_url: The resource server URL (e.g., https://example.com/mcp) + + Returns: + The metadata URL (e.g., https://example.com/.well-known/oauth-protected-resource/mcp) + """ + parsed = urlparse(str(resource_server_url)) + # Handle trailing slash: if path is just "/", treat as empty + resource_path = parsed.path if parsed.path != "/" else "" + return AnyHttpUrl(f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}") + + def create_protected_resource_routes( resource_url: AnyHttpUrl, authorization_servers: list[AnyHttpUrl], @@ -218,9 +238,15 @@ def create_protected_resource_routes( handler = ProtectedResourceMetadataHandler(metadata) + # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path + metadata_url = build_resource_metadata_url(resource_url) + # Extract just the path part for route registration + parsed = urlparse(str(metadata_url)) + well_known_path = parsed.path + return [ Route( - "/.well-known/oauth-protected-resource", + well_known_path, endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]), methods=["GET", "OPTIONS"], ) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 6a70a89ea..05d9f6e19 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -824,11 +824,10 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send): # Determine resource metadata URL resource_metadata_url = None if self.settings.auth and self.settings.auth.resource_server_url: - from pydantic import AnyHttpUrl + from mcp.server.auth.routes import build_resource_metadata_url - resource_metadata_url = AnyHttpUrl( - str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource" - ) + # Build compliant metadata URL for WWW-Authenticate header + resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url) # Auth is enabled, wrap the endpoints with RequireAuthMiddleware routes.append( @@ -937,18 +936,10 @@ def streamable_http_app(self) -> Starlette: # Determine resource metadata URL resource_metadata_url = None if self.settings.auth and self.settings.auth.resource_server_url: - from urllib.parse import urlparse - - from pydantic import AnyHttpUrl + from mcp.server.auth.routes import build_resource_metadata_url - # RFC 9728 §3.1: Insert /.well-known/oauth-protected-resource between host and resource path - # This URL will be used in WWW-Authenticate header for client discovery - parsed = urlparse(str(self.settings.auth.resource_server_url)) - # Handle trailing slash: if path is just "/", treat as empty - resource_path = parsed.path if parsed.path != "/" else "" - resource_metadata_url = AnyHttpUrl( - f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}" - ) + # Build compliant metadata URL for WWW-Authenticate header + resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url) routes.append( Route( @@ -967,32 +958,13 @@ def streamable_http_app(self) -> Starlette: # Add protected resource metadata endpoint if configured as RS if self.settings.auth and self.settings.auth.resource_server_url: - from urllib.parse import urlparse - - from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler - from mcp.server.auth.routes import cors_middleware - from mcp.shared.auth import ProtectedResourceMetadata - - protected_resource_metadata = ProtectedResourceMetadata( - resource=self.settings.auth.resource_server_url, - authorization_servers=[self.settings.auth.issuer_url], - scopes_supported=self.settings.auth.required_scopes, - ) - - # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path - parsed = urlparse(str(self.settings.auth.resource_server_url)) - # Handle trailing slash: if path is just "/", treat as empty - resource_path = parsed.path if parsed.path != "/" else "" - well_known_path = f"/.well-known/oauth-protected-resource{resource_path}" + from mcp.server.auth.routes import create_protected_resource_routes - routes.append( - Route( - well_known_path, - endpoint=cors_middleware( - ProtectedResourceMetadataHandler(protected_resource_metadata).handle, - ["GET", "OPTIONS"], - ), - methods=["GET", "OPTIONS"], + routes.extend( + create_protected_resource_routes( + resource_url=self.settings.auth.resource_server_url, + authorization_servers=[self.settings.auth.issuer_url], + scopes_supported=self.settings.auth.required_scopes, ) ) From 5780d4dfffbf05279c15b79b3f00c61f61b6272a Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Sat, 27 Sep 2025 01:38:18 +0800 Subject: [PATCH 4/8] test: enhance OAuth 2.0 Protected Resource tests for path-based resources - Renamed `test_metadata_endpoint` to `test_metadata_endpoint_with_path` for clarity. - Added a new test `test_metadata_endpoint_root_path_returns_404` to verify 404 response for root path. - Introduced fixtures `root_resource_app` and `root_resource_client` for testing root-level resources. - Added `test_metadata_endpoint_without_path` to validate metadata retrieval for root-level resources. --- tests/server/auth/test_protected_resource.py | 59 +++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/tests/server/auth/test_protected_resource.py b/tests/server/auth/test_protected_resource.py index 0dc34625d..f42237647 100644 --- a/tests/server/auth/test_protected_resource.py +++ b/tests/server/auth/test_protected_resource.py @@ -36,10 +36,11 @@ async def test_client(test_app: Starlette): @pytest.mark.anyio -async def test_metadata_endpoint(test_client: httpx.AsyncClient): - """Test the OAuth 2.0 Protected Resource metadata endpoint.""" +async def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient): + """Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource.""" - response = await test_client.get("/.well-known/oauth-protected-resource") + # For resource with path "/resource", metadata should be accessible at the path-aware location + response = await test_client.get("/.well-known/oauth-protected-resource/resource") assert response.json() == snapshot( { "resource": "https://example.com/resource", @@ -50,3 +51,55 @@ async def test_metadata_endpoint(test_client: httpx.AsyncClient): "bearer_methods_supported": ["header"], } ) + + +@pytest.mark.anyio +async def test_metadata_endpoint_root_path_returns_404(test_client: httpx.AsyncClient): + """Test that root path returns 404 for path-based resource.""" + + # Root path should return 404 for path-based resources + response = await test_client.get("/.well-known/oauth-protected-resource") + assert response.status_code == 404 + + +@pytest.fixture +def root_resource_app(): + """Fixture to create protected resource routes for root-level resource.""" + + # Create routes for a resource without path component + protected_resource_routes = create_protected_resource_routes( + resource_url=AnyHttpUrl("https://example.com"), + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + scopes_supported=["read"], + resource_name="Root Resource", + ) + + app = Starlette(routes=protected_resource_routes) + return app + + +@pytest.fixture +async def root_resource_client(root_resource_app: Starlette): + """Fixture to create an HTTP client for the root resource app.""" + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=root_resource_app), base_url="https://mcptest.com" + ) as client: + yield client + + +@pytest.mark.anyio +async def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncClient): + """Test metadata endpoint for root-level resource.""" + + # For root resource, metadata should be at standard location + response = await root_resource_client.get("/.well-known/oauth-protected-resource") + assert response.status_code == 200 + assert response.json() == snapshot( + { + "resource": "https://example.com/", + "authorization_servers": ["https://auth.example.com/"], + "scopes_supported": ["read"], + "resource_name": "Root Resource", + "bearer_methods_supported": ["header"], + } + ) From 4cfc05639a18863c6c705cb124adf09b800dfd41 Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Sat, 27 Sep 2025 14:28:39 +0800 Subject: [PATCH 5/8] fix: append /mcp to resource_server_url with trailing slash handling Append '/mcp' to resource_server_url and handle trailing slashes to ensure proper OAuth Protected Resource Metadata validation. This prevents the error: "Protected Resource Metadata resource does not match MCP server resolved resource" which occurs when the PRM resource URL doesn't exactly match the server URL. Fixes compatibility with VS Code + MCP authentication per RFC 9728. References: https://github.com/microsoft/vscode/issues/255255 --- examples/servers/simple-auth/mcp_simple_auth/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index ac449ebff..c959ca780 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -79,7 +79,7 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP: auth=AuthSettings( issuer_url=settings.auth_server_url, required_scopes=[settings.mcp_scope], - resource_server_url=settings.server_url, + resource_server_url=AnyHttpUrl(f"{str(settings.server_url).rstrip('/')}/mcp"), ), ) From d2d3c745e7b2e4f4423c7ef497be0ba804431bfd Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Sat, 27 Sep 2025 14:53:15 +0800 Subject: [PATCH 6/8] test: add tests for resource metadata URL construction and route consistency - Introduced `TestMetadataUrlConstruction` to validate URL construction for various resource configurations. - Added `TestRouteConsistency` to ensure consistency between generated metadata URLs and route paths. - Enhanced existing tests for better clarity and coverage of edge cases in URL handling. --- tests/server/auth/test_protected_resource.py | 95 +++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/tests/server/auth/test_protected_resource.py b/tests/server/auth/test_protected_resource.py index f42237647..82af16c5b 100644 --- a/tests/server/auth/test_protected_resource.py +++ b/tests/server/auth/test_protected_resource.py @@ -8,7 +8,7 @@ from pydantic import AnyHttpUrl from starlette.applications import Starlette -from mcp.server.auth.routes import create_protected_resource_routes +from mcp.server.auth.routes import build_resource_metadata_url, create_protected_resource_routes @pytest.fixture @@ -103,3 +103,96 @@ async def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncC "bearer_methods_supported": ["header"], } ) + + +class TestMetadataUrlConstruction: + """Test URL construction utility function.""" + + def test_url_without_path(self): + """Test URL construction for resource without path component.""" + resource_url = AnyHttpUrl("https://example.com") + result = build_resource_metadata_url(resource_url) + assert str(result) == "https://example.com/.well-known/oauth-protected-resource" + + def test_url_with_path_component(self): + """Test URL construction for resource with path component.""" + resource_url = AnyHttpUrl("https://example.com/mcp") + result = build_resource_metadata_url(resource_url) + assert str(result) == "https://example.com/.well-known/oauth-protected-resource/mcp" + + def test_url_with_trailing_slash_only(self): + """Test URL construction for resource with trailing slash only.""" + resource_url = AnyHttpUrl("https://example.com/") + result = build_resource_metadata_url(resource_url) + # Trailing slash should be treated as empty path + assert str(result) == "https://example.com/.well-known/oauth-protected-resource" + + @pytest.mark.parametrize( + "resource_url,expected_url", + [ + ("https://example.com", "https://example.com/.well-known/oauth-protected-resource"), + ("https://example.com/", "https://example.com/.well-known/oauth-protected-resource"), + ("https://example.com/mcp", "https://example.com/.well-known/oauth-protected-resource/mcp"), + ("http://localhost:8001/mcp", "http://localhost:8001/.well-known/oauth-protected-resource/mcp"), + ], + ) + def test_various_resource_configurations(self, resource_url: str, expected_url: str): + """Test URL construction with various resource configurations.""" + result = build_resource_metadata_url(AnyHttpUrl(resource_url)) + assert str(result) == expected_url + + +class TestRouteConsistency: + """Test consistency between URL generation and route registration.""" + + def test_route_path_matches_metadata_url(self): + """Test that route path matches the generated metadata URL.""" + resource_url = AnyHttpUrl("https://example.com/mcp") + + # Generate metadata URL + metadata_url = build_resource_metadata_url(resource_url) + + # Create routes + routes = create_protected_resource_routes( + resource_url=resource_url, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + ) + + # Extract path from metadata URL + from urllib.parse import urlparse + + metadata_path = urlparse(str(metadata_url)).path + + # Verify consistency + assert len(routes) == 1 + assert routes[0].path == metadata_path + + @pytest.mark.parametrize( + "resource_url,expected_path", + [ + ("https://example.com", "/.well-known/oauth-protected-resource"), + ("https://example.com/", "/.well-known/oauth-protected-resource"), + ("https://example.com/mcp", "/.well-known/oauth-protected-resource/mcp"), + ], + ) + def test_consistent_paths_for_various_resources(self, resource_url: str, expected_path: str): + """Test that URL generation and route creation are consistent.""" + resource_url_obj = AnyHttpUrl(resource_url) + + # Test URL generation + metadata_url = build_resource_metadata_url(resource_url_obj) + from urllib.parse import urlparse + + url_path = urlparse(str(metadata_url)).path + + # Test route creation + routes = create_protected_resource_routes( + resource_url=resource_url_obj, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + ) + route_path = routes[0].path + + # Both should match expected path + assert url_path == expected_path + assert route_path == expected_path + assert url_path == route_path From 2bc3292c5ca1422d95a8e8119dc4b03e360ecf11 Mon Sep 17 00:00:00 2001 From: Marcus Shu <46469249+shulkx@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:09:47 +0800 Subject: [PATCH 7/8] fix: align token verifier audience with OAuth server response The OAuth server returns `aud` including the `/mcp` path, while the token verifier previously expected only the base URL. This mismatch caused introspection failures under `--oauth-strict`. Updated the verifier configuration to use the correct audience to ensure successful token introspection. --- examples/servers/simple-auth/mcp_simple_auth/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index c959ca780..7ea133232 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -63,7 +63,7 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP: # Create token verifier for introspection with RFC 8707 resource validation token_verifier = IntrospectionTokenVerifier( introspection_endpoint=settings.auth_server_introspection_endpoint, - server_url=str(settings.server_url), + server_url=f"{str(settings.server_url).rstrip('/')}/mcp", validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set ) From e4ba2ee192ba2f98596fdb0ebb45cf19141cdde7 Mon Sep 17 00:00:00 2001 From: Marcus Shu Date: Mon, 6 Oct 2025 20:49:26 +0800 Subject: [PATCH 8/8] refactor: consolidate /mcp path into server_url - Include /mcp in server_url defaults and CLI construction - Remove redundant string manipulation in token verifier initialization - Remove redundant string manipulation in auth settings configuration --- examples/servers/simple-auth/mcp_simple_auth/server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index 7ea133232..c0a456cd3 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -32,7 +32,7 @@ class ResourceServerSettings(BaseSettings): # Server settings host: str = "localhost" port: int = 8001 - server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8001") + server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8001/mcp") # Authorization Server settings auth_server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:9000") @@ -63,7 +63,7 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP: # Create token verifier for introspection with RFC 8707 resource validation token_verifier = IntrospectionTokenVerifier( introspection_endpoint=settings.auth_server_introspection_endpoint, - server_url=f"{str(settings.server_url).rstrip('/')}/mcp", + server_url=str(settings.server_url), validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set ) @@ -79,7 +79,7 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP: auth=AuthSettings( issuer_url=settings.auth_server_url, required_scopes=[settings.mcp_scope], - resource_server_url=AnyHttpUrl(f"{str(settings.server_url).rstrip('/')}/mcp"), + resource_server_url=settings.server_url, ), ) @@ -137,7 +137,7 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http # Create settings host = "localhost" - server_url = f"http://{host}:{port}" + server_url = f"http://{host}:{port}/mcp" settings = ResourceServerSettings( host=host, port=port,