feat(tasks): import and relay client MCP servers into cloud task runs - #68954
Conversation
|
Hey @richardsolomou! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
|
Reviews (1): Last reviewed commit: "chore: update OpenAPI generated types" | Re-trigger Greptile |
🤖 CI report✅ Bundle size — no changeUncompressed size of every built Total: 64.77 MiB · no change No file changed by more than 1000 B. Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report ✅ Eager graph — within budgetHow much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy
🟢 Largest files eagerly shipped from
|
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.3 KiB | ../node_modules/.pnpm/posthog-js@1.402.3/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 235.5 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 222.9 KiB | ../node_modules/.pnpm/posthog-js@1.402.3/node_modules/posthog-js/dist/module.js |
| 164.0 KiB | src/queries/validators.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 105.8 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 92.7 KiB | ../packages/quill/packages/quill/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Dist folder size — 🔺 +8.7 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1312.69 MiB · 🔺 +8.7 KiB (+0.0%)
⚠️ Backend coverage — 62.0% of changed backend lines covered — 133 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ████████████░░░░░░░░ 62.0% (225 / 358)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/tasks/backend/temporal/process_task/tests/test_utils.py |
0.0% | 896–899, 901, 903–904, 926, 936, 940, 951, 954, 958, 960–963, 965–968, 970–971, 974–977, 979–980, 982, 984, 992–993, 995–996, 1008, 1010–1011, 1014, 1016, 1019, 1030 |
products/tasks/backend/temporal/process_task/tests/test_send_followup_to_sandbox.py |
0.0% | 59, 96–97, 100, 103, 106, 111–114, 116–117, 128, 130–132 |
products/tasks/backend/temporal/process_task/activities/tests/test_start_agent_server.py |
0.0% | 184–189, 193–194, 197, 201, 205, 214, 219, 228–231, 255 |
products/tasks/backend/temporal/process_task/activities/send_followup_to_sandbox.py |
0.0% | 265–267 |
products/tasks/backend/temporal/process_task/utils.py |
9.1% | 470–471, 474–488, 493, 501, 518–520, 523–536, 548–551 |
products/tasks/backend/temporal/process_task/activities/start_agent_server.py |
10.0% | 213–215, 248–250, 252–254 |
products/tasks/backend/logic/services/docker_sandbox.py |
66.7% | 909 |
products/tasks/backend/logic/services/modal_sandbox.py |
66.7% | 1054 |
products/tasks/backend/presentation/serializers.py |
96.3% | 1576, 1579, 1631 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29483512350 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
tasks |
█████████████░░░░░░░ 67.4% |
25,785 / 38,250 |
signals |
████████████████░░░░ 79.1% |
19,035 / 24,073 |
data_modeling |
████████████████░░░░ 80.0% |
4,834 / 6,045 |
cdp |
████████████████░░░░ 80.7% |
3,118 / 3,864 |
notebooks |
█████████████████░░░ 84.3% |
6,343 / 7,520 |
agent_platform |
█████████████████░░░ 84.7% |
3,273 / 3,862 |
cohorts |
█████████████████░░░ 86.1% |
4,022 / 4,671 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
product_tours |
█████████████████░░░ 87.5% |
1,266 / 1,447 |
exports |
██████████████████░░ 88.3% |
6,891 / 7,800 |
conversations |
██████████████████░░ 88.9% |
16,129 / 18,133 |
dashboards |
██████████████████░░ 89.1% |
5,719 / 6,418 |
mcp_analytics |
██████████████████░░ 89.1% |
2,502 / 2,807 |
error_tracking |
██████████████████░░ 89.6% |
9,718 / 10,852 |
alerts |
██████████████████░░ 89.9% |
3,638 / 4,046 |
engineering_analytics |
██████████████████░░ 90.1% |
5,105 / 5,665 |
streamlit_apps |
██████████████████░░ 90.4% |
2,499 / 2,764 |
slack_app |
██████████████████░░ 90.6% |
9,511 / 10,503 |
marketing_analytics |
██████████████████░░ 90.8% |
11,514 / 12,684 |
product_analytics |
██████████████████░░ 91.1% |
5,599 / 6,143 |
data_warehouse |
██████████████████░░ 92.1% |
18,133 / 19,683 |
workflows |
██████████████████░░ 92.4% |
5,148 / 5,574 |
web_analytics |
███████████████████░ 92.7% |
13,624 / 14,691 |
ai_observability |
███████████████████░ 92.8% |
14,868 / 16,019 |
surveys |
███████████████████░ 92.9% |
5,687 / 6,120 |
posthog_ai |
███████████████████░ 93.2% |
1,322 / 1,418 |
approvals |
███████████████████░ 93.3% |
3,395 / 3,640 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
early_access_features |
███████████████████░ 93.8% |
848 / 904 |
endpoints |
███████████████████░ 94.1% |
8,606 / 9,143 |
skills |
███████████████████░ 94.4% |
2,827 / 2,995 |
revenue_analytics |
███████████████████░ 94.5% |
3,598 / 3,809 |
review_hog |
███████████████████░ 94.6% |
6,532 / 6,905 |
logs |
███████████████████░ 95.3% |
9,528 / 9,994 |
experiments |
███████████████████░ 95.6% |
24,179 / 25,296 |
replay_vision |
███████████████████░ 95.7% |
13,354 / 13,952 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
warehouse_sources |
███████████████████░ 96.3% |
237,267 / 246,438 |
feature_flags |
███████████████████░ 96.3% |
16,118 / 16,741 |
user_interviews |
███████████████████░ 96.4% |
2,242 / 2,325 |
data_catalog |
███████████████████░ 97.1% |
2,034 / 2,095 |
customer_analytics |
███████████████████░ 97.2% |
7,480 / 7,698 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
⚠️ Django migration SQL — 2 new migrations to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/tasks/backend/migrations/0059_taskrun_imported_mcp_servers.py
BEGIN;
--
-- Add field imported_mcp_servers to taskrun
--
ALTER TABLE "posthog_task_run" ADD COLUMN "imported_mcp_servers" text NULL;
COMMIT;products/tasks/backend/migrations/0060_taskrun_relayed_mcp_servers.py
BEGIN;
--
-- Add field relayed_mcp_servers to taskrun
--
ALTER TABLE "posthog_task_run" ADD COLUMN "relayed_mcp_servers" jsonb NULL;
COMMIT;Last updated: 2026-07-16 08:43 UTC (8324534)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 2 Safe | 0 Needs Review | 0 Blocked
✅ Safe
Brief or no lock, backwards compatible
tasks.0059_taskrun_imported_mcp_servers
└─ #1 ✅ AddField
Adding nullable field requires brief lock
model: taskrun, field: imported_mcp_servers
tasks.0060_taskrun_relayed_mcp_servers
└─ #1 ✅ AddField
Adding nullable field requires brief lock
model: taskrun, field: relayed_mcp_servers
📚 How to Deploy These Changes Safely
AddField:
This operation acquires a brief lock but doesn't rewrite the table.
Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.
Last updated: 2026-07-16 08:44 UTC (8324534)
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 3 · PR risk: 0/10 |
fda9db4 to
dce6f8e
Compare
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
555e3d7 to
a37db29
Compare
Backend half of PostHog/code#3231 (importing the user's local url-based MCP servers into cloud task runs). - Both run-creation endpoints (POST .../run/ and .../runs/) accept an optional write-only imported_mcp_servers list: {type: http|sse, name, url, headers: [{name, value}]}, already in the agent server's --mcpServers entry shape. Validation: <=20 servers, names <=64 chars, unique, "posthog" reserved, header values <=4KB, whole field <=32KB, and a server-side is_url_allowed SSRF re-check (the client's public/private classification is UX, not a security boundary). - Stored in a new encrypted TaskRun.imported_mcp_servers column (EncryptedJSONStringField) rather than the plain state JSON, since header values carry credentials; never returned by the API. - start_agent_server merges the stored entries into --mcpServers after the PostHog MCP and MCP Store installations (existing names win). Claude-only for now: codex-acp hard-fails on unreachable servers and the sandbox does no reachability pruning. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
- Fetch the TaskRun once in _prepare_launch and reuse it for both the event-ingest token and the imported MCP servers, dropping the second query on the claude path. - Assert the run/ endpoint response never echoes imported_mcp_servers, mirroring the runs/ test's credential-leak guard. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
The periodic MCP token refresh rebuilds the session's server list as PostHog MCP + installations only, and the agent treats a refresh_session list as authoritative — so client-imported servers silently vanished at the first mid-run token refresh. - New shared get_imported_mcp_server_configs(task_run, existing_names) in utils.py owns the claude-only gate and collision rule; both the launch path and _refresh_sandbox_mcp use it now. - build_imported_mcp_server_configs guards against non-list values in the encrypted column so stored drift can't break a launch. - Tests: refresh keeps imported servers (and posthog-name collisions still lose); non-list guard case. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Address review feedback: the missing-run check was only applied on the event-ingest path, so with ingest disabled a missing TaskRun row would silently launch without imported MCP servers instead of failing. Check unconditionally right after the fetch, raising SandboxExecutionError per this activity's convention. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
- mypy: type build_imported_mcp_server_configs' input as Any — the encrypted column is schemaless at read time and the guards that mypy flagged as unreachable exist precisely for non-conforming values; give SandboxExecutionError its required cause. - test_start_agent_server: the launch test now mocks TaskRun.objects.filter().first() to match the hoisted single fetch (its fake run-id isn't a valid UUID, so the real query would raise). - test_api: patch is_url_allowed in the two happy-path tests — CI runs the real SSRF guard, which does live DNS, and the example.com hosts don't resolve. The rejection tests keep exercising the real guard. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Address review feedback: reserved names were checked case-insensitively but duplicates case-sensitively, so ["MyServer", "myserver"] slipped through. Compare both checks on the same lowercased key — two imported names differing only by case are far more likely a client bug than intent. Error messages keep the original casing. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Django half of the PostHog Code MCP relay (docs/cloud-mcp-relay.md in
posthog/code): lets the creating desktop relay stdio/private-URL MCP
servers into a cloud sandbox over the existing event-stream + command
channel.
- TaskRun.relayed_mcp_servers: plain JSONField of {"name": ...} dicts —
names only, no secrets to encrypt; migration 0055.
- Run creation accepts relayed_mcp_servers on both endpoints (≤20,
case-insensitive dedupe, reserved names rejected, disjoint from
imported_mcp_servers), persisted with the same single save as
imported servers.
- Launch passes --relayMcpServers (names) to the agent server on both
sandbox backends; name collisions with resolved MCP configs drop.
- Command endpoint allows mcp_response: requestId + server plus exactly
one of payload/error, params ≤300KB, forwarded verbatim to the
sandbox and excluded from relay telemetry — response payloads carry
data from the user's private systems and must not be persisted.
Generated-By: PostHog Code
Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Extract the reserved-name + case-insensitive uniqueness check shared by the imported and relayed MCP server validators into `_validate_unique_unreserved_mcp_names`, clarify the `get_relayed_mcp_server_names` docstring (relay endpoints are loopback and always answer, so codex reachability probing is satisfied and relay names are not adapter-gated), and add tests: imported configs resolve for claude/unset adapters and return [] for codex, and mcp_response rejects a boolean error code. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
…rver The activity tests only adapted the mock for the `.filter().first()` refactor; none asserted that `_prepare_launch` resolves imported configs and relayed names and forwards them into `sandbox.start_agent_server`. Add a case that stores an imported server and a relayed name and asserts both reach the launch call. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Two test-only fixes for CI failures on the MCP import/relay change: - test_refresh_keeps_imported_mcp_servers patched a non-existent `create_oauth_access_token`; the module imports `create_oauth_access_token_for_run`. Corrected the patch target so the decorator no longer raises AttributeError. - test_start_agent_server_passes_initial_permission_mode did not mock `TaskRun.objects.filter`. Now that `_prepare_launch` fetches the TaskRun unconditionally, the real query ran with a non-UUID run id and raised ValueError. Added the same filter/first mock the sibling tests use. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
…itive The serializer validates MCP server names case-insensitively (reserved names and within-list duplicates are compared lowercased), but the launch-time reconciliation in build_imported_mcp_server_configs and get_relayed_mcp_server_names deduped case-sensitively. That let a name differing only in case from another source (imported vs relayed, or an MCP Store install) slip past the "existing servers win" collision check and register a near-duplicate. Lowercase the comparison in both functions while preserving each surviving entry's original casing, and cover it with tests. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
Without state={}, the Mock's auto-generated state attribute is truthy and
.get("runtime_adapter") returns a MagicMock, so get_imported_mcp_server_configs
bails on the adapter gate instead of taking the intended claude/unset path.
Behaviorally inert here (imported_mcp_servers=None yields [] either way), but
the mock should model a real TaskRun; matches the sibling forwards test.
Generated-By: PostHog Code
Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
dbd2c6e to
a0c68be
Compare
Problem
A cloud sandbox only gets the PostHog MCP and MCP Store servers baked into
--mcpServersat spawn; the user's local MCP servers never reach it. PostHog/code#3231 is the client half; this is the backend that validates, stores, and forwards.Changes
imported_mcp_servers(url-based entries merged into--mcpServers) andrelayed_mcp_servers(names of desktop-only servers, passed as--relayMcpServers).posthogreserved, existing servers win.mcp_responsecommand method forwards relay replies to the sandbox verbatim and is excluded from telemetry: payloads carry data from the user's private systems.How did you test this code?
stateor responses), SSRF rejection, and parameterized invalid-shape/limit cases.mcp_responseforwarding, and thestart_agent_serverforward.Automatic notifications
Docs update
N/A. Design docs (
cloud-mcp-import.md,cloud-mcp-relay.md) live in the PostHog/code repo.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored with PostHog Code. Skills invoked: rs-address-pr-review, rs-update-pr. Review feedback was verified adversarially: rebuttals posted where reviewer claims did not hold (relay command authorization, credential redaction scope), and the case-insensitive collision fix adopted from one.
Created with PostHog Code