Skip to content

Commit ad701e4

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 ad701e4

11 files changed

Lines changed: 931 additions & 0 deletions

File tree

.claude/commands/pr-ce.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
name: pr-ce
3+
description: "Format, verify markers, update docs, commit, and create PR for altimate-code"
4+
---
5+
6+
You are preparing altimate-code for a pull request. This project is a fork of opencode — upstream-shared files need `altimate_change` markers and docs must be updated.
7+
8+
**CRITICAL: ALL steps are MANDATORY. You MUST complete every single step. DO NOT skip any step. DO NOT mark docs as "N/A" if you added tools, skills, agents, or changed config. DO NOT rationalize skipping with "can follow later" — that is a workflow violation. If you skip a step, you have failed the task.**
9+
10+
## Step 1: Run Tests
11+
12+
```bash
13+
cd /home/kulvir/altimate-code/packages/opencode && bun test 2>&1 | tail -5
14+
```
15+
16+
If tests fail, fix the issues before continuing.
17+
18+
## Step 2: Verify Upstream Markers
19+
20+
Run the marker guard to check upstream-shared files have `altimate_change` markers:
21+
22+
```bash
23+
cd /home/kulvir/altimate-code && bun run script/upstream/analyze.ts --markers --base main 2>&1
24+
```
25+
26+
If warnings appear for files YOU changed in this PR, wrap the custom code with markers:
27+
28+
```typescript
29+
// altimate_change start — description of what we changed
30+
... our modifications ...
31+
// altimate_change end
32+
```
33+
34+
Files in `packages/opencode/src/altimate/` are fully ours (in `keepOurs`) — no markers needed.
35+
Files in `packages/opencode/src/` that exist upstream need markers.
36+
New files (not in upstream) don't need markers.
37+
38+
## Step 3: Update Docs
39+
40+
Check if your changes affect any documented behavior:
41+
42+
```bash
43+
git diff main...HEAD --name-only | head -30
44+
```
45+
46+
If changes touch:
47+
- **Agent/mode definitions** → update `docs/docs/configure/agents.md`
48+
- **Permission rules** → update `docs/docs/configure/permissions.md`
49+
- **Config schema** → update `docs/docs/configure/config.md`
50+
- **Skills** → update `docs/docs/configure/skills.md`
51+
- **Tools** → update `docs/docs/configure/tools.md`
52+
- **Tracing** → update `docs/docs/configure/tracing.md`
53+
54+
Read the relevant doc file and ensure it matches the new behavior.
55+
56+
**YOU MUST UPDATE DOCS if you added or changed any tool, skill, agent, permission, or config option. "N/A" is ONLY acceptable if the change is purely internal with zero user-facing impact. When in doubt, update the docs.**
57+
58+
## Step 4: Stage and Commit
59+
60+
```bash
61+
cd /home/kulvir/altimate-code && git add -A
62+
```
63+
64+
Check if branch has existing commits ahead of main:
65+
66+
```bash
67+
git log origin/main..HEAD --oneline
68+
```
69+
70+
- **No commits:** Create new commit with a clear message based on changes.
71+
- **Commits exist:** Amend the existing commit: `git commit --amend --no-edit`
72+
73+
## Step 5: Push
74+
75+
```bash
76+
git push --force-with-lease origin HEAD
77+
```
78+
79+
## Step 6: Create Pull Request
80+
81+
Check if PR already exists:
82+
83+
```bash
84+
gh pr view --json url -q .url 2>/dev/null
85+
```
86+
87+
**If PR exists:** Report the URL.
88+
89+
**If no PR exists:** Create one:
90+
91+
```bash
92+
gh pr create --title "COMMIT_TITLE_HERE" --body "$(cat <<'EOF'
93+
## Summary
94+
- Brief description of changes
95+
96+
## Checklist
97+
- [x] Tests pass
98+
- [x] TypeScript compiles
99+
- [x] Upstream markers verified
100+
- [x] Docs updated (if applicable)
101+
102+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
103+
EOF
104+
)"
105+
```
106+
107+
## Step 7: Confirm
108+
109+
Report:
110+
- Commit message
111+
- PR URL
112+
- Marker status (clean / warnings)
113+
- Docs updated (which files, or "N/A")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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, Cursor, GitHub Copilot, Claude Code, Gemini CLI) and add them to the altimate-code config.
6+
7+
## Instructions
8+
9+
1. First, call the `mcp_discover` tool with `action: "list"` to see what's available.
10+
11+
2. Show the user the results — which servers are new and which are already configured.
12+
13+
3. If there are new servers, ask the user which ones they want to add and what scope (project or global).
14+
15+
4. Call `mcp_discover` with `action: "add"`, the chosen `scope`, and the selected `servers` array.
16+
17+
5. Report what was added and where.
18+
19+
If $ARGUMENTS contains `--scope global`, use `scope: "global"`. Otherwise default to `scope: "project"`.

.opencode/opencode.jsonc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515
"github-triage": false,
1616
"github-pr-search": false,
1717
},
18+
"experimental": {
19+
"auto_mcp_discovery": false,
20+
},
1821
}

docs/docs/configure/tools.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,41 @@ When [LSP servers](lsp.md) are configured, the `lsp` tool provides:
110110
- Go-to-definition
111111
- Hover information
112112
- Completions
113+
114+
### MCP Discover Tool
115+
116+
The `mcp_discover` tool finds MCP servers configured in other AI coding tools and can add them to your altimate-code config permanently.
117+
118+
**Supported sources:**
119+
120+
| Tool | Config Path | Key |
121+
|------|------------|-----|
122+
| VS Code / Copilot | `.vscode/mcp.json` | `servers` |
123+
| Cursor | `.cursor/mcp.json` | `mcpServers` |
124+
| GitHub Copilot | `.github/copilot/mcp.json` | `mcpServers` |
125+
| Claude Code | `.mcp.json`, `~/.claude.json` | `mcpServers` |
126+
| Gemini CLI | `.gemini/settings.json` | `mcpServers` |
127+
128+
**Actions:**
129+
130+
- `mcp_discover(action: "list")` — Show discovered servers and which are already in your config
131+
- `mcp_discover(action: "add", scope: "project")` — Write new servers to `.altimate-code/altimate-code.json`
132+
- `mcp_discover(action: "add", scope: "global")` — Write to `~/.config/opencode/opencode.json`
133+
134+
**Auto-discovery:** At startup, altimate-code automatically discovers external MCP servers and loads them into the current session. A toast notification shows what was found. Use the `/discover-and-add-mcps` skill or the `mcp_discover` tool to make them permanent.
135+
136+
!!! tip
137+
Servers discovered from external configs are loaded automatically — no restart needed. Making them permanent via `mcp_discover(action: "add")` ensures they persist even if the original config file is removed.
138+
139+
!!! warning "Security: untrusted repositories"
140+
Auto-discovery loads MCP servers from project-level files like `.vscode/mcp.json` and `.mcp.json`. A malicious repository could include configs that execute arbitrary commands. Review discovered servers before making them permanent, especially in cloned repositories you don't trust. Disable auto-discovery with `experimental.auto_mcp_discovery: false`.
141+
142+
To disable auto-discovery, set in your config:
143+
144+
```json
145+
{
146+
"experimental": {
147+
"auto_mcp_discovery": false
148+
}
149+
}
150+
```

packages/opencode/src/altimate/telemetry/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ export namespace Telemetry {
255255
tool_count: number
256256
resource_count: number
257257
}
258+
| {
259+
type: "mcp_discovery"
260+
timestamp: number
261+
session_id: string
262+
server_count: number
263+
server_names: string[]
264+
sources: string[]
265+
}
258266
| {
259267
type: "memory_operation"
260268
timestamp: number
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import z from "zod"
2+
import { Tool } from "../../tool/tool"
3+
import { discoverExternalMcp } from "../../mcp/discover"
4+
import { resolveConfigPath, addMcpToConfig, findAllConfigPaths, listMcpInConfig } from "../../mcp/config"
5+
import { Instance } from "../../project/instance"
6+
import { Global } from "../../global"
7+
8+
/**
9+
* Check which MCP server names are permanently configured on disk
10+
* (as opposed to ephemeral auto-discovered servers in memory).
11+
*/
12+
async function getPersistedMcpNames(): Promise<Set<string>> {
13+
const configPaths = await findAllConfigPaths(Instance.worktree, Global.Path.config)
14+
const names = new Set<string>()
15+
for (const p of configPaths) {
16+
for (const name of await listMcpInConfig(p)) {
17+
names.add(name)
18+
}
19+
}
20+
return names
21+
}
22+
23+
/** Redact server details for safe display — show type and name only, not commands/URLs */
24+
function safeDetail(server: { type: string } & Record<string, any>): string {
25+
if (server.type === "remote") return "(remote)"
26+
if (server.type === "local" && Array.isArray(server.command) && server.command.length > 0) {
27+
// Show only the executable name, not args (which may contain credentials)
28+
return `(local: ${server.command[0]})`
29+
}
30+
return `(${server.type})`
31+
}
32+
33+
export const McpDiscoverTool = Tool.define("mcp_discover", {
34+
description:
35+
"Discover MCP servers from external AI tool configs (VS Code, Claude Code, Copilot, Gemini) and optionally add them to altimate-code config permanently.",
36+
parameters: z.object({
37+
action: z
38+
.enum(["list", "add"])
39+
.describe('"list" to show discovered servers, "add" to write them to config'),
40+
scope: z
41+
.enum(["project", "global"])
42+
.optional()
43+
.default("project")
44+
.describe('Where to write when action is "add". "project" = .altimate-code/altimate-code.json, "global" = ~/.config/opencode/'),
45+
servers: z
46+
.array(z.string())
47+
.optional()
48+
.describe('Server names to add. If omitted with action "add", adds all new servers.'),
49+
}),
50+
async execute(args, ctx) {
51+
const { servers: discovered } = await discoverExternalMcp(Instance.worktree)
52+
const discoveredNames = Object.keys(discovered)
53+
54+
if (discoveredNames.length === 0) {
55+
return {
56+
title: "MCP Discover: none found",
57+
metadata: { discovered: 0, new: 0, existing: 0, added: 0 },
58+
output:
59+
"No MCP servers found in external configs.\nChecked: .vscode/mcp.json, .github/copilot/mcp.json, .mcp.json, ~/.gemini/settings.json, ~/.claude.json",
60+
}
61+
}
62+
63+
// Check what's actually persisted on disk, NOT the merged in-memory config
64+
const persistedNames = await getPersistedMcpNames()
65+
const newServers = discoveredNames.filter((n) => !persistedNames.has(n))
66+
const alreadyAdded = discoveredNames.filter((n) => persistedNames.has(n))
67+
68+
// Build discovery report — redact details for security (no raw commands/URLs)
69+
const lines: string[] = []
70+
if (newServers.length > 0) {
71+
lines.push(`New servers (not yet in config):`)
72+
for (const name of newServers) {
73+
lines.push(` - ${name} ${safeDetail(discovered[name])}`)
74+
}
75+
}
76+
if (alreadyAdded.length > 0) {
77+
lines.push(`\nAlready in config: ${alreadyAdded.join(", ")}`)
78+
}
79+
80+
if (args.action === "list") {
81+
return {
82+
title: `MCP Discover: ${newServers.length} new, ${alreadyAdded.length} existing`,
83+
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: 0 },
84+
output: lines.join("\n"),
85+
}
86+
}
87+
88+
// action === "add"
89+
const toAdd = args.servers
90+
? args.servers.filter((n) => newServers.includes(n))
91+
: newServers
92+
93+
if (toAdd.length === 0) {
94+
return {
95+
title: "MCP Discover: nothing to add",
96+
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: 0 },
97+
output: lines.join("\n") + "\n\nNo matching servers to add.",
98+
}
99+
}
100+
101+
const useGlobal = args.scope === "global"
102+
const configPath = await resolveConfigPath(
103+
useGlobal ? Global.Path.config : Instance.worktree,
104+
useGlobal,
105+
)
106+
107+
for (const name of toAdd) {
108+
await addMcpToConfig(name, discovered[name], configPath)
109+
}
110+
111+
lines.push(`\nAdded ${toAdd.length} server(s) to ${configPath}: ${toAdd.join(", ")}`)
112+
lines.push("These servers are already active in the current session via auto-discovery.")
113+
114+
return {
115+
title: `MCP Discover: added ${toAdd.length} server(s)`,
116+
metadata: { discovered: discoveredNames.length, new: newServers.length, existing: alreadyAdded.length, added: toAdd.length },
117+
output: lines.join("\n"),
118+
}
119+
},
120+
})

packages/opencode/src/config/config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,24 @@ export namespace Config {
264264

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

267+
// altimate_change start — auto-discover MCP servers from external AI tool configs
268+
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG && result.experimental?.auto_mcp_discovery !== false) {
269+
const { discoverExternalMcp, setDiscoveryResult } = await import("../mcp/discover")
270+
const { servers: externalMcp, sources } = await discoverExternalMcp(Instance.worktree)
271+
if (Object.keys(externalMcp).length > 0) {
272+
result.mcp ??= {}
273+
const added: string[] = []
274+
for (const [name, server] of Object.entries(externalMcp)) {
275+
if (!(name in result.mcp)) {
276+
result.mcp[name] = server
277+
added.push(name)
278+
}
279+
}
280+
setDiscoveryResult(added, sources)
281+
}
282+
}
283+
// altimate_change end
284+
267285
return {
268286
config: result,
269287
directories,
@@ -1273,6 +1291,12 @@ export namespace Config {
12731291
.optional()
12741292
.describe("Use environment fingerprint to select relevant skills once per session (default: false). Set to true to enable LLM-based skill filtering."),
12751293
// altimate_change end
1294+
// altimate_change start - auto MCP discovery toggle
1295+
auto_mcp_discovery: z
1296+
.boolean()
1297+
.optional()
1298+
.describe("Auto-discover MCP servers from VS Code, Claude Code, Copilot, and Gemini configs at startup (default: true). Set to false to disable."),
1299+
// altimate_change end
12761300
})
12771301
.optional(),
12781302
})

0 commit comments

Comments
 (0)