diff --git a/products/tasks/backend/logic/services/mcp_url.py b/products/tasks/backend/logic/services/mcp_url.py new file mode 100644 index 000000000000..4ba1f064f232 --- /dev/null +++ b/products/tasks/backend/logic/services/mcp_url.py @@ -0,0 +1,20 @@ +from urllib.parse import urlparse + + +def resolve_mcp_url(*, sandbox_mcp_url: str | None, site_url: str | None) -> str | None: + if sandbox_mcp_url: + return sandbox_mcp_url + if not site_url: + return None + + hostname = urlparse(site_url).hostname or "" + if hostname in ("app.posthog.com", "us.posthog.com"): + return "https://mcp.posthog.com/mcp" + if hostname == "eu.posthog.com": + return "https://mcp-eu.posthog.com/mcp" + if hostname == "app.dev.posthog.dev": + return "https://mcp.dev.posthog.dev/mcp" + if hostname in ("localhost", "127.0.0.1"): + return "http://host.docker.internal:8787/mcp" + + return None diff --git a/products/tasks/backend/logic/services/modal_sandbox.py b/products/tasks/backend/logic/services/modal_sandbox.py index 3f3e07e7a01c..c259ac2b4f6c 100644 --- a/products/tasks/backend/logic/services/modal_sandbox.py +++ b/products/tasks/backend/logic/services/modal_sandbox.py @@ -79,6 +79,7 @@ LocalSkillsCache, populate_skills_directory, ) +from products.tasks.backend.logic.services.mcp_url import resolve_mcp_url from products.tasks.backend.logic.services.modal_provision_diagnostics import ( SandboxProvisionDiagnostics, capture_modal_output_if_debug, @@ -146,7 +147,6 @@ "gateway.us.posthog.com", "gateway.eu.posthog.com", "api.anthropic.com", - "mcp.posthog.com", ) @@ -157,6 +157,9 @@ def _session_init_probe_hosts() -> list[str]: reason to exist. """ hosts = list(SESSION_INIT_PROBE_HOSTS) + mcp_host = _hostname_from_url(resolve_mcp_url(sandbox_mcp_url=settings.SANDBOX_MCP_URL, site_url=settings.SITE_URL)) + if mcp_host and mcp_host not in hosts: + hosts.insert(0, mcp_host) for setting_name in ("SANDBOX_LLM_GATEWAY_URL", "SANDBOX_AI_GATEWAY_URL"): gateway_host = _hostname_from_url(getattr(settings, setting_name, None)) if gateway_host and gateway_host not in hosts: diff --git a/products/tasks/backend/logic/services/tests/test_modal_sandbox.py b/products/tasks/backend/logic/services/tests/test_modal_sandbox.py index 00fdbbbf13c3..c3c251da90f5 100644 --- a/products/tasks/backend/logic/services/tests/test_modal_sandbox.py +++ b/products/tasks/backend/logic/services/tests/test_modal_sandbox.py @@ -1048,13 +1048,14 @@ def test_reports_termination_when_sandbox_gone(self): assert "poll=137" in diagnostics["failure_reason"] sandbox._sandbox.exec.assert_not_called() + @override_settings(SITE_URL="https://eu.posthog.com", SANDBOX_MCP_URL=None) def test_reports_blocked_egress_host(self): sandbox = self._sandbox() def _exec(command: str, timeout_seconds: Any = None) -> ExecutionResult: if "printf" in command: return ExecutionResult( - stdout="api.anthropic.com code=200\nmcp.posthog.com http_code=000", + stdout="api.anthropic.com http_code=200\nmcp-eu.posthog.com http_code=000", stderr="", exit_code=0, error=None, @@ -1071,7 +1072,7 @@ def _exec(command: str, timeout_seconds: Any = None) -> ExecutionResult: assert diagnostics["sandbox_terminated"] == "false" assert "egress blocked" in diagnostics["failure_reason"] - assert "mcp.posthog.com" in diagnostics["failure_reason"] + assert "mcp-eu.posthog.com" in diagnostics["failure_reason"] def test_reports_alive_without_session_when_no_block(self): sandbox = self._sandbox() @@ -1607,6 +1608,28 @@ def test_publish_filesystem_image_uses_published_image_snapshot_timeout(self, mo class TestSessionInitProbeHosts: + @pytest.mark.parametrize( + ("site_url", "mcp_url", "expected_host", "unused_host"), + [ + ("https://us.posthog.com", None, "mcp.posthog.com", "mcp-eu.posthog.com"), + ("https://eu.posthog.com", None, "mcp-eu.posthog.com", "mcp.posthog.com"), + ( + "https://us.posthog.com", + "https://custom-mcp.example.com/mcp", + "custom-mcp.example.com", + "mcp.posthog.com", + ), + ], + ) + def test_includes_only_resolved_mcp_host( + self, site_url: str, mcp_url: str | None, expected_host: str, unused_host: str + ): + with override_settings(SITE_URL=site_url, SANDBOX_MCP_URL=mcp_url): + hosts = _session_init_probe_hosts() + + assert expected_host in hosts + assert unused_host not in hosts + @override_settings( SANDBOX_LLM_GATEWAY_URL="https://gateway.dev.posthog.dev", SANDBOX_AI_GATEWAY_URL="https://ai-gateway.dev.posthog.dev", diff --git a/products/tasks/backend/temporal/process_task/utils.py b/products/tasks/backend/temporal/process_task/utils.py index abad25149511..cec35da56fd0 100644 --- a/products/tasks/backend/temporal/process_task/utils.py +++ b/products/tasks/backend/temporal/process_task/utils.py @@ -5,7 +5,6 @@ from dataclasses import dataclass, field from enum import StrEnum from typing import TYPE_CHECKING, Any, Optional -from urllib.parse import urlparse from django.conf import settings from django.db import transaction @@ -28,6 +27,7 @@ filter_user_sandbox_env_vars, ) from products.tasks.backend.exceptions import CredentialUnavailableError +from products.tasks.backend.logic.services.mcp_url import resolve_mcp_url as _resolve_mcp_url # Re-exported so existing activity/workflow imports keep working after the move to # logic/services (non-temporal callers import run_actor directly). @@ -695,7 +695,7 @@ def get_sandbox_ph_mcp_configs( - app.dev.posthog.dev → https://mcp.dev.posthog.dev/mcp - Other hosts → empty list (MCP not available) """ - url = _resolve_mcp_url() + url = _resolve_mcp_url(sandbox_mcp_url=settings.SANDBOX_MCP_URL, site_url=settings.SITE_URL) if not url: return [] read_only = not has_write_scopes(scopes) @@ -711,31 +711,6 @@ def get_sandbox_ph_mcp_configs( return [McpServerConfig(type="http", name="posthog", url=url, headers=headers)] -def _resolve_mcp_url() -> str | None: - if settings.SANDBOX_MCP_URL: - return settings.SANDBOX_MCP_URL - - site_url = settings.SITE_URL - if not site_url: - return None - - hostname = urlparse(site_url).hostname or "" - if hostname in ("app.posthog.com", "us.posthog.com"): - return "https://mcp.posthog.com/mcp" - if hostname == "eu.posthog.com": - return "https://mcp-eu.posthog.com/mcp" - if hostname == "app.dev.posthog.dev": - return "https://mcp.dev.posthog.dev/mcp" - - # Local dev: point to the local wrangler dev MCP server via - # host.docker.internal, since the sandbox runs in Docker. - # On Linux without Docker Desktop, set SANDBOX_MCP_URL instead. - if hostname in ("localhost", "127.0.0.1"): - return "http://host.docker.internal:8787/mcp" - - return None - - def get_github_token(github_integration_id: int) -> Optional[str]: integration = Integration.objects.get(id=github_integration_id) github_integration = GitHubIntegration(integration)