fix(mcp): resolve local mcp runtimes

Resolve built-in local MCP runtime executables before handing command arrays to OpenCode so lsp and ast_grep do not depend on a bare node or bun lookup in the host PATH.

Keep source, dist, bootstrap, workspace-safety, and disabled_mcps behavior covered by focused tests and real OpenCode MCP status QA.

Plan: plans/fix-built-in-mcp-runtime-executables.md
This commit is contained in:
YeonGyu-Kim
2026-05-20 23:32:48 +09:00
parent 37be0d5691
commit eea27d6f7e
8 changed files with 299 additions and 31 deletions
+60
View File
@@ -0,0 +1,60 @@
import { describe, expect, test } from "bun:test"
import { join } from "node:path"
import { resolveRuntimeExecutable } from "./runtime-executable"
describe("resolveRuntimeExecutable", () => {
test("#given lookup returns an absolute command #when resolving #then marks the executable available", () => {
// given
const nodePath = join("/tmp", "omo-runtime", "node")
// when
const result = resolveRuntimeExecutable("node", {
which: (commandName) => (commandName === "node" ? nodePath : null),
})
// then
expect(result).toEqual({ command: nodePath, available: true })
})
test("#given lookup misses #when resolving #then keeps the command unavailable", () => {
// given
const commandName = "definitely-not-installed"
// when
const result = resolveRuntimeExecutable(commandName, {
which: () => null,
execPath: join("/tmp", "omo-runtime", "bun"),
})
// then
expect(result).toEqual({ command: commandName, available: false })
})
test("#given an unsafe command name #when resolving #then does not trust the lookup result", () => {
// given
const unsafeName = "../node"
// when
const result = resolveRuntimeExecutable(unsafeName, {
which: () => join("/tmp", "omo-runtime", "node"),
execPath: join("/tmp", "omo-runtime", "node"),
})
// then
expect(result).toEqual({ command: unsafeName, available: false })
})
test("#given the host process is node #when resolving node #then uses process execPath before PATH lookup", () => {
// given
const nodePath = join("/tmp", "omo-runtime", "node")
// when
const result = resolveRuntimeExecutable("node", {
which: () => null,
execPath: nodePath,
})
// then
expect(result).toEqual({ command: nodePath, available: true })
})
})