Skip to content

Commit e38f96f

Browse files
kulvirgitclaude
andcommitted
feat: auto-discover MCP servers from VS Code, Claude Code, Copilot, and Gemini configs
Users who configure MCP servers in other AI tools no longer need to manually duplicate that config. At startup, altimate-code now reads .vscode/mcp.json, .github/copilot/mcp.json, .mcp.json, and .gemini/settings.json, transforming their entries into native config format at lowest priority (user config always wins). Adds /discover-and-add-mcps command to permanently write discovered servers into project or global config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2988e41 commit e38f96f

4 files changed

Lines changed: 516 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
description: "Discover MCP servers from external AI tool configs and add them permanently"
3+
---
4+
5+
Discover MCP servers configured in other AI tools (VS Code, GitHub Copilot, Claude Code, Gemini CLI) and offer to add them to the altimate-code config.
6+
7+
## Instructions
8+
9+
1. Look for MCP server configurations in the current project directory from these sources:
10+
- `.vscode/mcp.json` (key: `servers`) — VS Code / Copilot
11+
- `.github/copilot/mcp.json` (key: `mcpServers`) — GitHub Copilot
12+
- `.mcp.json` (key: `mcpServers`) — Claude Code
13+
- `.gemini/settings.json` (key: `mcpServers`) — Gemini CLI
14+
15+
2. For each file found, parse it and extract server definitions. Transform them to altimate-code format:
16+
- Local servers: `{ command, args?, env? }``{ type: "local", command: [command, ...args], environment: env }`
17+
- Remote servers: `{ url }``{ type: "remote", url, headers }`
18+
19+
3. Show the user what was discovered:
20+
- Server name
21+
- Source file it came from
22+
- Command or URL
23+
24+
4. Ask the user which servers they want to add permanently.
25+
26+
5. Write selected servers to the project config at `.altimate-code/altimate-code.json` using the `mcp` key.
27+
- If $ARGUMENTS contains `--scope global`, write to the global config at `~/.config/opencode/opencode.json` instead.
28+
- If $ARGUMENTS contains `--scope project`, write to `.altimate-code/altimate-code.json` (default).
29+
30+
6. Skip any servers that are already configured in the target config file.
31+
32+
7. Report what was added and where.

packages/opencode/src/config/config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,21 @@ export namespace Config {
127127
}
128128
}
129129

130+
// Discover MCP servers from external AI tool configs (VS Code, Claude Code, Gemini).
131+
// Lowest priority — user's explicit mcp config always wins.
132+
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
133+
const { discoverExternalMcp } = await import("../mcp/discover")
134+
const externalMcp = await discoverExternalMcp(Instance.worktree)
135+
if (Object.keys(externalMcp).length > 0) {
136+
result.mcp ??= {}
137+
for (const [name, server] of Object.entries(externalMcp)) {
138+
if (!(name in result.mcp)) {
139+
result.mcp[name] = server
140+
}
141+
}
142+
}
143+
}
144+
130145
result.agent = result.agent || {}
131146
result.mode = result.mode || {}
132147
result.plugin = result.plugin || []
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import path from "path"
2+
import { parse as parseJsonc } from "jsonc-parser"
3+
import { Log } from "../util/log"
4+
import { Filesystem } from "../util/filesystem"
5+
import type { Config } from "../config/config"
6+
7+
const log = Log.create({ service: "mcp.discover" })
8+
9+
interface ExternalMcpSource {
10+
/** Relative path from worktree root */
11+
file: string
12+
/** Key in the parsed JSON that holds the server map */
13+
key: string
14+
}
15+
16+
const SOURCES: ExternalMcpSource[] = [
17+
{ file: ".vscode/mcp.json", key: "servers" },
18+
{ file: ".github/copilot/mcp.json", key: "mcpServers" },
19+
{ file: ".mcp.json", key: "mcpServers" },
20+
{ file: ".gemini/settings.json", key: "mcpServers" },
21+
]
22+
23+
/**
24+
* Transform a single external MCP entry into our Config.Mcp shape.
25+
* Returns undefined if the entry is invalid (no command or url).
26+
*/
27+
function transform(entry: Record<string, any>): Config.Mcp | undefined {
28+
// Remote server
29+
if (entry.url && typeof entry.url === "string") {
30+
const result: Record<string, any> = {
31+
type: "remote" as const,
32+
url: entry.url,
33+
}
34+
if (entry.headers && typeof entry.headers === "object") {
35+
result.headers = entry.headers
36+
}
37+
return result as Config.Mcp
38+
}
39+
40+
// Local server
41+
if (entry.command) {
42+
const cmd = Array.isArray(entry.command)
43+
? entry.command.map(String)
44+
: [String(entry.command), ...(Array.isArray(entry.args) ? entry.args.map(String) : [])]
45+
46+
const result: Record<string, any> = {
47+
type: "local" as const,
48+
command: cmd,
49+
}
50+
if (entry.env && typeof entry.env === "object") {
51+
result.environment = entry.env
52+
}
53+
return result as Config.Mcp
54+
}
55+
56+
return undefined
57+
}
58+
59+
/**
60+
* Discover MCP servers configured in external AI tool configs
61+
* (VS Code, GitHub Copilot, Claude Code, Gemini CLI).
62+
*
63+
* Returns a Record where keys are server names and values are Config.Mcp objects.
64+
* First-discovered-wins per server name across sources.
65+
*/
66+
export async function discoverExternalMcp(worktree: string): Promise<Record<string, Config.Mcp>> {
67+
const result: Record<string, Config.Mcp> = {}
68+
const contributingSources: string[] = []
69+
70+
for (const source of SOURCES) {
71+
const filePath = path.join(worktree, source.file)
72+
73+
let text: string
74+
try {
75+
text = await Filesystem.readText(filePath)
76+
} catch {
77+
// File doesn't exist or can't be read — skip
78+
continue
79+
}
80+
81+
let parsed: any
82+
try {
83+
// Use JSONC parser to handle comments (common in VS Code configs)
84+
parsed = parseJsonc(text)
85+
} catch {
86+
log.debug("failed to parse external MCP config", { file: source.file })
87+
continue
88+
}
89+
90+
if (!parsed || typeof parsed !== "object") continue
91+
92+
const servers = parsed[source.key]
93+
if (!servers || typeof servers !== "object") continue
94+
95+
let added = 0
96+
for (const [name, entry] of Object.entries(servers)) {
97+
if (name in result) continue // first source wins
98+
if (!entry || typeof entry !== "object") continue
99+
100+
const transformed = transform(entry as Record<string, any>)
101+
if (transformed) {
102+
result[name] = transformed
103+
added++
104+
}
105+
}
106+
107+
if (added > 0) {
108+
contributingSources.push(source.file)
109+
}
110+
}
111+
112+
if (contributingSources.length > 0) {
113+
const count = Object.keys(result).length
114+
log.info(`Discovered ${count} MCP server(s) from ${contributingSources.join(", ")}. Run /discover-and-add-mcps to make them permanent.`)
115+
}
116+
117+
return result
118+
}

0 commit comments

Comments
 (0)