Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__
.idea
.vscode
.codex
.claude
*~
playground
tmp
Expand Down
19 changes: 19 additions & 0 deletions .opencode/command/discover-and-add-mcps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
description: "Discover MCP servers from external AI tool configs and add them permanently"
---

Discover MCP servers configured in other AI tools (VS Code, Cursor, GitHub Copilot, Claude Code, Gemini CLI) and add them to the altimate-code config.

## Instructions

1. First, call the `mcp_discover` tool with `action: "list"` to see what's available.

2. Show the user the results — which servers are new and which are already configured.

3. If there are new servers, ask the user which ones they want to add and what scope (project or global).

4. Call `mcp_discover` with `action: "add"`, the chosen `scope`, and the selected `servers` array.

5. Report what was added and where.

If $ARGUMENTS contains `--scope global`, use `scope: "global"`. Otherwise default to `scope: "project"`.
3 changes: 3 additions & 0 deletions .opencode/opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
"github-triage": false,
"github-pr-search": false,
},
"experimental": {
"auto_mcp_discovery": false,
},
}
38 changes: 38 additions & 0 deletions docs/docs/configure/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,41 @@ When [LSP servers](lsp.md) are configured, the `lsp` tool provides:
- Go-to-definition
- Hover information
- Completions

### MCP Discover Tool

The `mcp_discover` tool finds MCP servers configured in other AI coding tools and can add them to your altimate-code config permanently.

**Supported sources:**

| Tool | Config Path | Key |
|------|------------|-----|
| VS Code / Copilot | `.vscode/mcp.json` | `servers` |
| Cursor | `.cursor/mcp.json` | `mcpServers` |
| GitHub Copilot | `.github/copilot/mcp.json` | `mcpServers` |
| Claude Code | `.mcp.json`, `~/.claude.json` | `mcpServers` |
| Gemini CLI | `.gemini/settings.json` | `mcpServers` |

**Actions:**

- `mcp_discover(action: "list")` — Show discovered servers and which are already in your config
- `mcp_discover(action: "add", scope: "project")` — Write new servers to `.altimate-code/altimate-code.json`
- `mcp_discover(action: "add", scope: "global")` — Write to the global config dir (`~/.config/opencode/`)

**Auto-discovery:** At startup, altimate-code discovers external MCP servers and shows a toast notification. Servers from your home directory (`~/.claude.json`, `~/.gemini/settings.json`) are auto-enabled since they're user-owned. Servers from project-level files (`.vscode/mcp.json`, `.mcp.json`, `.cursor/mcp.json`) are discovered but **disabled by default** for security — run `/discover-and-add-mcps` to review and enable them.

!!! tip
Home-directory MCP servers (from `~/.claude.json`, `~/.gemini/settings.json`) are loaded automatically. Project-scoped servers require explicit approval via `/discover-and-add-mcps` or `mcp_discover(action: "add")`.

!!! warning "Security: untrusted repositories"
Project-level MCP configs (`.vscode/mcp.json`, `.mcp.json`, `.cursor/mcp.json`) are discovered but not auto-connected. This prevents malicious repositories from executing arbitrary commands. You must explicitly approve project-scoped servers before they run.

To disable auto-discovery, set in your config:

