Skip to content

Conversation

@ThomasK33
Copy link
Member

Summary

Mux MCP configuration is now global-first:

  • Global MCP servers are stored in ~/.mux/mcp.jsonc and managed via Settings → MCP.
  • Repos may optionally provide read-only overrides in ./.mux/mcp.jsonc (merged over global by server name).
  • Workspace overrides remain ./.mux/mcp.local.jsonc.

This PR also globalizes MCP secrets + OAuth:

  • Secrets are stored in ~/.mux/secrets.json using a __global__ sentinel plus per-project entries; project secrets override global.
  • MCP OAuth credentials are stored globally in ~/.mux/mcp-oauth.json using a V2 schema keyed by normalized server URL, with automatic V1→V2 migration.

Background

Previously MCP servers were configured per-project (<repo>/.mux/mcp.jsonc), forcing users to reconfigure the same servers across repositories. OAuth tokens were also stored project-scoped.

Implementation

  • Backend:
    • Refactored MCP config resolution + writes to support ~/.mux/mcp.jsonc (global) + optional repo overrides.
    • Added global + effective secrets helpers and top-level secrets.* ORPC endpoints.
    • Refactored MCP OAuth store to V2 (global by server URL) with migration and top-level mcpOauth.* ORPC endpoints.
  • UI:
    • Settings now has MCP (global) + Secrets tabs.
    • Workspace MCP modal lists servers from the effective global+repo view.

Follow-ups included

  • Removed the /mcp slash command (writes are global-only; docs updated accordingly).
  • Fixed IPC tests for global MCP semantics.
  • Fixed Secrets UI visibility leak on row deletion; hardened secrets store parsing, key normalization, and file permissions.

Validation

  • make fmt
  • make static-check
  • make test

📋 Implementation Plan

Plan: Make MCP + MCP OAuth global (with optional repo overrides)

Context / Why

Mux currently treats MCP server definitions as project-scoped (<project>/.mux/mcp.jsonc) with workspace-local overrides (<workspace>/.mux/mcp.local.jsonc). This forces users to reconfigure MCP servers for every project.

Goal: make MCP configuration global (JSONC-only):

  • Global MCP servers live at ~/.mux/mcp.jsonc and apply to the whole app.
  • Optional repo overrides can be provided via ./.mux/mcp.jsonc (no UI; checked into the repo).
  • Workspace-local overrides remain ./.mux/mcp.local.jsonc (gitignored).

Additionally, MCP OAuth was recently added and persists to ~/.mux/mcp-oauth.json but its schema is still project-scoped (projectPath -> serverName -> creds). We want to make OAuth credentials global as well (shared across projects), and migrate the on-disk schema.

Net result: configure MCP once, use everywhere; OAuth login once per server URL, use everywhere.

Evidence

  • Project MCP config is stored in .mux/mcp.jsonc via MCPConfigService (src/node/services/mcpConfigService.ts).
  • Workspace overrides are stored in .mux/mcp.local.jsonc via WorkspaceMcpOverridesService (src/node/services/workspaceMcpOverridesService.ts).
  • MCP server resolution + workspace overrides + allowlist logic lives in MCPServerManager (src/node/services/mcpServerManager.ts).
  • MCP Settings UI is project-scoped in src/browser/components/Settings/sections/ProjectSettingsSection.tsx and calls api.projects.mcp.*.
  • MCP OAuth store is ~/.mux/mcp-oauth.json but schema is V1 project-scoped and implemented in src/node/services/mcpOauthService.ts.
  • MCP OAuth ORPC endpoints are project-scoped under projects.mcpOauth (src/common/orpc/schemas/api.ts, src/node/orpc/router.ts).

User decisions captured via ask_user_question:

  • Repo override merge: merge with override winning by server name.
  • Config files: JSONC-only (.mux/mcp.jsonc for repo overrides, ~/.mux/mcp.jsonc for global base).
  • Secrets: {secret:"KEY"} should resolve from project secrets first, then global secrets.
  • Global MCP Settings “test server”: global-only context (no project selector).
  • Secrets: add a new Settings tab Secrets (with a Chromatic story) to manage global + per-project secrets.

Recommended approach (single approach)

Implement a global MCP config layer + global secrets + globalized MCP OAuth store.

Estimated net LoC (product code only): ~+650 / -450 (≈ +200 net)

  • Significant UI + API rewiring, plus schema migrations.

Implementation details

1) MCP config files: define the new resolution order

Update MCP server resolution to (JSONC-only):

  1. Inline servers (CLI --mcp, constructor inlineServers) – overrides by name.
  2. Workspace overrides: <workspace>/.mux/mcp.local.jsonc (existing behavior; gitignored).
  3. Repo override: <projectPath>/.mux/mcp.jsonc (optional; checked into the repo; no UI).
  4. Global base: ~/.mux/mcp.jsonc.

