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
+41 -3
View File
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
import { join } from "node:path"
import { pathToFileURL } from "node:url"
import { createAstGrepMcpConfig } from "./ast-grep"
import type { RuntimeExecutable } from "./runtime-executable"
const temporaryDirectories: string[] = []
@@ -26,6 +27,7 @@ describe("createAstGrepMcpConfig", () => {
const unrelatedCwd = createTemporaryDirectory("omo-ast-grep-unrelated-cwd-")
const moduleFilePath = join(packageRoot, "dist", "index.js")
const cliPath = join(packageRoot, "packages", "ast-grep-mcp", "dist", "cli.js")
const nodePath = join(packageRoot, "bin", "node")
mkdirSync(join(packageRoot, "dist"), { recursive: true })
mkdirSync(join(packageRoot, "packages", "ast-grep-mcp", "dist"), { recursive: true })
writeFileSync(cliPath, "#!/usr/bin/env node\n", "utf-8")
@@ -34,10 +36,12 @@ describe("createAstGrepMcpConfig", () => {
const config = createAstGrepMcpConfig({
cwd: unrelatedCwd,
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({ node: nodePath }),
})
// then
expect(config.command).toEqual(["node", cliPath, "mcp"])
expect(config.enabled).toBe(true)
expect(config.command).toEqual([nodePath, cliPath, "mcp"])
expect(config.environment?.OMO_AST_GREP_WORKSPACE).toBe(unrelatedCwd)
})
@@ -46,6 +50,7 @@ describe("createAstGrepMcpConfig", () => {
const packageRoot = createTemporaryDirectory("omo-ast-grep-source-root-")
const moduleFilePath = join(packageRoot, "src", "mcp", "ast-grep.ts")
const sourceCliPath = join(packageRoot, "packages", "ast-grep-mcp", "src", "cli.ts")
const bunPath = join(packageRoot, "bin", "bun")
mkdirSync(join(packageRoot, "src", "mcp"), { recursive: true })
mkdirSync(join(packageRoot, "packages", "ast-grep-mcp", "src"), { recursive: true })
writeFileSync(sourceCliPath, "console.log('mcp')\n", "utf-8")
@@ -54,31 +59,55 @@ describe("createAstGrepMcpConfig", () => {
const config = createAstGrepMcpConfig({
cwd: createTemporaryDirectory("omo-ast-grep-source-cwd-"),
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({ bun: bunPath }),
})
// then
expect(config.command).toEqual(["bun", sourceCliPath, "mcp"])
expect(config.enabled).toBe(true)
expect(config.command).toEqual([bunPath, sourceCliPath, "mcp"])
})
it("still returns a built-in MCP config when the cli has not been built yet", () => {
// given
const packageRoot = createTemporaryDirectory("omo-ast-grep-missing-root-")
const moduleFilePath = join(packageRoot, "dist", "index.js")
const nodePath = join(packageRoot, "bin", "node")
mkdirSync(join(packageRoot, "dist"), { recursive: true })
// when
const config = createAstGrepMcpConfig({
cwd: createTemporaryDirectory("omo-ast-grep-missing-cwd-"),
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({ node: nodePath }),
})
// then
expect(config.enabled).toBe(true)
expect(config.command[0]).toBe("node")
expect(config.command[0]).toBe(nodePath)
expect(config.command[1]).toContain(join("packages", "ast-grep-mcp", "dist", "cli.js"))
expect(config.command[2]).toBe("mcp")
})
it("disables the MCP config when no runtime can launch ast-grep", () => {
// given
const packageRoot = createTemporaryDirectory("omo-ast-grep-no-runtime-root-")
const moduleFilePath = join(packageRoot, "dist", "index.js")
const cliPath = join(packageRoot, "packages", "ast-grep-mcp", "dist", "cli.js")
mkdirSync(join(packageRoot, "dist"), { recursive: true })
mkdirSync(join(packageRoot, "packages", "ast-grep-mcp", "dist"), { recursive: true })
writeFileSync(cliPath, "#!/usr/bin/env node\n", "utf-8")
// when
const config = createAstGrepMcpConfig({
cwd: createTemporaryDirectory("omo-ast-grep-no-runtime-cwd-"),
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({}),
})
// then
expect(config.enabled).toBe(false)
})
it("does not resolve the MCP command from the opened workspace", () => {
// given
const packageRoot = createTemporaryDirectory("omo-ast-grep-safe-package-root-")
@@ -93,6 +122,7 @@ describe("createAstGrepMcpConfig", () => {
const config = createAstGrepMcpConfig({
cwd: workspaceRoot,
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({ node: join(packageRoot, "bin", "node") }),
})
// then
@@ -111,9 +141,17 @@ describe("createAstGrepMcpConfig", () => {
cwd: createTemporaryDirectory("omo-ast-grep-disabled-cwd-"),
disabledTools: ["ast_grep_replace", "glob"],
moduleUrl: pathToFileURL(moduleFilePath).href,
resolveExecutable: createResolver({ node: join(packageRoot, "bin", "node") }),
})
// then
expect(config.environment?.OMO_AST_GREP_DISABLED_TOOLS).toBe("replace")
})
})
function createResolver(commands: Readonly<Record<string, string>>) {
return (commandName: string): RuntimeExecutable => {
const command = commands[commandName]
return command ? { command, available: true } : { command: commandName, available: false }
}
}