```json
{
"experimental": {
"auto_mcp_discovery": false
}
}
```
8 changes: 8 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ export namespace Telemetry {
tool_count: number
resource_count: number
}
| {
type: "mcp_discovery"
timestamp: number
session_id: string
server_count: number
server_names: string[]
sources: string[]
}
| {
type: "memory_operation"
timestamp: number
Expand Down
120 changes: 120 additions & 0 deletions packages/opencode/src/altimate/tools/mcp-discover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import z from "zod"
import { Tool } from "../../tool/tool"
import { discoverExternalMcp } from "../../mcp/discover"
import { resolveConfigPath, addMcpToConfig, findAllConfigPaths, listMcpInConfig } from "../../mcp/config"
import { Instance } from "../../project/instance"
import { Global } from "../../global"

/**
* Check which MCP server names are permanently configured on disk
* (as opposed to ephemeral auto-discovered servers in memory).
*/
async function getPersistedMcpNames(): Promise<Set<string>> {
const configPaths = await findAllConfigPaths(Instance.worktree, Global.Path.config)
const names = new Set<string>()
for (const p of configPaths) {
for (const name of await listMcpInConfig(p)) {
names.add(name)
}
}
return names
}

/** Redact server details for safe display — show type and name only, not commands/URLs */
function safeDetail(server: { type: string } & Record<string, any>): string {
if (server.type === "remote") return "(remote)"
if (server.type === "local" && Array.isArray(server.command) && server.command.length > 0) {
// Show only the executable name, not args (which may contain credentials)
return `(local: ${server.command[0]})`
}
return `(${server.type})`
}

export const McpDiscoverTool = Tool.define("mcp_discover", {
description:
"Discover MCP servers from external AI tool configs (VS Code, Cursor, Claude Code, Copilot, Gemini) and optionally add them to altimate-code config permanently.",
parameters: z.object({
action: z
.enum(["list", "add"])
.describe('"list" to show discovered servers, "add" to write them to config'),
scope: z
.enum(["project", "global"])
.optional()
.default("project")
.describe('Where to write when action is "add". "project" = .altimate-code/altimate-code.json, "global" = ~/.config/opencode/'),
servers: z
.array(z.string())
.optional()
.describe('Server names to add. If omitted with action "add", adds all new servers.'),
}),
async execute(args, ctx) {
const { servers: discovered } = await discoverExternalMcp(Instance.worktree)
const discoveredNames = Object.keys(discovered)

if (discoveredNames.length === 0) {
return {
title: "MCP Discover: none found",
metadata: { discovered: 0, new: 0, existing: 0, added: 0 },
output:
"No MCP servers found in external configs.\nChecked: .vscode/mcp.json, .cursor/mcp.json, .github/copilot/mcp.json, .mcp.json (project + home), .gemini/settings.json (project + home), ~/.claude.json",
}
}

// Check what's actually persisted on disk, NOT the merged in-memory config
const persistedNames = await getPersistedMcpNames()
const newServers = discoveredNames.filter((n) => !persistedNames.has(n))
const alreadyAdded = discoveredNames.filter((n) => persistedNames.has(n))

// Build discovery report — redact details for security (no raw commands/URLs)
const lines: string[] = []
if (newServers.length > 0) {
lines.push(`New servers (not yet in config):`)
for (const name of newServers) {
lines.push(` - ${name} ${safeDetail(discovered[name])}`)
}
}
if (alreadyAdded.length > 0) {
lines.push(`\nAlready in config: ${alreadyAdded.join(", ")}`)
}

if (args.action === "list") {
return {
title: `MCP Discover: ${newServers.length} new, ${alreadyAdded.length} existing`,
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: 0 },
output: lines.join("\n"),
}
}

// action === "add"
const toAdd = args.servers
? args.servers.filter((n) => newServers.includes(n))
: newServers

if (toAdd.length === 0) {
return {
title: "MCP Discover: nothing to add",
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: 0 },
output: lines.join("\n") + "\n\nNo matching servers to add.",
}
}

const useGlobal = args.scope === "global"
const configPath = await resolveConfigPath(
useGlobal ? Global.Path.config : Instance.worktree,
useGlobal,
)

for (const name of toAdd) {
await addMcpToConfig(name, discovered[name], configPath)
}

lines.push(`\nAdded ${toAdd.length} server(s) to ${configPath}: ${toAdd.join(", ")}`)
lines.push("These servers are already active in the current session via auto-discovery.")

return {
title: `MCP Discover: added ${toAdd.length} server(s)`,
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: toAdd.length },
output: lines.join("\n"),
}
},
})
24 changes: 24 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,24 @@ export namespace Config {

result.plugin = deduplicatePlugins(result.plugin ?? [])

// altimate_change start — auto-discover MCP servers from external AI tool configs
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG && result.experimental?.auto_mcp_discovery !== false) {
const { discoverExternalMcp, setDiscoveryResult } = await import("../mcp/discover")
const { servers: externalMcp, sources } = await discoverExternalMcp(Instance.worktree)
if (Object.keys(externalMcp).length > 0) {
result.mcp ??= {}
const added: string[] = []
for (const [name, server] of Object.entries(externalMcp)) {
if (!(name in result.mcp)) {
result.mcp[name] = server
added.push(name)
}
}
setDiscoveryResult(added, sources)
Comment on lines +267 to +280
Copy link

@coderabbitai coderabbitai bot Mar 20, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Project MCP auto-discovery is a repo-open code-execution path.

This merges repo-owned MCP definitions into the live config before startup completes. Since packages/opencode/src/mcp/discover.ts turns local MCP entries into executable commands, opening an untrusted repo with .vscode/mcp.json or .mcp.json can start arbitrary local processes without an approval step. Please gate project-scope discovery behind repo trust / explicit consent, or keep discovered project servers disabled until approved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/config/config.ts` around lines 267 - 280, The
auto-discovery flow currently merges external MCP servers into the live config
(see discoverExternalMcp, setDiscoveryResult and result.mcp) during startup;
change it so discovered project-scoped servers are not enabled automatically:
either require a repo-trust/consent check (e.g. gate behind a trust flag or call
to an Instance.isTrustedRepo/confirmApproval) before merging into result.mcp, or
instead store discovered entries in a separate pending/discovered bucket (e.g.
result.experimental.discoveredMcp) and do not copy them into result.mcp until an
explicit approve action runs; ensure the existing
Flag.OPENCODE_DISABLE_PROJECT_CONFIG and result.experimental?.auto_mcp_discovery
checks are extended to enforce this gating and update setDiscoveryResult usage
accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added security warning in docs and experimental.auto_mcp_discovery: false opt-out. This matches VS Code and Claude Code behavior — they also auto-load MCP servers from project files.

Copy link

Choose a reason for hiding this comment

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

@kulvirgit, thanks for adding the docs warning and the opt-out flag — those are helpful additions. However, the comparison to VS Code doesn't quite hold up on closer inspection.

VS Code prompts users to trust each MCP server before it runs, and re-prompts after configuration changes. This is a separate trust boundary from Workspace Trust — an untrusted workspace runs in restricted mode, which also disables agents. When you add an MCP server to your workspace or change its configuration, VS Code shows a dialog to confirm that you trust the server when you start a server for the first time. If you don't trust the MCP server, it will not be started.

So VS Code's model is actually: opt-in via explicit per-server consent dialog, not opt-out via a config flag. The current implementation is closer to the reverse — silently merging and executing project-owned MCP commands at startup, with a global kill-switch to turn the feature off entirely.

The remaining concern is specifically the project-scope discovery path (.vscode/mcp.json, .mcp.json). A developer cloning an untrusted repository will have those servers executed automatically unless they know to set experimental.auto_mcp_discovery: false in advance — which requires pre-existing knowledge of the feature.

A middle ground that would narrow the risk without adding a full consent UI:

  • Keep global/home-directory sources (user's own ~/.claude.json, ~/.gemini/settings.json) auto-applied as-is, since those are user-owned files.
  • For project-scope sources (.vscode/mcp.json, .mcp.json, .github/copilot/mcp.json inside the worktree), treat discovered servers as disabled-by-default (enabled: false) so they're visible via /discover-and-add-mcps but don't spawn processes until explicitly approved.

This would still surface the configurations for easy adoption while preventing automatic code execution from repo-committed files. Would that tradeoff work for your use case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fair point on VS Code's trust prompt. We match Claude Code's behavior (auto-loads .mcp.json without prompting). The auto_mcp_discovery: false flag is the opt-out. A trust prompt is a reasonable follow-up but out of scope for this PR — tracking as future work.

Copy link

Choose a reason for hiding this comment

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

@kulvirgit, understood — the Claude Code precedent is a reasonable baseline, and the auto_mcp_discovery: false flag provides a way for security-conscious users to opt out. I'll drop the blocker.

Would you like me to open a GitHub issue to track the future trust-prompt work? I can include the relevant context (per-server consent dialog, project-scope vs. global-scope distinction) so it's easy to pick up later.

🐰 noted and hopping along


✏️ Learnings added
Learnt from: kulvirgit
Repo: AltimateAI/altimate-code PR: 311
File: packages/opencode/src/config/config.ts:267-280
Timestamp: 2026-03-20T18:08:52.042Z
Learning: In `packages/opencode/src/config/config.ts`, the `auto_mcp_discovery` feature intentionally auto-loads MCP servers from project-scope config files (`.vscode/mcp.json`, `.mcp.json`, `.github/copilot/mcp.json`, `.gemini/settings.json`) at startup without a per-server trust prompt, matching Claude Code's behavior. The `experimental.auto_mcp_discovery: false` flag is the designated opt-out. A future trust/consent prompt for project-scoped MCP servers is tracked as follow-up work, not part of this PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
}
// altimate_change end

return {
config: result,
directories,
Expand Down Expand Up @@ -1273,6 +1291,12 @@ export namespace Config {
.optional()
.describe("Use environment fingerprint to select relevant skills once per session (default: false). Set to true to enable LLM-based skill filtering."),
// altimate_change end
// altimate_change start - auto MCP discovery toggle
auto_mcp_discovery: z
.boolean()
.optional()
.describe("Auto-discover MCP servers from VS Code, Claude Code, Copilot, and Gemini configs at startup (default: true). Set to false to disable."),
// altimate_change end
})
.optional(),
})
Expand Down
Loading
Loading