Merge semantics (per user):

  • effectiveServers = { ...globalServers, ...repoOverrideServers } (repo wins by name).
  • Workspace overrides keep their existing meaning (enabledServers/disabledServers + tool allowlist intersection).

2) Backend: refactor MCPConfigService to support global + overrides

Files:

  • src/node/services/mcpConfigService.ts
  • src/node/services/serviceContainer.ts
  • src/node/services/mcpConfigService.test.ts

Key changes:

  1. Inject Config so the service can locate config.rootDir.
  2. Add global path helper:
    • ~/.mux/mcp.jsonc (use path.join(config.rootDir, "mcp.jsonc")).
  3. Add repo override path helper:
    • .mux/mcp.jsonc (optional override; read-only)
  4. Single read API:
    • listServers(projectPath?: string): Promise<Record<string, MCPServerInfo>>
      • no projectPath ⇒ global servers only
      • with projectPath ⇒ merged { ...globalServers, ...repoOverrideServers } (repo wins by name)
  5. Split write APIs (write global only):
    • addServer(name, input)
    • removeServer(name)
    • setServerEnabled(name, enabled)
    • setToolAllowlist(name, toolAllowlist)

Suggested shape:

export class MCPConfigService {
  constructor(private readonly config: Config) {
    assert(config, "MCPConfigService requires Config");
  }

  async listServers(projectPath?: string): Promise<Record<string, MCPServerInfo>> {
    // Global base
    const global = await this.readGlobalServers();

    // Global-only mode (used by Settings → MCP)
    if (!projectPath) {
      return global;
    }

    // Repo override + merge
    const override = await this.readRepoOverrideServers(projectPath); // .jsonc only
    return { ...global, ...override };
  }

  async addServer(name: string, input: {...}): Promise<Result<void>> {
    const cfg = await this.getGlobalConfig();
    ...
    await this.saveGlobalConfig(cfg);
  }
}

Defensive programming notes:

  • Parse failures should be “fail closed”: treat as empty config (never crash startup).
  • When both global + repo config exist, merge by server name (repo wins) even if either file is missing/empty.

3) Backend: global secrets (project overrides global)

We need global secrets to support:

  • Header {secret:"KEY"} references in global MCP config.
  • “Test server” in global MCP Settings without selecting a project.

Files:

  • src/node/config.ts
  • src/node/services/aiService.ts
  • src/node/orpc/router.ts (MCP test endpoints)
  • src/common/types/secrets.ts (doc comment updates only)

Storage approach (minimal-change):

  • Keep ~/.mux/secrets.json as Record<string, Secret[]>.
  • Reserve a sentinel key (e.g. "__global__") for global secrets.

Add new Config helpers:

  • getGlobalSecrets(): Secret[]
  • updateGlobalSecrets(secrets: Secret[]): Promise<void>
  • getEffectiveSecrets(projectPath: string): Secret[] (merge; project wins by key)

Merge algorithm:

function mergeSecrets(global: Secret[], project: Secret[]): Secret[] {
  const byKey = new Map(global.map((s) => [s.key, s]));
  for (const s of project) byKey.set(s.key, s);
  return [...byKey.values()];
}

Wire-up changes:

  • In AIService (src/node/services/aiService.ts), replace config.getProjectSecrets() usages that feed tool env injection and MCP header resolution with config.getEffectiveSecrets() (project overrides global).
  • Add a top-level ORPC secrets.get/update that take an optional projectPath:
    • projectPath omitted ⇒ global secrets
    • projectPath provided ⇒ project-only secrets
      This powers the new Settings → Secrets UI (and can replace projects.secrets.* call sites over time).
  • Update any UI that offers “pick a secret key” (e.g. MCP headers) to use:
    • global secret keys when editing global MCP config
    • effective secret keys (global + project) when operating in a project context

4) ORPC: single MCP list/test endpoint (optional projectPath) + Secrets endpoints

We want:

  • One MCP “list/test” API that works in both contexts:
    • global (no projectPath) for Settings → MCP
    • project-scoped (with projectPath) for workspace UI, returning merged global + repo override servers
  • A Secrets API usable from a single Settings → Secrets UI for both global + per-project secrets.

Files:

  • src/common/orpc/schemas/api.ts
  • src/common/orpc/schemas/mcp.ts
  • src/node/orpc/router.ts

