Skip to content

Commit 599807a

Browse files
author
saravmajestic
committed
refactor: replace hardcoded IDE paths with glob scan for mcp.json
Instead of checking .vscode/mcp.json, .cursor/mcp.json, and .github/copilot/mcp.json separately, scan all mcp.json files under the project root and use the first one that contains a datamate entry. This is IDE-agnostic and will work with any editor that writes an mcp.json. - Remove IDE_MCP_SOURCES constant - Add findAllMcpJsonFiles() using Glob.scan(**/mcp.json) - Add extractServersMap() helper to try both "servers" and "mcpServers" keys - Update readDatamateTransportFromIde() and syncDatamateUrlFromVscodeMcp() to use the new scan approach
1 parent 520143f commit 599807a

1 file changed

Lines changed: 89 additions & 69 deletions

File tree

packages/opencode/src/altimate/datamate-transport.ts

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { readFile } from "fs/promises"
22
import path from "path"
3-
import { existsSync } from "fs"
43
import { parseTree, findNodeAtLocation } from "jsonc-parser"
54
import { resolveConfigPath, addMcpToConfig } from "../mcp/config"
65
import { Filesystem } from "../util/filesystem"
6+
import { Glob } from "../util/glob"
77
import { Log } from "../util/log"
88
import type { Config } from "../config/config"
99

@@ -13,25 +13,61 @@ const log = Log.create({ service: "datamate-transport" })
1313
export const DATAMATE_KEY = "datamate"
1414
// altimate_change end
1515

16-
/** IDE config sources where the extension may write a "datamate" MCP entry. */
17-
const IDE_MCP_SOURCES = [
18-
// VS Code (1.99+: "servers", older: "mcpServers")
19-
{ file: ".vscode/mcp.json", keys: ["servers", "mcpServers"] },
20-
// Cursor
21-
{ file: ".cursor/mcp.json", keys: ["mcpServers", "servers"] },
22-
// GitHub Copilot
23-
{ file: ".github/copilot/mcp.json", keys: ["mcpServers", "servers"] },
24-
]
16+
/**
17+
* Top-level keys that MCP config files use to map server name → entry.
18+
* VS Code 1.99+ uses "servers"; older VS Code and Cursor use "mcpServers".
19+
* We try both so the scan works regardless of which IDE wrote the file.
20+
*/
21+
const MCP_SERVERS_KEYS = ["servers", "mcpServers"] as const
22+
23+
/** Glob patterns to exclude from mcp.json scans (large, irrelevant trees). */
24+
const MCP_SCAN_EXCLUDE = ["/node_modules/", "/.git/", "/dist/", "/build/", "/.pnpm/"]
2525

2626
export type DatamateTransport =
2727
| { type: "remote"; url: string }
2828
| { type: "local"; command: string[] }
2929

3030
/**
31-
* Scan across all IDE MCP config files in projectRootDir and return the
32-
* transport type for the "datamate" server entry.
31+
* Parse a single mcp.json file and return the servers map, trying each of the
32+
* known top-level key names in order.
33+
*/
34+
function extractServersMap(
35+
parsed: Record<string, unknown>,
36+
): Record<string, Record<string, unknown>> {
37+
for (const key of MCP_SERVERS_KEYS) {
38+
const candidate = parsed[key]
39+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
40+
return candidate as Record<string, Record<string, unknown>>
41+
}
42+
}
43+
return {}
44+
}
45+
46+
/**
47+
* Find all mcp.json files under projectRootDir (excluding noise directories)
48+
* and return the paths sorted for deterministic ordering.
49+
*/
50+
async function findAllMcpJsonFiles(projectRootDir: string): Promise<string[]> {
51+
try {
52+
const paths = await Glob.scan("**/mcp.json", {
53+
cwd: projectRootDir,
54+
absolute: true,
55+
dot: true,
56+
})
57+
return paths
58+
.filter((p) => !MCP_SCAN_EXCLUDE.some((ex) => p.includes(ex)))
59+
.sort()
60+
} catch {
61+
log.warn("findAllMcpJsonFiles: glob scan failed", { cwd: projectRootDir })
62+
return []
63+
}
64+
}
65+
66+
/**
67+
* Scan all mcp.json files under projectRootDir and return the transport type
68+
* for the first "datamate" server entry found.
3369
*
34-
* Returns null if no IDE config has a "datamate" entry — the caller should
70+
* Returns null if no mcp.json contains a "datamate" entry — the caller should
3571
* fall back to the cloud config.
3672
*
3773
* Reuses the exact command from the IDE config so altimate-code spawns the
@@ -40,37 +76,27 @@ export type DatamateTransport =
4076
export async function readDatamateTransportFromIde(
4177
projectRootDir: string,
4278
): Promise<DatamateTransport | null> {
43-
for (const source of IDE_MCP_SOURCES) {
44-
const mcpJsonPath = path.join(projectRootDir, source.file)
45-
if (!existsSync(mcpJsonPath)) continue
79+
const mcpJsonPaths = await findAllMcpJsonFiles(projectRootDir)
4680

81+
for (const mcpJsonPath of mcpJsonPaths) {
82+
const relPath = path.relative(projectRootDir, mcpJsonPath)
4783
try {
4884
const text = await readFile(mcpJsonPath, "utf-8")
4985
const parsed = JSON.parse(text) as Record<string, unknown>
50-
51-
let serversMap: Record<string, Record<string, unknown>> = {}
52-
for (const key of source.keys) {
53-
const candidate = parsed[key]
54-
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
55-
serversMap = candidate as Record<string, Record<string, unknown>>
56-
break
57-
}
58-
}
59-
86+
const serversMap = extractServersMap(parsed)
6087
const entry = serversMap[DATAMATE_KEY]
6188
if (!entry) continue
6289

6390
log.info("readDatamateTransportFromIde: found entry", {
64-
source: source.file,
91+
source: relPath,
6592
type: entry["type"] ?? "(no type)",
6693
})
6794

6895
if (typeof entry["url"] === "string") {
6996
return { type: "remote", url: entry["url"] }
7097
}
7198

72-
// stdio entry — extract command + args so we reuse the same process
73-
// the extension already manages, rather than spawning a second one.
99+
// stdio entry — reuse the exact command + args the extension registered
74100
const cmd = typeof entry["command"] === "string" ? entry["command"] : undefined
75101
const args = Array.isArray(entry["args"]) ? (entry["args"] as string[]) : []
76102
if (cmd) {
@@ -80,8 +106,7 @@ export async function readDatamateTransportFromIde(
80106
// Entry exists but has no usable command — treat as local marker
81107
return { type: "local", command: [DATAMATE_KEY, "start-stdio"] }
82108
} catch {
83-
log.warn("readDatamateTransportFromIde: failed to parse", { source: source.file })
84-
// File missing or unparseable — try next source
109+
log.warn("readDatamateTransportFromIde: failed to parse", { source: relPath })
85110
}
86111
}
87112

@@ -90,10 +115,11 @@ export async function readDatamateTransportFromIde(
90115
}
91116

92117
/**
93-
* Sync the "datamate" entry (and other remote MCP entries) from IDE MCP config
94-
* files to altimate-code.json. Uses `updatedAt` as the change signal for the
95-
* datamate entry (covers both stdio and HTTP transport), and URL comparison for
96-
* all other remote entries.
118+
* Sync the "datamate" entry (and other remote MCP entries) from the first
119+
* mcp.json that contains a "datamate" key to altimate-code.json.
120+
*
121+
* Uses `updatedAt` as the change signal for the datamate entry (covers both
122+
* stdio and HTTP transport), and URL comparison for all other remote entries.
97123
*
98124
* Fire-and-forget friendly: errors are logged but never thrown.
99125
* Returns the list of MCP server names whose config was updated on disk.
@@ -103,39 +129,34 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
103129
try {
104130
log.info("syncDatamateUrlFromVscodeMcp: start", { cwd })
105131

106-
// Try each IDE source in priority order; use the first one that exists.
132+
// Find the first mcp.json that contains a "datamate" entry.
133+
const mcpJsonPaths = await findAllMcpJsonFiles(cwd)
107134
let mcpJsonPath: string | undefined
108-
let ideSource: (typeof IDE_MCP_SOURCES)[number] | undefined
109-
for (const source of IDE_MCP_SOURCES) {
110-
const candidate = path.join(cwd, source.file)
111-
if (existsSync(candidate)) {
112-
mcpJsonPath = candidate
113-
ideSource = source
114-
break
115-
}
116-
}
135+
let serversMap: Record<string, Record<string, unknown>> = {}
117136

118-
if (!mcpJsonPath || !ideSource) {
119-
log.info("syncDatamateUrlFromVscodeMcp: no IDE MCP config found, skipping sync")
120-
return updated
137+
for (const candidate of mcpJsonPaths) {
138+
try {
139+
const text = await readFile(candidate, "utf-8")
140+
const parsed = JSON.parse(text) as Record<string, unknown>
141+
const map = extractServersMap(parsed)
142+
if (map[DATAMATE_KEY]) {
143+
mcpJsonPath = candidate
144+
serversMap = map
145+
break
146+
}
147+
} catch {
148+
// Unparseable — skip
149+
}
121150
}
122151

123-
const text = await readFile(mcpJsonPath, "utf-8")
124-
let parsed: Record<string, unknown>
125-
try {
126-
parsed = JSON.parse(text) as Record<string, unknown>
127-
} catch {
152+
if (!mcpJsonPath) {
153+
log.info("syncDatamateUrlFromVscodeMcp: no mcp.json with datamate entry found, skipping sync")
128154
return updated
129155
}
130156

131-
let serversMap: Record<string, Record<string, unknown>> = {}
132-
for (const key of ideSource.keys) {
133-
const candidate = parsed[key]
134-
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
135-
serversMap = candidate as Record<string, Record<string, unknown>>
136-
break
137-
}
138-
}
157+
log.info("syncDatamateUrlFromVscodeMcp: using config", {
158+
source: path.relative(cwd, mcpJsonPath),
159+
})
139160

140161
// ── "datamate" entry: sync by updatedAt (works for stdio + HTTP) ────────
141162
const datamateVscode = serversMap[DATAMATE_KEY]
@@ -154,7 +175,6 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
154175
: undefined
155176

156177
if (existingNode) {
157-
// Extract current updatedAt + enabled from altimate-code.json
158178
let existingUpdatedAt: string | undefined
159179
let existingEnabled: boolean | undefined
160180
if (existingNode.type === "object" && existingNode.children) {
@@ -167,7 +187,7 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
167187
}
168188

169189
if (vscodeUpdatedAt === existingUpdatedAt) {
170-
log.info("syncDatamateUrlFromVscodeMcp: datamate entry already up to date, skipping", {
190+
log.info("syncDatamateUrlFromVscodeMcp: datamate entry already up to date", {
171191
updatedAt: vscodeUpdatedAt,
172192
})
173193
} else {
@@ -178,13 +198,13 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
178198
if (datamateVscode["type"] === "stdio") {
179199
const env = datamateVscode["env"] as Record<string, string> | undefined
180200
const { ALTIMATE_EXTENSION_RPC: _rpc, ...restEnv } = env ?? {}
181-
const cmd = typeof datamateVscode["command"] === "string" ? datamateVscode["command"] as string : DATAMATE_KEY
201+
const cmd =
202+
typeof datamateVscode["command"] === "string"
203+
? (datamateVscode["command"] as string)
204+
: DATAMATE_KEY
182205
newEntry = {
183206
type: "local",
184-
command: [
185-
cmd,
186-
...((datamateVscode["args"] as string[]) ?? []),
187-
],
207+
command: [cmd, ...((datamateVscode["args"] as string[]) ?? [])],
188208
...(Object.keys(restEnv).length > 0 ? { environment: restEnv } : {}),
189209
updatedAt: vscodeUpdatedAt,
190210
}
@@ -216,7 +236,7 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
216236
// ── All other remote MCP entries: existing URL-comparison logic ──────────
217237
const httpEntries: Array<{ key: string; url: string }> = []
218238
for (const [key, entry] of Object.entries(serversMap)) {
219-
if (key === DATAMATE_KEY) continue // already handled above
239+
if (key === DATAMATE_KEY) continue
220240
if (typeof entry["url"] === "string") {
221241
httpEntries.push({ key, url: entry["url"] })
222242
}

0 commit comments

Comments
 (0)