-
Notifications
You must be signed in to change notification settings - Fork 16
feat: auto-discover MCP servers from external AI tool configs #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ __pycache__ | |
| .idea | ||
| .vscode | ||
| .codex | ||
| .claude | ||
| *~ | ||
| playground | ||
| tmp | ||
|
|
||
| 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"`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,4 +15,7 @@ | |
| "github-triage": false, | ||
| "github-pr-search": false, | ||
| }, | ||
| "experimental": { | ||
| "auto_mcp_discovery": false, | ||
| }, | ||
| } | ||
| 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", | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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.", | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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"), | ||
| } | ||
| }, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added security warning in docs and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 ( A middle ground that would narrow the risk without adding a full consent UI:
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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
|
||
| } | ||
| } | ||
| // altimate_change end | ||
|
|
||
| return { | ||
| config: result, | ||
| directories, | ||
|
|
@@ -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(), | ||
| }) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.