From 6f5aee81ac7e9e00815291a9f39cd739a70cb0ba Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 06:30:22 +0000 Subject: [PATCH 1/8] fix: route datamate tools through local stdio mcp-engine instead of cloud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DatamateManagerTool.handleAdd() was creating MCP server entries pointing to the cloud MCP (https://mcpserver.getaltimate.com) because buildMcpConfig() fell back to DEFAULT_MCP_URL when mcpServerUrl was not set. The cloud MCP has no access to connections.json, causing "No connection configured for integration: jira" even when connections were configured locally. Replace the HTTP/remote config with a local stdio config: { type: "local", command: ["datamate", "start-stdio", "--datamate", id] } Each spawned process: - Reads connections.json from ~/.altimate/ directly (local engine) - Discovers the right VS Code extension bridge via cwd-based sidecar matching in ~/.altimate/extension-rpc/, supporting multiple VS Code windows cleanly without any shared URL or socket path in the config Also strip ALTIMATE_EXTENSION_RPC from environment when persisting discovered servers via mcp_discover — the socket path is session-specific and would cross-wire spawned processes to wrong bridges on future sessions. --- .../opencode/src/altimate/tools/datamate.ts | 13 ++++++++++-- .../src/altimate/tools/mcp-discover.ts | 20 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/altimate/tools/datamate.ts b/packages/opencode/src/altimate/tools/datamate.ts index e15e5af450..ad7cbd7d47 100644 --- a/packages/opencode/src/altimate/tools/datamate.ts +++ b/packages/opencode/src/altimate/tools/datamate.ts @@ -163,10 +163,19 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p } } try { - const creds = await AltimateApi.getCredentials() + // altimate_change start — use stdio local transport so datamate tools run + // inside the locally-spawned mcp-engine (which reads connections.json) instead + // of routing to the cloud MCP server. Each spawned process discovers the right + // VS Code extension bridge via cwd-based sidecar matching in + // ~/.altimate/extension-rpc/, so multiple VS Code windows are handled correctly + // without any shared URL or socket path in the config. const datamate = await AltimateApi.getDatamate(args.datamate_id) const serverName = args.name ?? `datamate-${slugify(datamate.name)}` - const mcpConfig = AltimateApi.buildMcpConfig(creds, args.datamate_id) + const mcpConfig = { + type: "local" as const, + command: ["datamate", "start-stdio", "--datamate", args.datamate_id], + } + // altimate_change end // Always save to config first so it persists for future sessions const isGlobal = args.scope === "global" diff --git a/packages/opencode/src/altimate/tools/mcp-discover.ts b/packages/opencode/src/altimate/tools/mcp-discover.ts index 30af405ac6..1a2592bf96 100644 --- a/packages/opencode/src/altimate/tools/mcp-discover.ts +++ b/packages/opencode/src/altimate/tools/mcp-discover.ts @@ -35,6 +35,22 @@ function safeDetail(server: { type: string } & Record): string { return `(${server.type})` } +// altimate_change start — strip session-specific env vars before persisting +// discovered servers. ALTIMATE_EXTENSION_RPC is a Unix socket path that is +// unique to the current VS Code extension host process. Writing it to disk +// causes altimate-code on a future session (or a different VS Code window) to +// spawn datamate processes that connect to the wrong bridge or a dead socket. +// Stripping it forces runtime discovery via ~/.altimate/extension-rpc/ sidecars, +// which always resolves the correct live bridge by matching process.cwd() against +// each bridge's recorded workspaceFolders. +function stripSessionEnv(cfg: import("../../config/config").Config.Mcp): import("../../config/config").Config.Mcp { + if (cfg.type !== "local" || !cfg.environment) return cfg + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ALTIMATE_EXTENSION_RPC: _rpc, ...rest } = cfg.environment + return { ...cfg, environment: Object.keys(rest).length > 0 ? rest : undefined } +} +// altimate_change end + 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.", @@ -110,7 +126,9 @@ export const McpDiscoverTool = Tool.define("mcp_discover", { ) for (const name of toAdd) { - await addMcpToConfig(name, discovered[name], configPath) + // altimate_change start — strip session-specific ALTIMATE_EXTENSION_RPC + await addMcpToConfig(name, stripSessionEnv(discovered[name]), configPath) + // altimate_change end } lines.push(`\nAdded ${toAdd.length} server(s) to ${configPath}: ${toAdd.join(", ")}`) From d0023b3e93a75f030ed759424cc103eaf649d357 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 09:43:10 +0000 Subject: [PATCH 2/8] feat: support http and stdio transport in datamate tool, sync url on serve restart --- .../opencode/src/altimate/tools/datamate.ts | 66 ++++++++++-- packages/opencode/src/cli/cmd/serve.ts | 101 ++++++++++++++++++ 2 files changed, 157 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/altimate/tools/datamate.ts b/packages/opencode/src/altimate/tools/datamate.ts index ad7cbd7d47..bd22ca8271 100644 --- a/packages/opencode/src/altimate/tools/datamate.ts +++ b/packages/opencode/src/altimate/tools/datamate.ts @@ -1,4 +1,6 @@ import z from "zod" +import { readFile } from "fs/promises" +import path from "path" import { Tool } from "../../tool/tool" import { AltimateApi } from "../api/client" import { MCP } from "../../mcp" @@ -25,6 +27,46 @@ export function slugify(name: string): string { .replace(/^-|-$/g, "") } +// altimate_change start — read transport type from .vscode/mcp.json +// Returns { type: "remote", url } if the datamate entry is an HTTP server, +// { type: "local" } if it is a stdio server, or null if the file is missing +// or no datamate entry is found. The caller uses this to pick the right +// mcpConfig shape and falls back to the cloud config when null is returned. +async function readVscodeMcpTransport( + projectRootDir: string, +): Promise<{ type: "remote"; url: string } | { type: "local" } | null> { + try { + const mcpJsonPath = path.join(projectRootDir, ".vscode", "mcp.json") + const text = await readFile(mcpJsonPath, "utf-8") + const parsed = JSON.parse(text) as Record + + // .vscode/mcp.json uses either "servers" (VS Code 1.99+) or "mcpServers" key + const serversMap = + (parsed["servers"] as Record> | undefined) ?? + (parsed["mcpServers"] as Record> | undefined) ?? + {} + + for (const [key, entry] of Object.entries(serversMap)) { + const args = Array.isArray(entry["args"]) ? (entry["args"] as string[]) : [] + const isDatamate = + key === "datamate" || + args.some((a) => a.includes("start-stdio") || a.includes("datamate-cli")) + + if (!isDatamate) continue + + if (typeof entry["url"] === "string") { + return { type: "remote", url: entry["url"] } + } + return { type: "local" } + } + return null + } catch { + // File missing or unparseable — caller falls back to cloud config + return null + } +} +// altimate_change end + export const DatamateManagerTool = Tool.define("datamate_manager", { description: "Manage Altimate Datamates — AI teammates with integrations (Snowflake, Jira, dbt, etc). " + @@ -163,18 +205,22 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p } } try { - // altimate_change start — use stdio local transport so datamate tools run - // inside the locally-spawned mcp-engine (which reads connections.json) instead - // of routing to the cloud MCP server. Each spawned process discovers the right - // VS Code extension bridge via cwd-based sidecar matching in - // ~/.altimate/extension-rpc/, so multiple VS Code windows are handled correctly - // without any shared URL or socket path in the config. + // altimate_change start — detect transport from .vscode/mcp.json; fall back to cloud + // Reads .vscode/mcp.json in the project root to determine whether the extension + // registered an HTTP or stdio transport for the datamate MCP server. This lets + // altimate-code honour whatever the VS Code extension wrote (HTTP when the local + // mcp-engine is running, stdio when it is not), and falls back to the cloud MCP + // config when no .vscode/mcp.json entry is found. const datamate = await AltimateApi.getDatamate(args.datamate_id) const serverName = args.name ?? `datamate-${slugify(datamate.name)}` - const mcpConfig = { - type: "local" as const, - command: ["datamate", "start-stdio", "--datamate", args.datamate_id], - } + const transport = await readVscodeMcpTransport(projectRoot()) + const creds = transport ? undefined : await AltimateApi.getCredentials() + const mcpConfig = + transport?.type === "remote" + ? { type: "remote" as const, url: transport.url } + : transport?.type === "local" + ? { type: "local" as const, command: ["datamate", "start-stdio", "--datamate", args.datamate_id] } + : AltimateApi.buildMcpConfig(creds!, args.datamate_id) // altimate_change end // Always save to config first so it persists for future sessions diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index cc7cb3c3ca..9d0a5950e7 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -5,6 +5,101 @@ import { Flag } from "../../flag/flag" import { Workspace } from "../../control-plane/workspace" import { Project } from "../../project/project" import { Installation } from "../../installation" +// altimate_change start — URL sync helpers +import { readFile } from "fs/promises" +import path from "path" +import { existsSync } from "fs" +import { resolveConfigPath, addMcpToConfig } from "../../mcp/config" +import { Filesystem } from "../../util/filesystem" +import { parseTree, findNodeAtLocation } from "jsonc-parser" +// altimate_change end + +// altimate_change start — sync datamate HTTP URL from .vscode/mcp.json +// Re-reads the URL that the VS Code extension wrote for the local mcp-engine +// and updates any matching remote MCP entries in the project altimate-code.json. +// Fire-and-forget: errors are logged but never thrown. +async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { + try { + const mcpJsonPath = path.join(cwd, ".vscode", "mcp.json") + if (!existsSync(mcpJsonPath)) return + + const text = await readFile(mcpJsonPath, "utf-8") + let parsed: Record + try { + parsed = JSON.parse(text) as Record + } catch { + return + } + + // Collect HTTP entries from .vscode/mcp.json (servers or mcpServers key) + const serversMap = + (parsed["servers"] as Record> | undefined) ?? + (parsed["mcpServers"] as Record> | undefined) ?? + {} + + const httpEntries: Array<{ key: string; url: string }> = [] + for (const [key, entry] of Object.entries(serversMap)) { + if (typeof entry["url"] === "string") { + httpEntries.push({ key, url: entry["url"] }) + } + } + if (httpEntries.length === 0) return + + // Find the project-level config (prefers .altimate-code/altimate-code.json) + const configPath = await resolveConfigPath(cwd) + if (!(await Filesystem.exists(configPath))) return + + const configText = await Filesystem.readText(configPath) + const tree = parseTree(configText) + if (!tree) return + + const mcpNode = findNodeAtLocation(tree, ["mcp"]) + if (!mcpNode || mcpNode.type !== "object" || !mcpNode.children) return + + // Collect all remote MCP entries in the project config + const remoteMcpEntries: Array<{ name: string; url: string }> = [] + for (const child of mcpNode.children) { + if (child.type !== "property" || !child.children) continue + const nameNode = child.children[0] + const valueNode = child.children[1] + if (!nameNode || !valueNode || valueNode.type !== "object" || !valueNode.children) continue + const typeNode = findNodeAtLocation(valueNode, ["type"]) + const urlNode = findNodeAtLocation(valueNode, ["url"]) + if ( + typeNode?.value === "remote" && + typeof urlNode?.value === "string" + ) { + remoteMcpEntries.push({ name: nameNode.value as string, url: urlNode.value }) + } + } + + // For each remote entry whose name matches an HTTP entry in .vscode/mcp.json, + // update the URL if it has changed. + for (const remote of remoteMcpEntries) { + const match = httpEntries.find((e) => e.key === remote.name) + if (match && match.url !== remote.url) { + // Read the full existing entry to preserve other fields (headers, oauth, etc.) + const entryNode = findNodeAtLocation(tree, ["mcp", remote.name]) + if (!entryNode || entryNode.type !== "object" || !entryNode.children) continue + + // Rebuild the entry object from the parsed tree + const entry: Record = {} + for (const prop of entryNode.children) { + if (prop.type === "property" && prop.children) { + entry[prop.children[0]!.value as string] = prop.children[1]!.value + } + } + entry["url"] = match.url + + await addMcpToConfig(remote.name, entry as Parameters[1], configPath) + console.log(`[altimate-code] synced datamate URL for '${remote.name}': ${match.url}`) + } + } + } catch (err) { + console.warn(`[altimate-code] syncDatamateUrlFromVscodeMcp failed (non-fatal):`, err) + } +} +// altimate_change end export const ServeCommand = cmd({ command: "serve", @@ -16,6 +111,12 @@ export const ServeCommand = cmd({ console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) + // altimate_change start — sync datamate URL from .vscode/mcp.json on serve startup + // When a VS Code window restarts, the extension picks a new local port and rewrites + // .vscode/mcp.json. Re-reading it here keeps altimate-code.json in sync without + // requiring any user action. + await syncDatamateUrlFromVscodeMcp(process.cwd()) + // altimate_change end const server = await Server.listen(opts) console.log(`altimate-code server listening on http://${server.hostname}:${server.port}`) // altimate_change end From ba55b8cf4d41d6c77429949de431c637623f99c9 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 10:02:36 +0000 Subject: [PATCH 3/8] feat: add reload-datamate endpoint for live MCP URL refresh --- packages/opencode/src/cli/cmd/serve.ts | 18 +++++++++------ packages/opencode/src/server/server.ts | 32 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 9d0a5950e7..932a290fc4 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -18,17 +18,19 @@ import { parseTree, findNodeAtLocation } from "jsonc-parser" // Re-reads the URL that the VS Code extension wrote for the local mcp-engine // and updates any matching remote MCP entries in the project altimate-code.json. // Fire-and-forget: errors are logged but never thrown. -async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { +// Returns the list of MCP server names whose URL was updated. +export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { + const updated: string[] = [] try { const mcpJsonPath = path.join(cwd, ".vscode", "mcp.json") - if (!existsSync(mcpJsonPath)) return + if (!existsSync(mcpJsonPath)) return updated const text = await readFile(mcpJsonPath, "utf-8") let parsed: Record try { parsed = JSON.parse(text) as Record } catch { - return + return updated } // Collect HTTP entries from .vscode/mcp.json (servers or mcpServers key) @@ -43,18 +45,18 @@ async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { httpEntries.push({ key, url: entry["url"] }) } } - if (httpEntries.length === 0) return + if (httpEntries.length === 0) return updated // Find the project-level config (prefers .altimate-code/altimate-code.json) const configPath = await resolveConfigPath(cwd) - if (!(await Filesystem.exists(configPath))) return + if (!(await Filesystem.exists(configPath))) return updated const configText = await Filesystem.readText(configPath) const tree = parseTree(configText) - if (!tree) return + if (!tree) return updated const mcpNode = findNodeAtLocation(tree, ["mcp"]) - if (!mcpNode || mcpNode.type !== "object" || !mcpNode.children) return + if (!mcpNode || mcpNode.type !== "object" || !mcpNode.children) return updated // Collect all remote MCP entries in the project config const remoteMcpEntries: Array<{ name: string; url: string }> = [] @@ -93,11 +95,13 @@ async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { await addMcpToConfig(remote.name, entry as Parameters[1], configPath) console.log(`[altimate-code] synced datamate URL for '${remote.name}': ${match.url}`) + updated.push(remote.name) } } } catch (err) { console.warn(`[altimate-code] syncDatamateUrlFromVscodeMcp failed (non-fatal):`, err) } + return updated } // altimate_change end diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 35f3304472..3c35535654 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -29,6 +29,10 @@ import { ProjectRoutes } from "./routes/project" import { SessionRoutes } from "./routes/session" import { PtyRoutes } from "./routes/pty" import { McpRoutes } from "./routes/mcp" +// altimate_change start — reload-datamate endpoint +import { MCP } from "../mcp" +import { syncDatamateUrlFromVscodeMcp } from "../cli/cmd/serve" +// altimate_change end import { FileRoutes } from "./routes/file" import { ConfigRoutes } from "./routes/config" import { ExperimentalRoutes } from "./routes/experimental" @@ -561,6 +565,34 @@ export namespace Server { }) }, ) + // altimate_change start — POST /altimate/mcp/reload-datamate + // Updates the datamate MCP server URL from .vscode/mcp.json and reconnects the + // live MCP client so the new URL takes effect immediately without a server restart. + .post("/altimate/mcp/reload-datamate", async (c) => { + try { + const directory = Instance.directory + // Sync URL from .vscode/mcp.json → project config; returns updated server names. + const updatedNames = await syncDatamateUrlFromVscodeMcp(directory) + const updated = updatedNames.length > 0 + + if (updated) { + // Reconnect each updated server that is currently live so the new URL takes effect. + const currentStatus = await MCP.status() + for (const name of updatedNames) { + if (currentStatus[name]?.status === "connected") { + await MCP.disconnect(name) + await MCP.connect(name) + } + } + } + + return c.json({ ok: true, updated }) + } catch (err) { + const error = err instanceof Error ? err.message : String(err) + return c.json({ ok: false, error }) + } + }) + // altimate_change end .all("/*", async (c) => { const path = c.req.path From 9f2f31e0572bffb3ab04462fb5ec7741fd337d96 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 10:23:07 +0000 Subject: [PATCH 4/8] feat: add logging to reload-datamate endpoint and syncDatamateUrl --- packages/opencode/src/cli/cmd/serve.ts | 14 +++++++++++++- packages/opencode/src/server/server.ts | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 932a290fc4..fd0f71b9f7 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -12,6 +12,11 @@ import { existsSync } from "fs" import { resolveConfigPath, addMcpToConfig } from "../../mcp/config" import { Filesystem } from "../../util/filesystem" import { parseTree, findNodeAtLocation } from "jsonc-parser" +import { Log } from "../../util/log" +// altimate_change end + +// altimate_change start +const log = Log.create({ service: "serve" }) // altimate_change end // altimate_change start — sync datamate HTTP URL from .vscode/mcp.json @@ -94,10 +99,17 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise[1], configPath) - console.log(`[altimate-code] synced datamate URL for '${remote.name}': ${match.url}`) + // altimate_change start + log.info("syncDatamateUrl: updating", { name: remote.name, oldUrl: remote.url, newUrl: match.url }) + // altimate_change end updated.push(remote.name) } } + // altimate_change start + if (updated.length === 0) { + log.info("syncDatamateUrl: no changes") + } + // altimate_change end } catch (err) { console.warn(`[altimate-code] syncDatamateUrlFromVscodeMcp failed (non-fatal):`, err) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 3c35535654..7e83d3a8a3 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -571,24 +571,40 @@ export namespace Server { .post("/altimate/mcp/reload-datamate", async (c) => { try { const directory = Instance.directory + // altimate_change start + log.info("reload-datamate: syncing URL from .vscode/mcp.json", { directory }) + // altimate_change end // Sync URL from .vscode/mcp.json → project config; returns updated server names. const updatedNames = await syncDatamateUrlFromVscodeMcp(directory) const updated = updatedNames.length > 0 if (updated) { + // altimate_change start + log.info("reload-datamate: URL updated, reconnecting MCP servers", { updatedNames }) + // altimate_change end // Reconnect each updated server that is currently live so the new URL takes effect. const currentStatus = await MCP.status() for (const name of updatedNames) { if (currentStatus[name]?.status === "connected") { + // altimate_change start + log.info("reload-datamate: reconnecting", { name }) + // altimate_change end await MCP.disconnect(name) await MCP.connect(name) } } + } else { + // altimate_change start + log.info("reload-datamate: no URL changes detected") + // altimate_change end } return c.json({ ok: true, updated }) } catch (err) { const error = err instanceof Error ? err.message : String(err) + // altimate_change start + log.error("reload-datamate: failed", { error }) + // altimate_change end return c.json({ ok: false, error }) } }) From e292724d7b88ed191d2fe1bb06567efee4d4da6f Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 13:03:16 +0000 Subject: [PATCH 5/8] feat: refresh session MCP tools on ToolsChanged, bump updatedAt on reload --- packages/opencode/src/cli/cmd/serve.ts | 3 +++ packages/opencode/src/config/config.ts | 6 ++++++ packages/opencode/src/session/prompt.ts | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index fd0f71b9f7..205f67a988 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -97,6 +97,9 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise[1], configPath) // altimate_change start diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 99295821c2..4e813883fb 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -622,6 +622,9 @@ export namespace Config { .positive() .optional() .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + // altimate_change start — sync timestamp written by syncDatamateUrlFromVscodeMcp + updatedAt: z.string().optional().describe("ISO timestamp of last URL sync; used to detect reconnect need."), + // altimate_change end }) .strict() .meta({ @@ -661,6 +664,9 @@ export namespace Config { .positive() .optional() .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + // altimate_change start — sync timestamp written by syncDatamateUrlFromVscodeMcp + updatedAt: z.string().optional().describe("ISO timestamp of last URL sync; used to detect reconnect need."), + // altimate_change end }) .strict() .meta({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b078d746ac..d41a3c1dd1 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -416,7 +416,30 @@ export namespace SessionPrompt { process.once("beforeExit", emergencySessionEnd) process.once("exit", emergencySessionEnd) // altimate_change end + // altimate_change start — refresh MCP tools on ToolsChanged event + // When a datamate MCP server reconnects (transport change, window restart), + // MCP.ToolsChanged is published. MCP.tools() already uses a per-client cache + // that is invalidated by the notification handler that publishes this event, + // so the next resolveTools() call (once per LLM turn) naturally picks up fresh + // tools without any extra work here. This subscription makes the session layer + // explicitly aware of the reconnect and logs it so it is traceable in prod. + let toolsNeedRefresh = false + const unsubscribeToolsChanged = Bus.subscribe(MCP.ToolsChanged, (event) => { + log.info("MCP.ToolsChanged received — tools will refresh on next turn", { + server: event.properties.server, + sessionID, + }) + toolsNeedRefresh = true + }) + using _unsubToolsChanged = defer(unsubscribeToolsChanged) + // altimate_change end while (true) { + // altimate_change start — log when a ToolsChanged event was received since last turn + if (toolsNeedRefresh) { + log.info("refreshing MCP tools after ToolsChanged event", { sessionID }) + toolsNeedRefresh = false + } + // altimate_change end // altimate_change start — SessionStatus.set became async in v1.4.0; await so busy state flushes before LLM call await SessionStatus.set(sessionID, { type: "busy" }) // altimate_change end From 5b48cba0b9f95b419acacd9f51f7664c8197b224 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 13:20:06 +0000 Subject: [PATCH 6/8] fix: enable datamate by default when added to project config --- packages/opencode/src/altimate/tools/datamate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/altimate/tools/datamate.ts b/packages/opencode/src/altimate/tools/datamate.ts index bd22ca8271..db312c45e1 100644 --- a/packages/opencode/src/altimate/tools/datamate.ts +++ b/packages/opencode/src/altimate/tools/datamate.ts @@ -226,7 +226,7 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p // Always save to config first so it persists for future sessions const isGlobal = args.scope === "global" const configPath = await resolveConfigPath(isGlobal ? Global.Path.config : projectRoot(), isGlobal) - await addMcpToConfig(serverName, mcpConfig, configPath) + await addMcpToConfig(serverName, { ...mcpConfig, enabled: true }, configPath) await MCP.add(serverName, mcpConfig) From ddc75b9346d4b4db8f30b2223dbe67c917ef3f92 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 13:31:28 +0000 Subject: [PATCH 7/8] fix: sync datamate entry by updatedAt (covers stdio + HTTP), URL comparison for other entries --- packages/opencode/src/cli/cmd/serve.ts | 192 +++++++++++++++++-------- 1 file changed, 129 insertions(+), 63 deletions(-) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 205f67a988..0524b4a313 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -19,11 +19,15 @@ import { Log } from "../../util/log" const log = Log.create({ service: "serve" }) // altimate_change end -// altimate_change start — sync datamate HTTP URL from .vscode/mcp.json -// Re-reads the URL that the VS Code extension wrote for the local mcp-engine -// and updates any matching remote MCP entries in the project altimate-code.json. +// altimate_change start — sync datamate from .vscode/mcp.json +// Keeps altimate-code.json in sync with what the VS Code extension writes to +// .vscode/mcp.json. For the extension-managed "datamate" entry, uses the +// updatedAt field as the change signal — works for both stdio and HTTP transport. +// All other remote MCP entries fall back to URL comparison (original behaviour). // Fire-and-forget: errors are logged but never thrown. -// Returns the list of MCP server names whose URL was updated. +// Returns the list of MCP server names whose config was updated. +const DATAMATE_KEY = "datamate" + export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise { const updated: string[] = [] try { @@ -38,81 +42,143 @@ export async function syncDatamateUrlFromVscodeMcp(cwd: string): Promise> | undefined) ?? (parsed["mcpServers"] as Record> | undefined) ?? {} + // ── "datamate" entry: sync by updatedAt (works for stdio + HTTP) ──────── + const datamateVscode = serversMap[DATAMATE_KEY] + const vscodeUpdatedAt = + datamateVscode && typeof datamateVscode["updatedAt"] === "string" + ? (datamateVscode["updatedAt"] as string) + : undefined + + if (datamateVscode && vscodeUpdatedAt) { + const configPath = await resolveConfigPath(cwd) + if (await Filesystem.exists(configPath)) { + const configText = await Filesystem.readText(configPath) + const existingTree = parseTree(configText) + const existingNode = existingTree + ? findNodeAtLocation(existingTree, ["mcp", DATAMATE_KEY]) + : undefined + + if (existingNode) { + // Extract current updatedAt + enabled from altimate-code.json + let existingUpdatedAt: string | undefined + let existingEnabled: boolean | undefined + if (existingNode.type === "object" && existingNode.children) { + for (const prop of existingNode.children) { + if (prop.type !== "property" || !prop.children) continue + const k = prop.children[0]!.value as string + if (k === "updatedAt") existingUpdatedAt = prop.children[1]!.value as string + if (k === "enabled") existingEnabled = prop.children[1]!.value as boolean + } + } + + if (vscodeUpdatedAt !== existingUpdatedAt) { + // Build the new config entry in altimate-code.json format. + // .vscode/mcp.json uses "stdio"/"http"/"streamable-http"/"sse"; + // altimate-code.json uses "local"/"remote". + let newEntry: Record + if (datamateVscode["type"] === "stdio") { + const env = datamateVscode["env"] as Record | undefined + const { ALTIMATE_EXTENSION_RPC: _rpc, ...restEnv } = env ?? {} + newEntry = { + type: "local", + command: [ + datamateVscode["command"] as string, + ...((datamateVscode["args"] as string[]) ?? []), + ], + ...(Object.keys(restEnv).length > 0 ? { environment: restEnv } : {}), + updatedAt: vscodeUpdatedAt, + } + } else { + // http / streamable-http / sse → remote + newEntry = { + type: "remote", + url: datamateVscode["url"] as string, + updatedAt: vscodeUpdatedAt, + } + } + if (typeof existingEnabled === "boolean") newEntry["enabled"] = existingEnabled + + await addMcpToConfig( + DATAMATE_KEY, + newEntry as Parameters[1], + configPath, + ) + log.info("syncDatamateUrl: datamate entry synced", { + type: datamateVscode["type"], + updatedAt: vscodeUpdatedAt, + }) + updated.push(DATAMATE_KEY) + } + } + } + } + + // ── All other remote MCP entries: existing URL-comparison logic ────────── const httpEntries: Array<{ key: string; url: string }> = [] for (const [key, entry] of Object.entries(serversMap)) { + if (key === DATAMATE_KEY) continue // already handled above if (typeof entry["url"] === "string") { httpEntries.push({ key, url: entry["url"] }) } } - if (httpEntries.length === 0) return updated - - // Find the project-level config (prefers .altimate-code/altimate-code.json) - const configPath = await resolveConfigPath(cwd) - if (!(await Filesystem.exists(configPath))) return updated - - const configText = await Filesystem.readText(configPath) - const tree = parseTree(configText) - if (!tree) return updated - - const mcpNode = findNodeAtLocation(tree, ["mcp"]) - if (!mcpNode || mcpNode.type !== "object" || !mcpNode.children) return updated - - // Collect all remote MCP entries in the project config - const remoteMcpEntries: Array<{ name: string; url: string }> = [] - for (const child of mcpNode.children) { - if (child.type !== "property" || !child.children) continue - const nameNode = child.children[0] - const valueNode = child.children[1] - if (!nameNode || !valueNode || valueNode.type !== "object" || !valueNode.children) continue - const typeNode = findNodeAtLocation(valueNode, ["type"]) - const urlNode = findNodeAtLocation(valueNode, ["url"]) - if ( - typeNode?.value === "remote" && - typeof urlNode?.value === "string" - ) { - remoteMcpEntries.push({ name: nameNode.value as string, url: urlNode.value }) - } - } - // For each remote entry whose name matches an HTTP entry in .vscode/mcp.json, - // update the URL if it has changed. - for (const remote of remoteMcpEntries) { - const match = httpEntries.find((e) => e.key === remote.name) - if (match && match.url !== remote.url) { - // Read the full existing entry to preserve other fields (headers, oauth, etc.) - const entryNode = findNodeAtLocation(tree, ["mcp", remote.name]) - if (!entryNode || entryNode.type !== "object" || !entryNode.children) continue - - // Rebuild the entry object from the parsed tree - const entry: Record = {} - for (const prop of entryNode.children) { - if (prop.type === "property" && prop.children) { - entry[prop.children[0]!.value as string] = prop.children[1]!.value + if (httpEntries.length > 0) { + const configPath = await resolveConfigPath(cwd) + if (await Filesystem.exists(configPath)) { + const configText = await Filesystem.readText(configPath) + const tree = parseTree(configText) + const mcpNode = tree ? findNodeAtLocation(tree, ["mcp"]) : undefined + + if (tree && mcpNode && mcpNode.type === "object" && mcpNode.children) { + const remoteMcpEntries: Array<{ name: string; url: string }> = [] + for (const child of mcpNode.children) { + if (child.type !== "property" || !child.children) continue + const nameNode = child.children[0] + const valueNode = child.children[1] + if (!nameNode || !valueNode || valueNode.type !== "object" || !valueNode.children) continue + const typeNode = findNodeAtLocation(valueNode, ["type"]) + const urlNode = findNodeAtLocation(valueNode, ["url"]) + if (typeNode?.value === "remote" && typeof urlNode?.value === "string") { + remoteMcpEntries.push({ name: nameNode.value as string, url: urlNode.value }) + } + } + + for (const remote of remoteMcpEntries) { + const match = httpEntries.find((e) => e.key === remote.name) + if (match && match.url !== remote.url) { + const entryNode = findNodeAtLocation(tree, ["mcp", remote.name]) + if (!entryNode || entryNode.type !== "object" || !entryNode.children) continue + const entry: Record = {} + for (const prop of entryNode.children) { + if (prop.type === "property" && prop.children) { + entry[prop.children[0]!.value as string] = prop.children[1]!.value + } + } + entry["url"] = match.url + entry["updatedAt"] = new Date().toISOString() + await addMcpToConfig( + remote.name, + entry as Parameters[1], + configPath, + ) + log.info("syncDatamateUrl: updating", { + name: remote.name, + oldUrl: remote.url, + newUrl: match.url, + }) + updated.push(remote.name) + } } } - entry["url"] = match.url - // altimate_change start — bump updatedAt so session layer can detect reconnect need - entry["updatedAt"] = new Date().toISOString() - // altimate_change end - - await addMcpToConfig(remote.name, entry as Parameters[1], configPath) - // altimate_change start - log.info("syncDatamateUrl: updating", { name: remote.name, oldUrl: remote.url, newUrl: match.url }) - // altimate_change end - updated.push(remote.name) } } - // altimate_change start - if (updated.length === 0) { - log.info("syncDatamateUrl: no changes") - } - // altimate_change end + + if (updated.length === 0) log.info("syncDatamateUrl: no changes") } catch (err) { console.warn(`[altimate-code] syncDatamateUrlFromVscodeMcp failed (non-fatal):`, err) } From ff6f9d4655ca5e0d4d78a59d069553b62c679ca6 Mon Sep 17 00:00:00 2001 From: saravmajestic Date: Fri, 5 Jun 2026 13:52:50 +0000 Subject: [PATCH 8/8] fix: persist enabled/disabled flag to config on MCP connect/disconnect --- packages/opencode/src/mcp/index.ts | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index b110467cec..df96478476 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -15,6 +15,10 @@ import { NamedError } from "@opencode-ai/util/error" import z from "zod/v4" import { Instance } from "../project/instance" import { Installation } from "../installation" +// altimate_change start — persist enabled flag +import { findAllConfigPaths, listMcpInConfig, addMcpToConfig } from "./config" +import { Global } from "../global" +// altimate_change end import { withTimeout } from "@/util/timeout" import { McpOAuthProvider } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" @@ -694,6 +698,31 @@ export namespace MCP { return state().then((state) => state.clients) } + // altimate_change start — persist enabled/disabled to disk so it survives session restarts + async function persistMcpEnabled(name: string, enabled: boolean): Promise { + try { + const paths = await findAllConfigPaths(Instance.directory, Global.Path.config) + for (const p of paths) { + const names = await listMcpInConfig(p) + if (names.includes(name)) { + const cfg = await Config.get() + const entry = cfg.mcp?.[name] + if (entry) + await addMcpToConfig( + name, + { ...entry, enabled } as Parameters[1], + p, + ) + log.info("persistMcpEnabled", { name, enabled, path: p }) + break + } + } + } catch (err) { + log.error("Failed to persist MCP enabled flag", { name, enabled, error: err }) + } + } + // altimate_change end + export async function connect(name: string) { const cfg = await Config.get() const config = cfg.mcp ?? {} @@ -732,6 +761,9 @@ export namespace MCP { s.clients[name] = result.mcpClient if (result.transport) s.transports[name] = result.transport } + // altimate_change start — persist enabled:true so it survives session restarts + await persistMcpEnabled(name, true) + // altimate_change end } export async function disconnect(name: string) { @@ -754,6 +786,9 @@ export namespace MCP { }) delete s.transports[name] s.status[name] = { status: "disabled" } + // altimate_change start — persist enabled:false so disable survives session restarts + await persistMcpEnabled(name, false) + // altimate_change end } /** Fully remove a dynamically-added MCP server — disconnects, and purges from runtime state. */