From 7c10feaeb1c3d984f170b5f16d5030e457ee5e6b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:03:21 -0400 Subject: [PATCH 1/8] Expand timeouts (#1954) --- .github/workflows/run-tests.yml | 6 +++--- tests/client/test_sse.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e93b346d9..7b9672978 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -48,10 +48,10 @@ jobs: run: uv sync --frozen - name: Run tests (excluding integration and client_process) - run: uv run pytest --inline-snapshot=disable -v tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal + run: uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal - name: Run client process tests separately - run: uv run pytest --inline-snapshot=disable -v tests -m "client_process" -x + run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x run_integration_tests: name: "Run integration tests" @@ -74,7 +74,7 @@ jobs: - name: Run integration tests # use longer per-test timeout than the default 3s - run: uv run pytest -v tests -m "integration" --timeout=15 --numprocesses auto --maxprocesses 2 --dist worksteal + run: uv run pytest tests -m "integration" --timeout=15 --numprocesses auto --maxprocesses 2 --dist worksteal env: FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }} FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }} diff --git a/tests/client/test_sse.py b/tests/client/test_sse.py index a60b975ac..f2fe86605 100644 --- a/tests/client/test_sse.py +++ b/tests/client/test_sse.py @@ -130,18 +130,18 @@ class TestTimeout: async def test_timeout(self, sse_server: str): with pytest.raises( McpError, - match="Timed out while waiting for response to ClientRequest. Waited 0.01 seconds", + match="Timed out while waiting for response to ClientRequest. Waited 0.03 seconds", ): async with Client( transport=SSETransport(sse_server), - timeout=0.01, + timeout=0.03, ) as client: await client.call_tool("sleep", {"seconds": 0.1}) async def test_timeout_tool_call(self, sse_server: str): async with Client(transport=SSETransport(sse_server)) as client: with pytest.raises(McpError, match="Timed out"): - await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01) + await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.03) async def test_timeout_tool_call_overrides_client_timeout_if_lower( self, sse_server: str @@ -151,7 +151,7 @@ class TestTimeout: timeout=2, ) as client: with pytest.raises(McpError, match="Timed out"): - await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01) + await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.03) async def test_timeout_client_timeout_does_not_override_tool_call_timeout_if_lower( self, sse_server: str @@ -165,4 +165,4 @@ class TestTimeout: transport=SSETransport(sse_server), timeout=0.1, ) as client: - await client.call_tool("sleep", {"seconds": 0.01}, timeout=2) + await client.call_tool("sleep", {"seconds": 0.03}, timeout=2) From 3a21e7b9b22bd3cc533c19d7a9bac866f961f85e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:04:12 -0400 Subject: [PATCH 2/8] Fix path resolution in install commands (#1951) --- src/fastmcp/cli/install/claude_code.py | 6 +-- src/fastmcp/cli/install/claude_desktop.py | 6 +-- src/fastmcp/cli/install/cursor.py | 12 +++--- src/fastmcp/cli/install/gemini_cli.py | 6 +-- src/fastmcp/cli/install/mcp_json.py | 6 +-- src/fastmcp/client/transports.py | 6 +-- .../mcp_server_config/v1/environments/uv.py | 12 +++--- .../mcp_server_config/v1/mcp_server_config.py | 6 +-- tests/cli/test_config.py | 15 +++++--- .../cli/test_mcp_server_config_integration.py | 5 ++- tests/client/transports/test_uv_transport.py | 2 +- tests/utilities/test_cli.py | 37 ++++++++++--------- 12 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 5a9a98625..da475afb5 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -110,9 +110,9 @@ def install_claude_code( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements) if with_requirements else None, - project=str(project) if project else None, - editable=[str(p) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index 803cf5286..bd8cf395d 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -76,9 +76,9 @@ def install_claude_desktop( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements) if with_requirements else None, - project=str(project) if project else None, - editable=[str(p) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components if server_object: diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 8c13c2803..dd885e5ea 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -110,9 +110,9 @@ def install_cursor_workspace( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements.resolve()) if with_requirements else None, - project=str(project.resolve()) if project else None, - editable=[str(p.resolve()) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components if server_object: @@ -180,9 +180,9 @@ def install_cursor( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements.resolve()) if with_requirements else None, - project=str(project.resolve()) if project else None, - editable=[str(p.resolve()) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components if server_object: diff --git a/src/fastmcp/cli/install/gemini_cli.py b/src/fastmcp/cli/install/gemini_cli.py index c09016990..8acaa413e 100644 --- a/src/fastmcp/cli/install/gemini_cli.py +++ b/src/fastmcp/cli/install/gemini_cli.py @@ -107,9 +107,9 @@ def install_gemini_cli( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements) if with_requirements else None, - project=str(project) if project else None, - editable=[str(p) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 7dcb0c0da..95ccb3ca3 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -51,9 +51,9 @@ def install_mcp_json( env_config = UVEnvironment( python=python_version, dependencies=(with_packages or []) + ["fastmcp"], - requirements=str(with_requirements) if with_requirements else None, - project=str(project) if project else None, - editable=[str(p) for p in with_editable] if with_editable else None, + requirements=with_requirements, + project=project, + editable=with_editable, ) # Build server spec from parsed components if server_object: diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 8492a5125..1016d35e0 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -583,15 +583,15 @@ class UvStdioTransport(StdioTransport): command: str, args: list[str] | None = None, module: bool = False, - project_directory: str | None = None, + project_directory: Path | None = None, python_version: str | None = None, with_packages: list[str] | None = None, - with_requirements: str | None = None, + with_requirements: Path | None = None, env_vars: dict[str, str] | None = None, keep_alive: bool | None = None, ): # Basic validation - if project_directory and not Path(project_directory).exists(): + if project_directory and not project_directory.exists(): raise NotADirectoryError( f"Project directory not found: {project_directory}" ) diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py index 6f71cd3ac..a88965435 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py @@ -28,19 +28,19 @@ class UVEnvironment(Environment): examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]], ) - requirements: str | None = Field( + requirements: Path | None = Field( default=None, description="Path to requirements.txt file", examples=["requirements.txt", "../requirements/prod.txt"], ) - project: str | None = Field( + project: Path | None = Field( default=None, description="Path to project directory containing pyproject.toml", examples=[".", "../my-project"], ) - editable: list[str] | None = Field( + editable: list[Path] | None = Field( default=None, description="Directories to install in editable mode", examples=[[".", "../my-package"], ["/path/to/package"]], @@ -64,7 +64,7 @@ class UVEnvironment(Environment): # Add project if specified if self.project: - args.extend(["--project", str(self.project)]) + args.extend(["--project", str(self.project.resolve())]) # Add Python version if specified (only if no project, as project has its own Python) if self.python and not self.project: @@ -78,12 +78,12 @@ class UVEnvironment(Environment): # Add requirements file if self.requirements: - args.extend(["--with-requirements", str(self.requirements)]) + args.extend(["--with-requirements", str(self.requirements.resolve())]) # Add editable packages if self.editable: for editable_path in self.editable: - args.extend(["--with-editable", str(editable_path)]) + args.extend(["--with-editable", str(editable_path.resolve())]) # Add the command args.extend(command) diff --git a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py index c269d0eda..1345d7f59 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py @@ -291,9 +291,9 @@ class MCPServerConfig(BaseModel): environment = UVEnvironment( python=python, dependencies=dependencies, - requirements=requirements, - project=project, - editable=[editable] if editable else None, + requirements=Path(requirements) if requirements else None, + project=Path(project) if project else None, + editable=[Path(editable)] if editable else None, ) # Build deployment config if any deployment args provided diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 7865e45c5..1a5d307eb 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -66,9 +66,10 @@ class TestEnvironment: env = config.environment assert env.python == "3.12" assert env.dependencies == ["requests", "numpy>=2.0"] - assert env.requirements == "requirements.txt" - assert env.project == "." - assert env.editable == ["../my-package"] + # Paths are stored as Path objects + assert env.requirements == Path("requirements.txt") + assert env.project == Path(".") + assert env.editable == [Path("../my-package")] def test_needs_uv(self): """Test needs_uv() method.""" @@ -112,12 +113,16 @@ class TestEnvironment: assert "--python" not in cmd assert "3.12" not in cmd assert "--project" in cmd - assert "." in cmd + # Project path should be resolved to absolute path + project_idx = cmd.index("--project") + assert Path(cmd[project_idx + 1]).is_absolute() assert "--with" in cmd assert "requests" in cmd assert "numpy" in cmd assert "--with-requirements" in cmd - assert "requirements.txt" in cmd + # Requirements path should be resolved to absolute path + req_idx = cmd.index("--with-requirements") + assert Path(cmd[req_idx + 1]).is_absolute() # Command args should be at the end assert "fastmcp" in cmd[-3:] assert "run" in cmd[-2:] diff --git a/tests/cli/test_mcp_server_config_integration.py b/tests/cli/test_mcp_server_config_integration.py index 05a98687c..219532b53 100644 --- a/tests/cli/test_mcp_server_config_integration.py +++ b/tests/cli/test_mcp_server_config_integration.py @@ -234,10 +234,11 @@ class TestPathResolution: assert config.environment is not None uv_cmd = config.environment.build_command(["fastmcp", "run"]) - # Should include requirements file + # Should include requirements file with absolute path assert "--with-requirements" in uv_cmd req_idx = uv_cmd.index("--with-requirements") + 1 - assert uv_cmd[req_idx] == "requirements.txt" + assert Path(uv_cmd[req_idx]).is_absolute() + assert Path(uv_cmd[req_idx]).name == "requirements.txt" class TestConfigValidation: diff --git a/tests/client/transports/test_uv_transport.py b/tests/client/transports/test_uv_transport.py index 45a2c3cfc..020f084ba 100644 --- a/tests/client/transports/test_uv_transport.py +++ b/tests/client/transports/test_uv_transport.py @@ -84,7 +84,7 @@ async def test_uv_transport_module(): with_packages=["fastmcp"], command="my_module", module=True, - project_directory=tmpdir, + project_directory=Path(tmpdir), keep_alive=False, ) ) diff --git a/tests/utilities/test_cli.py b/tests/utilities/test_cli.py index a14aab2f5..f463c2913 100644 --- a/tests/utilities/test_cli.py +++ b/tests/utilities/test_cli.py @@ -1,5 +1,7 @@ """Tests for CLI utility functions.""" +from pathlib import Path + from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment @@ -16,14 +18,14 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_editable(self): """Test building uv command with editable package.""" - editable_path = "/path/to/package" + editable_path = Path("/path/to/package") env = UVEnvironment(editable=[editable_path]) cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", "--with-editable", - editable_path, + str(editable_path.resolve()), "fastmcp", "run", "server.py", @@ -64,14 +66,14 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_requirements(self): """Test building uv command with requirements file.""" - requirements_path = "/path/to/requirements.txt" + requirements_path = Path("/path/to/requirements.txt") env = UVEnvironment(requirements=requirements_path) cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", "--with-requirements", - requirements_path, + str(requirements_path.resolve()), "fastmcp", "run", "server.py", @@ -80,14 +82,14 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_project(self): """Test building uv command with project directory.""" - project_path = "/path/to/project" + project_path = Path("/path/to/project") env = UVEnvironment(project=project_path) cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", "--project", - project_path, + str(project_path.resolve()), "fastmcp", "run", "server.py", @@ -96,8 +98,8 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_everything(self): """Test building uv command with all options.""" - requirements_path = "/path/to/requirements.txt" - editable_path = "/local/pkg" + requirements_path = Path("/path/to/requirements.txt") + editable_path = Path("/local/pkg") env = UVEnvironment( python="3.10", dependencies=["pandas", "numpy"], @@ -115,9 +117,9 @@ class TestEnvironmentBuildUVRunCommand: "--with", "pandas", "--with-requirements", - requirements_path, + str(requirements_path.resolve()), "--with-editable", - editable_path, + str(editable_path.resolve()), "fastmcp", "run", "server.py", @@ -129,23 +131,24 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_project_with_extras(self): """Test that project flag works with additional dependencies.""" - project_path = "/path/to/project" + project_path = Path("/path/to/project") + editable_path = Path("/pkg") env = UVEnvironment( project=project_path, python="3.10", # Should be ignored with project dependencies=["pandas"], # Should be added on top of project - editable=["/pkg"], # Should be added on top of project + editable=[editable_path], # Should be added on top of project ) cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", "--project", - project_path, + str(project_path.resolve()), "--with", "pandas", "--with-editable", - "/pkg", + str(editable_path.resolve()), "fastmcp", "run", "server.py", @@ -168,17 +171,17 @@ class TestEnvironmentNeedsUV: def test_needs_uv_with_requirements(self): """Test that needs_uv returns True with requirements.""" - env = UVEnvironment(requirements="/path/to/requirements.txt") + env = UVEnvironment(requirements=Path("/path/to/requirements.txt")) assert env._must_run_with_uv() is True def test_needs_uv_with_project(self): """Test that needs_uv returns True with project.""" - env = UVEnvironment(project="/path/to/project") + env = UVEnvironment(project=Path("/path/to/project")) assert env._must_run_with_uv() is True def test_needs_uv_with_editable(self): """Test that needs_uv returns True with editable.""" - env = UVEnvironment(editable=["/pkg"]) + env = UVEnvironment(editable=[Path("/pkg")]) assert env._must_run_with_uv() is True def test_needs_uv_empty(self): From 27dbb9eabec94e3730b3bb172ad90422595ef5b0 Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:12:24 -0400 Subject: [PATCH 3/8] chore: Update fastmcp.json schema (#1955) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/public/schemas/fastmcp.json/latest.json | 3 +++ docs/public/schemas/fastmcp.json/v1.json | 3 +++ src/fastmcp/utilities/mcp_server_config/v1/schema.json | 3 +++ 3 files changed, 9 insertions(+) diff --git a/docs/public/schemas/fastmcp.json/latest.json b/docs/public/schemas/fastmcp.json/latest.json index e027cf967..aa1f59ce4 100644 --- a/docs/public/schemas/fastmcp.json/latest.json +++ b/docs/public/schemas/fastmcp.json/latest.json @@ -250,6 +250,7 @@ "requirements": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -267,6 +268,7 @@ "project": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -285,6 +287,7 @@ "anyOf": [ { "items": { + "format": "path", "type": "string" }, "type": "array" diff --git a/docs/public/schemas/fastmcp.json/v1.json b/docs/public/schemas/fastmcp.json/v1.json index e027cf967..aa1f59ce4 100644 --- a/docs/public/schemas/fastmcp.json/v1.json +++ b/docs/public/schemas/fastmcp.json/v1.json @@ -250,6 +250,7 @@ "requirements": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -267,6 +268,7 @@ "project": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -285,6 +287,7 @@ "anyOf": [ { "items": { + "format": "path", "type": "string" }, "type": "array" diff --git a/src/fastmcp/utilities/mcp_server_config/v1/schema.json b/src/fastmcp/utilities/mcp_server_config/v1/schema.json index e027cf967..aa1f59ce4 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/schema.json +++ b/src/fastmcp/utilities/mcp_server_config/v1/schema.json @@ -250,6 +250,7 @@ "requirements": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -267,6 +268,7 @@ "project": { "anyOf": [ { + "format": "path", "type": "string" }, { @@ -285,6 +287,7 @@ "anyOf": [ { "items": { + "format": "path", "type": "string" }, "type": "array" From d9fe3733585927054c053aa6e09e0ce3fc3a9779 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:12:59 -0400 Subject: [PATCH 4/8] Fix SSE endpoint accepting POST requests instead of returning 405 (#1953) --- src/fastmcp/server/auth/auth.py | 44 +++++-------------- src/fastmcp/server/auth/oauth_proxy.py | 5 +-- src/fastmcp/server/auth/providers/descope.py | 7 +-- src/fastmcp/server/auth/providers/scalekit.py | 7 +-- src/fastmcp/server/auth/providers/workos.py | 7 +-- src/fastmcp/server/http.py | 43 ++++++++++++------ 6 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/fastmcp/server/auth/auth.py b/src/fastmcp/server/auth/auth.py index e67928651..de545c7c2 100644 --- a/src/fastmcp/server/auth/auth.py +++ b/src/fastmcp/server/auth/auth.py @@ -3,10 +3,7 @@ from __future__ import annotations from typing import Any from mcp.server.auth.middleware.auth_context import AuthContextMiddleware -from mcp.server.auth.middleware.bearer_auth import ( - BearerAuthBackend, - RequireAuthMiddleware, -) +from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend from mcp.server.auth.provider import ( AccessToken as _SDKAccessToken, ) @@ -81,7 +78,6 @@ class AuthProvider(TokenVerifierProtocol): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get the routes for this authentication provider. @@ -93,30 +89,13 @@ class AuthProvider(TokenVerifierProtocol): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata, but the + provider does not create the actual MCP endpoint route. Returns: - List of routes for this provider, including protected MCP endpoints if provided + List of routes for this provider (excluding the MCP endpoint itself) """ - - routes = [] - - # Add protected MCP endpoint if provided - if mcp_path and mcp_endpoint: - resource_metadata_url = self._get_resource_url( - "/.well-known/oauth-protected-resource" - ) - - routes.append( - Route( - mcp_path, - endpoint=RequireAuthMiddleware( - mcp_endpoint, self.required_scopes, resource_metadata_url - ), - ) - ) - - return routes + return [] def get_middleware(self) -> list: """Get HTTP application-level middleware for this auth provider. @@ -225,14 +204,13 @@ class RemoteAuthProvider(AuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes for this provider. - Creates protected resource metadata routes and optionally wraps MCP endpoints with auth. + Creates protected resource metadata routes. """ - # Start with base routes (protected MCP endpoint) - routes = super().get_routes(mcp_path, mcp_endpoint) + # Start with base routes + routes = super().get_routes(mcp_path) # Get the resource URL based on the MCP path resource_url = self._get_resource_url(mcp_path) @@ -326,14 +304,12 @@ class OAuthProvider( def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth authorization server routes and optional protected resource routes. This method creates the full set of OAuth routes including: - Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.) - Optional protected resource routes - - Protected MCP endpoints if provided Returns: List of OAuth routes @@ -366,7 +342,7 @@ class OAuthProvider( ) oauth_routes.extend(protected_routes) - # Add protected MCP endpoint from base class - oauth_routes.extend(super().get_routes(mcp_path, mcp_endpoint)) + # Add base routes + oauth_routes.extend(super().get_routes(mcp_path)) return oauth_routes diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py index 5d0dd217d..97f5e7526 100644 --- a/src/fastmcp/server/auth/oauth_proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy.py @@ -873,7 +873,6 @@ class OAuthProxy(OAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes with custom proxy token handler. @@ -882,10 +881,10 @@ class OAuthProxy(OAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get standard OAuth routes from parent class - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) custom_routes = [] token_route_found = False diff --git a/src/fastmcp/server/auth/providers/descope.py b/src/fastmcp/server/auth/providers/descope.py index 1195cdfb9..03ae1d007 100644 --- a/src/fastmcp/server/auth/providers/descope.py +++ b/src/fastmcp/server/auth/providers/descope.py @@ -7,8 +7,6 @@ for seamless MCP client authentication. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict @@ -127,7 +125,6 @@ class DescopeProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including Descope authorization server metadata forwarding. @@ -136,10 +133,10 @@ class DescopeProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward Descope OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/auth/providers/scalekit.py b/src/fastmcp/server/auth/providers/scalekit.py index 8c6a0a79d..a0c568515 100644 --- a/src/fastmcp/server/auth/providers/scalekit.py +++ b/src/fastmcp/server/auth/providers/scalekit.py @@ -7,8 +7,6 @@ authentication for seamless MCP client authentication. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict @@ -135,7 +133,6 @@ class ScalekitProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including Scalekit authorization server metadata forwarding. @@ -144,10 +141,10 @@ class ScalekitProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward Scalekit OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py index d68e16c71..b0c5f1817 100644 --- a/src/fastmcp/server/auth/providers/workos.py +++ b/src/fastmcp/server/auth/providers/workos.py @@ -10,8 +10,6 @@ Choose based on your WorkOS setup and authentication requirements. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -364,7 +362,6 @@ class AuthKitProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including AuthKit authorization server metadata forwarding. @@ -373,10 +370,10 @@ class AuthKitProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward AuthKit OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 21271129a..a5e41daf6 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -167,16 +167,25 @@ def create_sse_app( # Get auth middleware from the provider auth_middleware = auth.get_middleware() - # Get auth routes including protected MCP endpoint - auth_routes = auth.get_routes( - mcp_path=sse_path, - mcp_endpoint=handle_sse, - ) - + # Get auth provider's own routes (OAuth endpoints, metadata, etc) + auth_routes = auth.get_routes(mcp_path=sse_path) server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) - # Manually wrap the SSE message endpoint with RequireAuthMiddleware + # Create protected SSE endpoint route with GET method only + server_routes.append( + Route( + sse_path, + endpoint=RequireAuthMiddleware( + handle_sse, + auth.required_scopes, + auth._get_resource_url("/.well-known/oauth-protected-resource"), + ), + methods=["GET"], + ) + ) + + # Wrap the SSE message endpoint with RequireAuthMiddleware server_routes.append( Mount( message_path, @@ -274,14 +283,22 @@ def create_streamable_http_app( # Get auth middleware from the provider auth_middleware = auth.get_middleware() - # Get auth routes including protected MCP endpoint - auth_routes = auth.get_routes( - mcp_path=streamable_http_path, - mcp_endpoint=streamable_http_app, - ) - + # Get auth provider's own routes (OAuth endpoints, metadata, etc) + auth_routes = auth.get_routes(mcp_path=streamable_http_path) server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) + + # Create protected HTTP endpoint route + server_routes.append( + Route( + streamable_http_path, + endpoint=RequireAuthMiddleware( + streamable_http_app, + auth.required_scopes, + auth._get_resource_url("/.well-known/oauth-protected-resource"), + ), + ) + ) else: # No auth required server_routes.append( From 69d9550cd722108e5fc80564a839a69dfd071429 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:28:46 -0400 Subject: [PATCH 5/8] Upgrade GitHub workflows to claude-code-action@v1 (#1956) --- .github/workflows/marvin-dedupe-issues.yml | 13 ++++++++----- .github/workflows/marvin-label-triage.yml | 14 ++++++++------ .github/workflows/marvin.yml | 6 +++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml index 7e3b5a27b..9c943f9d4 100644 --- a/.github/workflows/marvin-dedupe-issues.yml +++ b/.github/workflows/marvin-dedupe-issues.yml @@ -51,6 +51,7 @@ jobs: - Use `gh` to interact with GitHub, rather than web fetch - Do not use other tools, beyond `gh` and Task (eg. don't use other MCP servers, file edit, etc.) - Make a todo list first + - Never include this issue as a duplicate of itself For your comment, follow this format precisely (example with 3 suspected duplicates): @@ -70,11 +71,13 @@ jobs: EOF - name: Run Marvin dedupe command - uses: anthropics/claude-code-base-action@beta + uses: anthropics/claude-code-action@v1 with: - model: claude-3-5-haiku-latest prompt_file: /tmp/claude-prompts/dedupe-prompt.txt - allowed_tools: "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} - claude_env: | - GH_TOKEN: ${{ steps.marvin-token.outputs.token }} + claude_args: | + --allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task + settings: | + { + "GH_TOKEN": "${{ steps.marvin-token.outputs.token }}" + } diff --git a/.github/workflows/marvin-label-triage.yml b/.github/workflows/marvin-label-triage.yml index bc5dfdcdc..a31991b3e 100644 --- a/.github/workflows/marvin-label-triage.yml +++ b/.github/workflows/marvin-label-triage.yml @@ -146,12 +146,14 @@ jobs: EOF - name: Run Marvin for Issue Triage - uses: anthropics/claude-code-base-action@beta + uses: anthropics/claude-code-action@v1 with: prompt_file: /tmp/claude-prompts/triage-prompt.txt - allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__get_pull_request_files" - timeout_minutes: "5" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} - mcp_config: /tmp/mcp-config/mcp-servers.json - claude_env: | - GH_TOKEN: ${{ steps.marvin-token.outputs.token }} + claude_args: | + --allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__get_pull_request_files + --mcp-config /tmp/mcp-config/mcp-servers.json + settings: | + { + "GH_TOKEN": "${{ steps.marvin-token.outputs.token }}" + } diff --git a/.github/workflows/marvin.yml b/.github/workflows/marvin.yml index 8d26185a9..cd2fef58a 100644 --- a/.github/workflows/marvin.yml +++ b/.github/workflows/marvin.yml @@ -59,13 +59,13 @@ jobs: # Marvin Assistant - name: Run Marvin - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: github_token: ${{ steps.marvin-token.outputs.token }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - mode: tag trigger_phrase: "/marvin" allowed_bots: "*" - allowed_tools: "WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request" + claude_args: | + --allowedTools WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request additional_permissions: | actions: read From b53604ca76b5e7b3a8de5e384d9bb85eb0d10d2b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:35:44 -0400 Subject: [PATCH 6/8] Fix claude-code-action@v1 authentication (#1958) --- .github/workflows/marvin-dedupe-issues.yml | 2 ++ .github/workflows/marvin-label-triage.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml index 9c943f9d4..a7ba89f68 100644 --- a/.github/workflows/marvin-dedupe-issues.yml +++ b/.github/workflows/marvin-dedupe-issues.yml @@ -17,6 +17,7 @@ jobs: permissions: contents: read issues: write + id-token: write steps: - name: Checkout repository @@ -73,6 +74,7 @@ jobs: - name: Run Marvin dedupe command uses: anthropics/claude-code-action@v1 with: + github_token: ${{ steps.marvin-token.outputs.token }} prompt_file: /tmp/claude-prompts/dedupe-prompt.txt anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} claude_args: | diff --git a/.github/workflows/marvin-label-triage.yml b/.github/workflows/marvin-label-triage.yml index a31991b3e..9cba2c6a5 100644 --- a/.github/workflows/marvin-label-triage.yml +++ b/.github/workflows/marvin-label-triage.yml @@ -24,6 +24,7 @@ jobs: contents: read issues: write pull-requests: write + id-token: write steps: - name: Checkout base repository @@ -148,6 +149,7 @@ jobs: - name: Run Marvin for Issue Triage uses: anthropics/claude-code-action@v1 with: + github_token: ${{ steps.marvin-token.outputs.token }} prompt_file: /tmp/claude-prompts/triage-prompt.txt anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} claude_args: | From 02b6a76d11cf0a38dea06e9eb3ffd0e6609a7684 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:42:24 -0400 Subject: [PATCH 7/8] Fix prompt_file input and bot attribution (#1960) --- .github/workflows/marvin-dedupe-issues.yml | 11 +++++++---- .github/workflows/marvin-label-triage.yml | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml index a7ba89f68..5d83cbdf7 100644 --- a/.github/workflows/marvin-dedupe-issues.yml +++ b/.github/workflows/marvin-dedupe-issues.yml @@ -30,10 +30,11 @@ jobs: app-id: ${{ secrets.MARVIN_APP_ID }} private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }} - - name: Create dedupe prompt + - name: Set dedupe prompt + id: dedupe-prompt run: | - mkdir -p /tmp/claude-prompts - cat > /tmp/claude-prompts/dedupe-prompt.txt << 'EOF' + cat >> $GITHUB_OUTPUT << 'EOF' + PROMPT< /tmp/claude-prompts/triage-prompt.txt << 'EOF' + cat >> $GITHUB_OUTPUT << 'EOF' + PROMPT< Date: Mon, 29 Sep 2025 21:04:05 -0400 Subject: [PATCH 8/8] Add --model claude-sonnet-4-5-20250929 to all workflows (#1963) --- .github/workflows/marvin-dedupe-issues.yml | 7 ++++--- .github/workflows/marvin-label-triage.yml | 1 + .github/workflows/marvin.yml | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml index 5d83cbdf7..84a88f063 100644 --- a/.github/workflows/marvin-dedupe-issues.yml +++ b/.github/workflows/marvin-dedupe-issues.yml @@ -39,13 +39,13 @@ jobs: Follow these steps precisely: - 1. Use the Task tool to check if the GitHub issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed. + 1. Check if the GitHub issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed. - 2. Use the Task tool to view the GitHub issue, and ask the agent to return a summary of the issue + 2. View the GitHub issue and produce a summary of the issue 3. Then, launch 3 parallel agents using the Task tool to search GitHub for duplicates of this issue, using diverse keywords and search approaches, using the summary from step 2 - 4. Next, feed the results from steps 2 and 3 into another agent using the Task tool, so that it can filter out false positives that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed. + 4. Next, consider the results from steps 2 and 3 and filter out false positives that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed. 5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit. @@ -81,6 +81,7 @@ jobs: prompt: ${{ steps.dedupe-prompt.outputs.PROMPT }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} claude_args: | + --model claude-sonnet-4-5-20250929 --allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task settings: | { diff --git a/.github/workflows/marvin-label-triage.yml b/.github/workflows/marvin-label-triage.yml index 4264741fd..549bbe217 100644 --- a/.github/workflows/marvin-label-triage.yml +++ b/.github/workflows/marvin-label-triage.yml @@ -156,6 +156,7 @@ jobs: prompt: ${{ steps.triage-prompt.outputs.PROMPT }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} claude_args: | + --model claude-sonnet-4-5-20250929 --allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__get_pull_request_files --mcp-config /tmp/mcp-config/mcp-servers.json settings: | diff --git a/.github/workflows/marvin.yml b/.github/workflows/marvin.yml index cd2fef58a..8b16e5928 100644 --- a/.github/workflows/marvin.yml +++ b/.github/workflows/marvin.yml @@ -66,6 +66,7 @@ jobs: trigger_phrase: "/marvin" allowed_bots: "*" claude_args: | + --model claude-sonnet-4-5-20250929 --allowedTools WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request additional_permissions: | actions: read