Skip to content

Commit 18da6b4

Browse files
author
saravmajestic
committed
fix: address review comments — glob ignore, type guard, persist from disk, dedup respond()
1 parent 2a1c5ac commit 18da6b4

7 files changed

Lines changed: 70 additions & 91 deletions

File tree

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

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { readFile } from "fs/promises"
22
import path from "path"
33
import { parseTree, findNodeAtLocation } from "jsonc-parser"
4-
import { resolveConfigPath, addMcpToConfig } from "../mcp/config"
4+
import { resolveConfigPath, addMcpToConfig, readMcpEntryFromDisk } from "../mcp/config"
55
import { Filesystem } from "../util/filesystem"
66
import { Glob } from "../util/glob"
77
import { Log } from "../util/log"
@@ -18,8 +18,6 @@ export const DATAMATE_KEY = "datamate"
1818
*/
1919
const MCP_SERVERS_KEYS = ["servers", "mcpServers"] as const
2020

21-
/** Glob patterns to exclude from mcp.json scans (large, irrelevant trees). */
22-
const MCP_SCAN_EXCLUDE = ["/node_modules/", "/.git/", "/dist/", "/build/", "/.pnpm/"]
2321

2422
export type DatamateTransport =
2523
| { type: "remote"; url: string }
@@ -51,10 +49,9 @@ async function findAllMcpJsonFiles(projectRootDir: string): Promise<string[]> {
5149
cwd: projectRootDir,
5250
absolute: true,
5351
dot: true,
52+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.pnpm/**"],
5453
})
55-
return paths
56-
.filter((p) => !MCP_SCAN_EXCLUDE.some((ex) => p.includes(ex)))
57-
.sort()
54+
return paths.sort()
5855
} catch {
5956
log.warn("findAllMcpJsonFiles: glob scan failed", { cwd: projectRootDir })
6057
return []
@@ -193,7 +190,7 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
193190
// IDE config uses "stdio"/"http"/"streamable-http"/"sse";
194191
// altimate-code.json uses "local"/"remote".
195192
let newEntry: Record<string, unknown>
196-
if (datamateVscode["type"] === "stdio") {
193+
if ("command" in datamateVscode) {
197194
const env = datamateVscode["env"] as Record<string, string> | undefined
198195
const { ALTIMATE_EXTENSION_RPC: _rpc, ...restEnv } = env ?? {}
199196
const cmd =
@@ -300,30 +297,3 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise<string[
300297
return updated
301298
}
302299

303-
/**
304-
* Read a single MCP entry directly from disk (bypasses the in-memory Config
305-
* singleton) so callers can get the freshly-written config without busting the
306-
* whole cache. Returns undefined if the entry is not found in any config file.
307-
*/
308-
export async function readMcpEntryFromDisk(
309-
name: string,
310-
configPath: string,
311-
): Promise<Config.Mcp | undefined> {
312-
if (!(await Filesystem.exists(configPath))) return undefined
313-
314-
const text = await Filesystem.readText(configPath)
315-
const tree = parseTree(text)
316-
if (!tree) return undefined
317-
318-
const node = findNodeAtLocation(tree, ["mcp", name])
319-
if (!node || node.type !== "object" || !node.children) return undefined
320-
321-
const entry: Record<string, unknown> = {}
322-
for (const prop of node.children) {
323-
if (prop.type === "property" && prop.children) {
324-
entry[prop.children[0]!.value as string] = prop.children[1]!.value
325-
}
326-
}
327-
328-
return entry as Config.Mcp
329-
}

packages/opencode/src/cli/cmd/serve.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ import { withNetworkOptions, resolveNetworkOptions } from "../network"
44
import { Flag } from "../../flag/flag"
55
// altimate_change start — trace: session tracing in headless serve
66
import { subscribeTraceConsumer } from "../../altimate/observability/trace-consumer"
7-
// datamate IDE-sync helpers extracted to shared module
8-
// Logic lives in altimate/datamate-transport.ts so serve.ts, server.ts, and
9-
// datamate.ts all import from one place (no duplication, shared DATAMATE_KEY constant).
10-
export { syncDatamateUrlFromVscodeMcp } from "../../altimate/datamate-transport"
117
// altimate_change start — self-update on headless serve startup
128
import { scheduleStartupUpgradeCheck } from "./serve-upgrade-check"
139
// altimate_change end

packages/opencode/src/mcp/config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,31 @@ export async function findAllConfigPaths(projectDir: string, globalDir: string):
101101
}
102102
return paths
103103
}
104+
105+
/**
106+
* Read a single MCP entry directly from a config file, bypassing the Config
107+
* singleton so callers can get the freshly-written config without busting the
108+
* whole cache. Returns undefined if the entry is not found in the file.
109+
*/
110+
export async function readMcpEntryFromDisk(
111+
name: string,
112+
configPath: string,
113+
): Promise<Config.Mcp | undefined> {
114+
if (!(await Filesystem.exists(configPath))) return undefined
115+
116+
const text = await Filesystem.readText(configPath)
117+
const tree = parseTree(text)
118+
if (!tree) return undefined
119+
120+
const node = findNodeAtLocation(tree, ["mcp", name])
121+
if (!node || node.type !== "object" || !node.children) return undefined
122+
123+
const entry: Record<string, unknown> = {}
124+
for (const prop of node.children) {
125+
if (prop.type === "property" && prop.children) {
126+
entry[prop.children[0]!.value as string] = prop.children[1]!.value
127+
}
128+
}
129+
130+
return entry as Config.Mcp
131+
}

