feat(mcp): scoped server config, project approval, and keychain storage - #44803
feat(mcp): scoped server config, project approval, and keychain storage#44803savagelysubtle wants to merge 13 commits into
Conversation
|
The following comment was made by an LLM, it may be inaccurate: Based on my search results, I found one potentially related PR: Related PR Found:
The current PR (#44803) is significantly more comprehensive, introducing the full scoped server configuration system with keychain storage, but #40125 may have addressed some foundational trust/approval concepts for MCP servers. However, #40125 is an older PR and addresses only the trust configuration aspect, while #44803 is a major upgrade covering scoped configuration (local/project/user), vendor shorthand expansion, project approval with hashing, and keychain storage—these are largely complementary or superseding features rather than duplicates. No other open duplicate PRs directly addressing this scope of work were found. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
… file writeJson created the target with umask permissions before chmodding, briefly exposing secrets group/world-readable on multi-user systems.
Project-level configuration can now live in .agents/, discovered from cwd up to the nearest git root plus ~/.agents for user-wide config. The directory supports opencode.json(c) for general overrides and a dedicated mcp.json holding MCP server definitions without the outer mcp wrapper. .agents sources merge after root-level files so they win on conflicts; root-level opencode.json continues to load as legacy.
MCP snippets copied from vendor docs (Claude Desktop, VS Code, Gemini CLI) now work without hand-translating: bare url/httpUrl entries become remote servers, and command string + args + env entries become local servers with args appended to command and env mapped to environment. Explicit type fields still win. .agents/mcp.json also accepts mcpServers/servers wrappers.
- Listen on the OAuth callback port first and treat EADDRINUSE as another opencode instance owning it, removing the check-then-listen race and the silent five-minute hang when a foreign app squats the default port. The timeout error now points users at the callbackPort option. - Route mode-restricted writeJson calls through a pre-chmoded temp file so MCP OAuth credentials never briefly exist with umask-default permissions.
The MCP dialog only offered enable/disable, leaving servers stuck in needs_auth with no in-TUI path through the OAuth flow. Add an authenticate action that starts the OAuth flow (browser opens, callback awaited) and refreshes status on completion, and surface a warning footer for needs_auth servers.
Dialog actions could not be triggered from the footer when bound to return because dialog.select.submit matched first. Order action bindings before the generic select bindings so dialog-specific actions win, and bind MCP authenticate to enter (with a as mnemonic).
Add Claude Code-style MCP scopes. opencode mcp add now accepts --scope local|project|user: local (default) stores the server in ~/.local/share/opencode/mcp-local.json keyed by project root so credentials never live in the repository; project writes .agents/mcp.json; user writes ~/.agents/mcp.json. Local entries replace same-named project/user definitions wholesale. mcp list shows each server's scope. Non-git projects key local scope by working directory since worktree is / there.
Project-owned MCP definitions (project opencode.json and .agents/mcp.json) are hashed at load time and will not connect until explicitly approved with 'opencode mcp approve <name>'. Choices are stored in the data directory keyed by project root, never in the repository, so a cloned repo cannot approve its own servers. Editing a definition invalidates its approval. Local-scope entries are user-private and exempt. Adds a needs_approval status surfaced in the MCP dialog and mcp list.
MCP OAuth tokens and client secrets now go to the OS keychain (macOS security, Linux secret-service via secret-tool) when available, matching Gemini CLI's hybrid approach. The auth file keeps only non-secret metadata as an enumeration index; legacy plaintext entries remain readable and migrate into the keychain on their next save. If the keychain write fails, credentials stay in the 0600 file rather than being lost. OPENCODE_MCP_FORCE_FILE_STORAGE=1 forces file storage.
…e metadata secret-tool lookups returned empty because stdio ignore disabled stdout for input-less commands, so keychain reads never saw stored secrets. Keychain hits now layer secret fields over the file entry instead of replacing it, preserving serverUrl, codeVerifier, and oauthState.
The same project reached through a symlinked path produced a different approval/local-scope key than the realpath, so approvals granted from one launch path never matched sessions launched from the other (Cursor workspaces open the symlink form). Canonicalize with realpath before keying approvals, local-scope storage, and approve/revoke.
One project can be reachable through several equally-real paths (symlinks, bind mounts) and realpath cannot unify bind mounts, so approval and local-scope keys derived from the raw worktree string made servers demand re-approval (or vanish) depending on launch path. Key approvals, local-scope storage, and scope detection on the .git directory's device+inode instead — identical across every path variant for a git repo, falling back to the path string for non-git directories.
9c616d9 to
c4bbbba
Compare
|
Two follow-up fixes pushed (rebased onto current dev):
Also verified the vendor-shorthand + |
Issue for this PR
Closes #
Type of change
What does this PR do?
Upgrades MCP server management, taking cues from Claude Code's config scopes and Gemini CLI's credential storage.
Main changes:
.agents/project directory — discovered from cwd to the git root (plus~/.agents), alongside.opencode. Holdsopencode.json(c)for overrides and a dedicatedmcp.jsonwith the bare server map.mcpServers/serverswrappers are accepted too.url/httpUrlbecomes a remote server,command+args+envbecomes a local server. Explicittypestill wins.opencode mcp add --scope local|project|user. Local (default) stores per-project servers in~/.local/share/opencode/mcp-local.jsonkeyed by project root, so credentials stay out of repositories. Local entries replace same-named project/user definitions wholesale.mcp listshows each server's scope.opencode mcp approve <name>. Choices live in the data directory keyed by project root, never in the repo, so a cloned repo can't approve its own servers. Editing a definition re-requires approval. Newneeds_approvalstatus shown in the TUI dialog andmcp list.security/secret-tool) when available; the auth file keeps only non-secret metadata as an index. Legacy plaintext entries still load and migrate on next save. Failed keychain writes fall back to the 0600 file instead of losing credentials.OPENCODE_MCP_FORCE_FILE_STORAGE=1forces the file.EADDRINUSEas another instance owning the port (fixes a check-then-listen race and a silent 5-minute hang when another app holds the port); mode-restrictedwriteJsonwrites through a pre-chmoded temp file so secrets never briefly exist with default permissions.How did you verify your code works?
.agentsdiscovery, wrapper unwrapping, shorthand expansion, local-scope precedence (including the non-gitworktree === "/"keying case), project-scope hashingserverUrl/oauthState/codeVerifier)Screenshots / recordings
TUI change: the MCP dialog now shows ⚠ Auth required / ⚠ Approval required footers and an authenticate action (enter or a). Happy to attach a recording if useful.
Checklist