Proposed API shape:

  • Replace projects.mcp.* with a top-level mcp namespace:
    • mcp.list({ projectPath?: string })
      • projectPath omitted ⇒ global servers only
      • projectPath provided ⇒ { ...globalServers, ...repoOverrideServers } (repo wins by name)
    • mcp.test({ projectPath?: string, name?: string })
      • projectPath omitted ⇒ test in config.rootDir, secrets = global secrets only
      • projectPath provided ⇒ test in project cwd, secrets = effective secrets (project overrides global)
    • mcp.add/remove/setEnabled/setToolAllowlist ⇒ operate on global MCP config only (no projectPath).
  • Add a top-level secrets namespace:
    • secrets.get({ projectPath?: string }) ⇒ global or project-only secrets
    • secrets.update({ projectPath?: string, secrets })
  • Keep workspace.mcp.get/set as-is for workspace-local enable/disable + allowlist overrides.

Defensive programming notes:

  • If projectPath is provided but repo override file is unreadable/invalid, log + treat as empty override (still return global servers).

5) UI: add Settings → Secrets + make MCP settings global

Files:

  • src/browser/components/Settings/SettingsModal.tsx
  • src/browser/components/Settings/sections/SecretsSection.tsx (new)
  • src/browser/components/Settings/sections/ProjectSettingsSection.tsx (replace with MCPSettingsSection.tsx)
  • src/browser/components/WorkspaceMCPModal.tsx
  • src/browser/stories/App.settings.stories.tsx (Chromatic)
  • src/browser/stories/mocks/orpc.ts (story mocks)
  • src/browser/utils/slashCommands/registry.ts

Steps:

  1. Add a new Settings tab: Secrets
    • id: "secrets" / label “Secrets” / icon: lucide KeyRound (or similar).
  2. Implement SecretsSection (src/browser/components/Settings/sections/SecretsSection.tsx)
    • UI: scope selector (Global vs Project) + (when Project) a project dropdown.
    • Read/write via api.secrets.get/update({ projectPath?: string }).
    • Copy from existing SecretsModal UX (row editor, visibility toggle), preferably by extracting a shared SecretsEditor.
  3. Add a Chromatic story for the Secrets tab
    • Add export const Secrets: AppStory to src/browser/stories/App.settings.stories.tsx.
    • Extend createMockORPCClient (src/browser/stories/mocks/orpc.ts) to implement secrets.get/update (and seed global + project secrets).
  4. Rename settings tab “Projects” → “MCP”
    • id: "projects"id: "mcp".
    • Replace ProjectSettingsSection with MCPSettingsSection (global MCP config).
    • Use api.mcp.list({}) for global servers; api.mcp.add/remove/setEnabled/setToolAllowlist for writes; api.mcp.test({}) for global-only testing.
    • For MCP header secret dropdown/validation, use global secret keys (from api.secrets.get({})).
  5. Workspace modal remains workspace-scoped
    • List servers via api.mcp.list({ projectPath }) (effective global+repo servers).
    • Test via api.mcp.test({ projectPath, name }) (project cwd + effective secrets).
    • Continue applying workspace overrides via api.workspace.mcp.get/set.

6) Update system prompt + onboarding copy

Files:

  • src/node/services/systemMessage.ts
  • src/browser/components/splashScreens/OnboardingWizardSplash.tsx

Update text from:

  • “Configured in user’s local project .mux/mcp.jsonc

To:

  • “Configured globally in ~/.mux/mcp.jsonc.”
  • “Optional per-repo overrides in ./.mux/mcp.jsonc.”

Also update Settings pointer: “Settings → MCP”.

6b) CLI: keep mux run consistent with global MCP config

Files:

  • src/cli/run.ts
  • src/cli/run.test.ts (if assertions depend on help text)

Changes:

  • Update --no-mcp-config help text to mean: “ignore ~/.mux/mcp.jsonc (and repo overrides), use only --mcp servers”.
  • Ensure CLI uses the real mux home for global MCP servers:
    • Instantiate MCPConfigService with the user’s real Config() (not the ephemeral temp config).
    • Still pass --dir as projectPath into MCPServerManager so ./.mux/mcp.jsonc overrides apply.

MCP OAuth: make store non-project-scoped + migrate schema

7) OAuth store schema V2 (global by server URL)

Files:

  • src/node/services/mcpOauthService.ts
  • src/common/types/mcpOauth.ts (update comments)
  • src/node/services/mcpOauthService.test.ts

Current store (V1):

// version:1
entries: Record<projectPath, Record<serverName, MCPOAuthStoredCredentials>>

New store (V2):

interface McpOauthStoreFileV2 {
  version: 2;
  /** serverUrlForStoreKey -> creds */
  entries: Record<string, MCPOAuthStoredCredentials>;
}

Migration strategy (on read, under file lock):

  • If file is V1, flatten all entries.
  • Use normalizeServerUrlForComparison(creds.serverUrl) as the V2 key.
  • If duplicates occur, keep the creds with the highest updatedAtMs.
  • Persist back to disk as V2 (atomic write, mode 0o600).

Pseudo:

function migrateV1ToV2(v1: McpOauthStoreFileV1): McpOauthStoreFileV2 {
  const entries: Record<string, MCPOAuthStoredCredentials> = {};
  for (const byServer of Object.values(v1.entries)) {
    for (const cred of Object.values(byServer)) {
      const key = normalizeServerUrlForComparison(cred.serverUrl);
      if (!key) continue;
      if (!entries[key] || cred.updatedAtMs > entries[key]!.updatedAtMs) {
        entries[key] = cred;
      }
    }
  }
  return { version: 2, entries };
}

8) Refactor McpOauthService APIs to remove project scoping

Files:

  • src/node/services/mcpOauthService.ts
  • src/node/services/mcpServerManager.ts
  • src/node/orpc/router.ts
  • src/common/orpc/schemas/api.ts
  • src/browser/components/Settings/sections/ProjectSettingsSection.tsx (moved into MCP settings)

Key decisions:

  • Store + lookups should be keyed by server URL, not (projectPath, serverName).
  • ORPC inputs should not require projectPath.

Concrete refactor:

  1. Change public methods:
    • getAuthStatus(projectPath, serverName)getAuthStatus({ serverUrl }) (or { serverName, serverUrl } for nicer UI)
    • logout(projectPath, serverName)logout({ serverUrl })
    • hasAuthTokens({ projectPath, serverName, serverUrl })hasAuthTokens({ serverUrl })
    • getAuthProviderForServer({ projectPath, serverName, serverUrl })getAuthProviderForServer({ serverName, serverUrl })
  2. Remove normalizeProjectPathKey usage and all store.entries[projectKey] logic.
  3. Keep the defensive URL normalization (normalizeServerUrlForComparison) as-is.

9) ORPC schema changes for OAuth

Move OAuth endpoints out of projects.* to avoid implying project scoping.

Proposed new ORPC section: mcpOauth (top-level)

  • mcpOauth.startDesktopFlow({ serverName, pendingServer? })
  • mcpOauth.startServerFlow({ serverName, pendingServer? })
  • mcpOauth.getAuthStatus({ serverUrl })
  • mcpOauth.logout({ serverUrl })

Where pendingServer is still:

{ transport: "http"|"sse"|"auto"; url: string }

10) UI: OAuth actions live in global MCP Settings

  • Move the OAuth UI currently embedded in ProjectSettingsSection into the new MCPSettingsSection.
  • Since OAuth is now global and keyed by URL, logging in/out from MCP Settings affects all projects using that same server URL.

Validation / Tests

11) Update + add tests

MCP config:

  • src/node/services/mcpConfigService.test.ts
    • Update to use a temp Config(rootDir) and assert writes go to <rootDir>/mcp.jsonc.
    • Add coverage for merging global + repo override by name.

Secrets:

  • Add unit tests around Config.getEffectiveSecrets() merge behavior.
  • Add Chromatic coverage:
    • src/browser/stories/App.settings.stories.tsx → new Secrets story that opens Settings → Secrets with seeded global + project secrets.

OAuth:

  • src/node/services/mcpOauthService.test.ts
    • Update expectations to store V2 schema.
    • Add explicit migration test: write V1 store with multiple project entries → service reads → file rewritten as V2.

12) Manual smoke checklist

  • Configure an MCP server in Settings → MCP; verify it is written to ~/.mux/mcp.jsonc and appears in multiple projects.
  • Add ./.mux/mcp.jsonc in a repo overriding a global server by name; verify effective server uses override.
  • Confirm workspace overrides still work (.mux/mcp.local.jsonc).
  • Settings → Secrets: add a global secret and reference it via {secret:"KEY"} in global MCP headers; verify it resolves. Add a project secret with the same key and verify it overrides global when testing via mcp.test({ projectPath }).
  • OAuth: login to a remote MCP server once; verify OAuth status is “logged in” anywhere that server URL is used.
  • Verify ~/.mux/mcp-oauth.json is migrated to V2 (version bumped) on first run.

Generated with mux • Model: openai:gpt-5.2 • Thinking: xhigh • Cost: $31.92

- Read global MCP config from <muxHome>/mcp.jsonc with repo overrides
- Add global secrets support + effective secret merge (project overrides)
- Add top-level oRPC endpoints: mcp.* and secrets.* (legacy projects.* kept)
- CLI uses real mux home for MCP config; --no-mcp-config ignores global+repo
- Add/adjust unit tests for MCP config + secrets
- Add v2 mcp-oauth.json store keyed by normalized server URL\n- Migrate legacy v1 project-scoped store on read (dedupe by updatedAtMs)\n- Update MCPServerManager + ORPC legacy handlers to use serverUrl-scoped APIs\n- Update tests incl. v1→v2 migration coverage
Expose MCP OAuth flows + auth status at the top-level ORPC router (mcpOauth.*), while keeping projects.mcpOauth.* as legacy wrappers.
@ThomasK33
Copy link
Member Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation enhancement New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant