Skip to content

Conversation

@ThomasK33
Copy link
Member

πŸ€– feat: HTTP/SSE MCP servers + PostHog usage telemetry

Adds support for MCP servers over:

  • Streamable HTTP
  • Legacy SSE
  • Auto transport (HTTP β†’ SSE fallback)

Also adds PostHog telemetry to measure MCP usage per message:

  • enabled/started/failed server counts
  • MCP vs total tool counts injected
  • transport mix and auto-fallback counts
  • MCP setup duration (base-2 rounded)

Validation:

  • make static-check

πŸ“‹ Implementation Plan

Add HTTP/SSE MCP server support

Goal

Enable Mux to connect to MCP servers over:

  • Streamable HTTP (current MCP spec)
  • Legacy HTTP+SSE (backwards compatibility)

…in addition to the existing stdio/NDJSON transport.

This should work end-to-end through Settings β†’ Projects (persisted to .mux/mcp.jsonc), including Test and tool allowlisting.

Recommended approach (net ~650 LoC, product code)

Use the existing @ai-sdk/mcp client wrapper (experimental_createMCPClient) and add:

  1. A richer MCP server config model (transport + endpoint + optional headers).
  2. A small header-resolution layer that supports static headers and Mux project secrets.
  3. Connection logic in MCPServerManager that can start either:
    • stdio server processes (existing), or
    • HTTP/SSE clients (new), including auto fallback (try Streamable HTTP, then fallback to legacy SSE on 400/404/405).
  4. Backend telemetry to PostHog to measure MCP adoption + per-message tool injection (see Telemetry (PostHog)).
Why this approach
  • Minimizes new dependencies: keep the existing AI SDK MCP integration.
  • Keeps stdio behavior unchanged (per-workspace processes, idle timeout, tool allowlists).
  • Adds HTTP/SSE with the same lifecycle/caching logic, so the rest of the app doesn’t need to learn new concepts.

Config format

Continue supporting the current formats:

  • Legacy string: "name": "<command>" β†’ stdio
  • Object form for advanced settings (disabled/toolAllowlist)

Extend object entries to support HTTP transports:

{
  "servers": {
    // stdio (existing)
    "memory": "npx -y @modelcontextprotocol/server-memory",

    // streamable HTTP (current spec)
    "remote": {
      "transport": "http", // or omit if url is present (defaults to auto)
      "url": "http://localhost:3333/mcp",
      "headers": {
        "Authorization": { "secret": "MCP_TOKEN" },
        "X-Client": "mux"
      }
    },

    // legacy SSE transport
    "legacy": {
      "transport": "sse",
      "url": "http://localhost:3333/sse"
    },

    // auto mode (try http then fallback to legacy sse)
    "auto": {
      "transport": "auto",
      "url": "http://localhost:3333/mcp"
    }
  }
}

Notes:

  • headers values are either:
    • a literal string, or
    • { "secret": "<project-secret-key>" } (resolved via Settings β†’ Projects β†’ Secrets).
  • Never include server commands/URLs/headers in the system prompt; only list server names (current behavior).

Backend changes

1) Types + schemas

  • Update src/common/types/mcp.ts:

    • Add transport: 'stdio' | 'http' | 'sse' | 'auto'.
    • Replace command-only server shape with a discriminated union:
      • stdio: { transport: 'stdio'; command: string; … }
      • http/sse/auto: { transport: 'http'|'sse'|'auto'; url: string; headers?: Record<string, string | {secret: string}>; … }
    • Keep disabled + toolAllowlist as common fields.
  • Update Zod schemas in src/common/orpc/schemas/mcp.ts to match.

    • Expand projects.mcp.add input from {name, command} to something like:
      • { name, transport, command?, url?, headers? }.
    • Expand projects.mcp.test input to support testing URL-based servers (plus transport).

2) Config parsing + persistence

Update src/node/services/mcpConfigService.ts:

  • Extend normalizeEntry to accept:
    • string β†’ stdio
    • object with command β†’ stdio
    • object with url β†’ http/auto (default transport: 'auto' unless explicitly set)
    • object with transport: 'sse' β†’ legacy SSE
  • Extend saveConfig to write:
    • strings for plain stdio servers,
    • objects for anything with non-default settings or non-stdio transports.

Add defensive validation:

  • Assert that stdio entries have command, HTTP entries have url.
  • Assert header values are either strings or {secret: string}.

3) Starting servers (stdio vs http/sse)