packages/opencode/src/mcp/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import z from "zod/v4"
1616
import { Instance } from "../project/instance"
1717
import { Installation } from "../installation"
1818
// altimate_change start — persist enabled flag
19-
import { findAllConfigPaths, listMcpInConfig, addMcpToConfig } from "./config"
19+
import { findAllConfigPaths, listMcpInConfig, addMcpToConfig, readMcpEntryFromDisk } from "./config"
2020
import { Global } from "../global"
2121
// altimate_change end
2222
import { withTimeout } from "@/util/timeout"
@@ -706,8 +706,7 @@ export namespace MCP {
706706
for (const p of paths) {
707707
const names = await listMcpInConfig(p)
708708
if (names.includes(name)) {
709-
const cfg = await Config.get()
710-
const entry = cfg.mcp?.[name]
709+
const entry = await readMcpEntryFromDisk(name, p)
711710
if (entry)
712711
await addMcpToConfig(
713712
name,

packages/opencode/src/server/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import { McpRoutes } from "./routes/mcp"
3333
import { MCP } from "../mcp"
3434
// Import sync + fresh-read helpers directly from the shared transport module.
3535
// Using datamate-transport.ts instead of serve.ts avoids a dep on a cmd handler.
36-
import { syncDatamateUrlFromVscodeMcp, readMcpEntryFromDisk } from "../altimate/datamate-transport"
36+
import { syncDatamateUrlFromVscodeMcp } from "../altimate/datamate-transport"
37+
import { readMcpEntryFromDisk } from "../mcp/config"
3738
import { resolveConfigPath } from "../mcp/config"
3839
// altimate_change end
3940
import { FileRoutes } from "./routes/file"

packages/opencode/src/session/prompt.ts

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -423,22 +423,15 @@ export namespace SessionPrompt {
423423
// so the next resolveTools() call (once per LLM turn) naturally picks up fresh
424424
// tools without any extra work here. This subscription makes the session layer
425425
// explicitly aware of the reconnect and logs it so it is traceable in prod.
426-
let toolsNeedRefresh = false
427426
const unsubscribeToolsChanged = Bus.subscribe(MCP.ToolsChanged, (event) => {
428427
log.info("MCP.ToolsChanged received — tools will refresh on next turn", {
429428
server: event.properties.server,
430429
sessionID,
431430
})
432-
toolsNeedRefresh = true
433431
})
434432
using _unsubToolsChanged = defer(unsubscribeToolsChanged)
435433
// altimate_change end
436434
while (true) {
437-
// altimate_change start — log when a ToolsChanged event was received since last turn
438-
if (toolsNeedRefresh) {
439-
log.info("refreshing MCP tools after ToolsChanged event", { sessionID })
440-
toolsNeedRefresh = false
441-
}
442435
// altimate_change end
443436
// altimate_change start — SessionStatus.set became async in v1.4.0; await so busy state flushes before LLM call
444437
await SessionStatus.set(sessionID, { type: "busy" })
@@ -2592,6 +2585,33 @@ NOTE: At any point in time through this workflow you should feel free to ask the
25922585

25932586
// altimate_change start — /mcps enable/disable: direct handler bypasses LLM
25942587
if (input.command === "mcps") {
2588+
2589+
// Helper: build and persist an assistant reply for a command shortcut.
2590+
async function respond(
2591+
parentID: string,
2592+
responseText: string,
2593+
): Promise<MessageV2.WithParts> {
2594+
const now = Date.now()
2595+
const assistantMsg: MessageV2.Assistant = {
2596+
id: MessageID.ascending(), role: "assistant", sessionID: input.sessionID,
2597+
parentID, modelID: model.modelID, providerID: model.providerID,
2598+
mode: "builder", agent: "builder",
2599+
path: { cwd: Instance.directory, root: Instance.worktree },
2600+
cost: 0, tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
2601+
finish: "stop", time: { created: now, completed: now },
2602+
}
2603+
await Session.updateMessage(assistantMsg)
2604+
const textPart: MessageV2.TextPart = {
2605+
id: PartID.ascending(), sessionID: input.sessionID, messageID: assistantMsg.id,
2606+
type: "text", text: responseText, time: { start: now, end: now },
2607+
}
2608+
await Session.updatePart(textPart)
2609+
Bus.publish(Command.Event.Executed, {
2610+
name: input.command, sessionID: input.sessionID,
2611+
arguments: input.arguments, messageID: assistantMsg.id,
2612+
})
2613+
return { info: assistantMsg, parts: [textPart] } as MessageV2.WithParts
2614+
}
25952615
const trimmed = input.arguments.trim()
25962616

25972617
if (!trimmed) {
@@ -2608,7 +2628,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
26082628
const icon = s.status === "connected" ? "\u2713" : "\u25cb"
26092629
const label =
26102630
s.status === "failed"
2611-
? icon + " " + s.status + " (" + (s as any).error + ")"
2631+
? icon + " " + s.status + " (" + s.error + ")"
26122632
: icon + " " + s.status
26132633
return "| `" + srv + "` | " + label + " |"
26142634
})
@@ -2617,26 +2637,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
26172637
? "MCP servers:\n\n| Server | Status |\n|---|---|\n" + rows
26182638
: "No MCP servers configured."
26192639

2620-
const now = Date.now()
2621-
const assistantMsg: MessageV2.Assistant = {
2622-
id: MessageID.ascending(), role: "assistant", sessionID: input.sessionID,
2623-
parentID: userMsg.info.id, modelID: model.modelID, providerID: model.providerID,
2624-
mode: "builder", agent: "builder",
2625-
path: { cwd: Instance.directory, root: Instance.worktree },
2626-
cost: 0, tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
2627-
finish: "stop", time: { created: now, completed: now },
2628-
}
2629-
await Session.updateMessage(assistantMsg)
2630-
const textPart: MessageV2.TextPart = {
2631-
id: PartID.ascending(), sessionID: input.sessionID, messageID: assistantMsg.id,
2632-
type: "text", text: responseText, time: { start: now, end: now },
2633-
}
2634-
await Session.updatePart(textPart)
2635-
Bus.publish(Command.Event.Executed, {
2636-
name: input.command, sessionID: input.sessionID,
2637-
arguments: input.arguments, messageID: assistantMsg.id,
2638-
})
2639-
return { info: assistantMsg, parts: [textPart] } as MessageV2.WithParts
2640+
return respond(userMsg.info.id, responseText)
26402641
}
26412642

26422643
const subMatch = trimmed.match(/^(enable|disable)\s+(\S+)/)
@@ -2660,33 +2661,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
26602661
if (entry?.status === "connected") {
26612662
responseText = `MCP server **${name}** enabled. Status: connected.`
26622663
} else {
2663-
responseText = `Attempted to enable MCP server **${name}**. Status: ${entry?.status ?? "unknown"}${(entry as any)?.error ? " — " + (entry as any).error : "."}.`
2664+
const errSuffix = entry?.status === "failed" ? " — " + entry.error : ""
2665+
responseText = `Attempted to enable MCP server **${name}**. Status: ${entry?.status ?? "unknown"}${errSuffix}.`
26642666
}
26652667
} else {
26662668
await MCP.disconnect(name)
26672669
responseText = `MCP server **${name}** disabled.`
26682670
}
26692671

2670-
const now = Date.now()
2671-
const assistantMsg: MessageV2.Assistant = {
2672-
id: MessageID.ascending(), role: "assistant", sessionID: input.sessionID,
2673-
parentID: userMsg.info.id, modelID: model.modelID, providerID: model.providerID,
2674-
mode: "builder", agent: "builder",
2675-
path: { cwd: Instance.directory, root: Instance.worktree },
2676-
cost: 0, tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
2677-
finish: "stop", time: { created: now, completed: now },
2678-
}
2679-
await Session.updateMessage(assistantMsg)
2680-
const textPart: MessageV2.TextPart = {
2681-
id: PartID.ascending(), sessionID: input.sessionID, messageID: assistantMsg.id,
2682-
type: "text", text: responseText, time: { start: now, end: now },
2683-
}
2684-
await Session.updatePart(textPart)
2685-
Bus.publish(Command.Event.Executed, {
2686-
name: input.command, sessionID: input.sessionID,
2687-
arguments: input.arguments, messageID: assistantMsg.id,
2688-
})
2689-
return { info: assistantMsg, parts: [textPart] } as MessageV2.WithParts
2672+
return respond(userMsg.info.id, responseText)
26902673
}
26912674
}
26922675
// altimate_change end

packages/opencode/src/util/glob.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export namespace Glob {
88
include?: "file" | "all"
99
dot?: boolean
1010
symlink?: boolean
11+
ignore?: string | string[]
1112
}
1213

1314
function toGlobOptions(options: Options): GlobOptions {
@@ -17,6 +18,7 @@ export namespace Glob {
1718
dot: options.dot,
1819
follow: options.symlink ?? false,
1920
nodir: options.include !== "all",
21+
ignore: options.ignore,
2022
}
2123
}
2224

0 commit comments

Comments
 (0)