Update src/node/services/mcpServerManager.ts:

  • Change the β€œenabled servers” map from Record<string, string> to Record<string, MCPServerInfo> (filtered to enabled).

  • Add a helper:

    • resolveHeaders(serverInfo, projectSecrets): Record<string, string>
      • For {secret: key} look up in project secrets record.
      • Fail fast with a clear error if missing.
  • Add a helper to create clients:

    • createClientForServer(serverInfo, resolvedHeaders): Promise<MCPClient>
      • stdio: current MCPStdioTransport
      • http: experimental_createMCPClient({ transport: { type: 'http', url, headers } })
      • sse: experimental_createMCPClient({ transport: { type: 'sse', url, headers } })
      • auto:
        1. try http
        2. if error indicates 400/404/405 (per MCP transport spec), retry as sse
  • Keep existing behavior:

    • per-workspace caching/signature,
    • idle timeout cleanup,
    • tool wrapping (transformMCPResult) and allowlist filtering.

4) Plumbing secrets into MCP startup

  • Pass project secrets into MCPServerManager.getToolsForWorkspace (from AIService, which already loads secrets).
    • Avoid reading global config inside MCPServerManager to keep it testable.

5) Update system prompt wiring

  • buildSystemMessage can keep taking a map of server names; it only lists keys.
    • If types change, adjust the function signature to accept Record<string, unknown> or a new EnabledMCPServerMap type.

Telemetry (PostHog)

Goals

Capture privacy-safe usage metrics that answer:

  • How many MCP servers are enabled/started per submitted message?
  • How many MCP tools (and total tools) are injected into the model request?
  • Adoption/health of the new transports (http vs sse vs stdio) and auto-fallback.

Privacy constraints

Follow src/common/telemetry/payload.ts guidelines:

  • Do NOT send: server names, commands, URLs, headers, tool names, tool args/results, project paths.
  • Do send: counts, booleans, coarse enums, and base-2-rounded durations.

New events + properties

Add new backend-emitted telemetry events (captured via TelemetryService.capture, not renderer trackEvent):

  1. mcp_context_injected (emitted once per AIService.streamMessage right before startStream β€” even if no MCP servers are configured; counts may be 0)

Properties (all safe):

  • workspaceId (stable random ID)
  • model
  • mode
  • runtimeType (local|worktree|ssh)
  • mcp_server_enabled_count
  • mcp_server_started_count
  • mcp_server_failed_count
  • mcp_tool_count
  • total_tool_count
  • builtin_tool_count
  • mcp_transport_mode (none|stdio_only|http_only|sse_only|mixed)
  • mcp_has_http, mcp_has_sse, mcp_has_stdio (booleans)
  • mcp_auto_fallback_count (how many auto servers required fallback; 0 if not tracked)
  • mcp_setup_duration_ms_b2 (time spent creating MCP clients + fetching tools)
  1. mcp_server_tested

Emit from the projects.mcp.test handler after the test completes:

  • transport (stdio|http|sse|auto)
  • success (boolean)
  • duration_ms_b2
  • error_category (timeout|connect|http_status|unknown) without raw error strings
  1. mcp_server_config_changed

Emit from project MCP mutation handlers (add/remove/setEnabled/setToolAllowlist):

  • action (add|edit|remove|enable|disable|set_tool_allowlist|set_headers)
  • transport
  • has_headers (boolean)
  • uses_secret_headers (boolean)
  • tool_allowlist_size_b2 (only for allowlist updates)

Backend implementation details

  • Extend TelemetryEventPayload union in src/common/telemetry/payload.ts with the new event types.
  • Mirror those changes in src/common/orpc/schemas/telemetry.ts even if events are backend-only, so schema + documentation stay in sync.
  • Wire telemetry into AI code:
    • Add AIService.setTelemetryService(telemetryService: TelemetryService) (similar to setMCPServerManager).
    • Call it from ServiceContainer after constructing services.
    • In AIService.streamMessage, measure MCP setup timing and emit mcp_context_injected after mcpTools + final tools are known.
    • To get mcp_server_started_count reliably, return stats from MCPServerManager.getToolsForWorkspace (e.g. { tools, stats }) or add a separate getWorkspaceStats API.
Optional (nice-to-have): tool call telemetry

If you also want β€œhow often are MCP tools actually invoked?” add mcp_tool_calls_summary on stream-end:

  • Track tool-call events in AIService (it already re-emits StreamManager tool events).
  • Store a per-messageId set of MCP tool names in-memory only (never sent) so you can classify calls as MCP vs non-MCP safely.
  • Emit one summary event per stream (counts only).

This is a follow-up if we want to keep the first PR smaller.


PostHog dashboard

Create a dashboard (e.g., β€œMCP Usage”) and add the following insights:

  1. Messages w/ MCP context injected
  • Trend of event mcp_context_injected (count)
  1. Enabled vs started servers per message
  • Trend on mcp_context_injected
    • Series A: avg(mcp_server_enabled_count)
    • Series B: avg(mcp_server_started_count)
    • Series C: avg(mcp_server_failed_count)
  1. Tools injected per message
  • Trend on mcp_context_injected
    • avg(mcp_tool_count)
    • avg(total_tool_count)
    • avg(builtin_tool_count)
  1. Transport adoption
  • Trend mcp_context_injected with breakdown by mcp_transport_mode.
  1. Auto-fallback rate
  • Trend mcp_context_injected
    • avg(mcp_auto_fallback_count)
    • or filter mcp_auto_fallback_count > 0 and count.
  1. Server test success rate
  • Trend mcp_server_tested broken down by success and/or transport.
  1. Config changes over time
  • Trend mcp_server_config_changed broken down by action and transport.
Programmatic dashboard creation (optional)

If we want this automated (rather than manual UI steps), we can do it in Exec mode using the PostHog MCP tools:

  • Create a dashboard.
  • Create each insight with the appropriate query.
  • Add insights to the dashboard.

This is best done after the events ship so PostHog has the event/property definitions.

Frontend (Settings UI) changes

1) List + edit servers

Update src/browser/components/Settings/sections/ProjectSettingsSection.tsx:

  • Display a transport badge per server (stdio/http/sse/auto).
  • Show:
    • stdio: command
    • http/sse/auto: url

2) Add server form

  • Add a transport select:
    • Stdio (default)
    • HTTP (Streamable)
    • SSE (Legacy)
    • Auto (HTTP β†’ SSE)
  • Input label switches between Command and URL.

3) Headers UI (static + secret)

  • Add an optional β€œHeaders” collapsible section for HTTP/SSE servers:
    • Allow adding/removing header rows.
    • Each row supports:
      • literal value, or
      • {secret: <key>} (typed input; optionally a dropdown populated via projects.secrets.get).

Testing

Unit tests

  • Update and extend src/node/services/mcpConfigService.test.ts:

    • normalization for url + transport entries
    • ensure disabled/allowlist handling remains
    • ensure save writes expected minimal vs object forms
  • Add unit tests for MCP telemetry helpers (pure functions):

    • transport-mode computation (mcp_transport_mode)
    • error categorization for mcp_server_tested
    • stats calculation (enabled/started/failed counts) without leaking strings
  • Add a focused unit test file for header resolution + auto fallback decision logic (pure functions).

Integration test (recommended)

  • Add a small real MCP test server in tests/integration using an official MCP server fixture (or a tiny in-test server) that exposes 1–2 tools over:
    • streamable HTTP
    • legacy SSE

This verifies end-to-end compatibility without mocks.


Rollout / compatibility notes

  • Existing .mux/mcp.jsonc entries continue to work unchanged.
  • The UI should preserve transport-specific fields when editing (don’t accidentally convert HTTP servers to stdio).
  • Auto fallback should be best-effort and transparent:
    • Log when fallback occurs.
    • If auth/headers are wrong, surface the underlying error (don’t hide it behind fallback).
Alternative considered: adopt @modelcontextprotocol/sdk transports directly

This can improve spec compliance (session IDs, retries, etc.) by using StreamableHTTPClientTransport / SSEClientTransport directly.

It’s a reasonable follow-up if we hit limitations with @ai-sdk/mcp’s built-in HTTP/SSE config, but it likely adds ~150–250 LoC + a new dependency.


Generated with mux β€’ Model: openrouter:openai/gpt-5.2 β€’ Thinking: high

Change-Id: I9b4ba646bded38efa160d25e5d4514db2350c06c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…rt field

Change-Id: I7271e9951ede4d7f9963aad8daeaeea7ead1086c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Change-Id: I
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Change-Id: I95d4d351bdc317416d876df0c5111dad3210d8ba
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
Copy link
Member Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ThomasK33 ThomasK33 added this pull request to the merge queue Dec 17, 2025
@ThomasK33 ThomasK33 removed this pull request from the merge queue due to a manual request Dec 17, 2025
@ThomasK33 ThomasK33 merged commit a6099d3 into main Dec 17, 2025
20 checks passed
@ThomasK33 ThomasK33 deleted the mcp-server-xbf5 branch December 17, 2025 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant