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
+33 -14
View File
@@ -1,6 +1,7 @@
import { existsSync } from "node:fs"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { resolveRuntimeExecutable, type RuntimeExecutable, type RuntimeExecutableResolver } from "./runtime-executable"
const SUBMODULE_REL = "packages/lsp-tools-mcp"
const DIST_CLI_REL = "dist/cli.js"
@@ -11,6 +12,9 @@ const LSP_BOOTSTRAP_SCRIPT = [
"const { join } = require('node:path')",
"const { spawnSync } = require('node:child_process')",
"const root = process.argv[1]",
"const git = process.argv[2] || 'git'",
"const npm = process.argv[3] || 'npm'",
"const bun = process.argv[4] || 'bun'",
"const submodule = join(root, 'packages/lsp-tools-mcp')",
"const dist = join(submodule, 'dist/cli.js')",
"const source = join(submodule, 'src/cli.ts')",
@@ -18,12 +22,12 @@ const LSP_BOOTSTRAP_SCRIPT = [
"const finish = (result) => { if (result.error) { console.error(result.error.message); process.exit(1) } process.exit(result.status ?? 1) }",
"const runIfAvailable = (command, args) => { const result = run(command, args, 'inherit'); if (result.error) return false; finish(result); return true }",
"if (existsSync(dist)) finish(run(process.execPath, [dist, 'mcp'], 'inherit'))",
"if (existsSync(source)) runIfAvailable('bun', [source, 'mcp'])",
"const submoduleResult = run('git', ['submodule', 'update', '--init', '--recursive', 'packages/lsp-tools-mcp'], ['ignore', 'ignore', 'inherit'])",
"if (existsSync(source)) runIfAvailable(bun, [source, 'mcp'])",
"const submoduleResult = run(git, ['submodule', 'update', '--init', '--recursive', 'packages/lsp-tools-mcp'], ['ignore', 'ignore', 'inherit'])",
"if (submoduleResult.error || submoduleResult.status !== 0) finish(submoduleResult)",
"if (existsSync(dist)) finish(run(process.execPath, [dist, 'mcp'], 'inherit'))",
"if (existsSync(source)) runIfAvailable('bun', [source, 'mcp'])",
"for (const [command, args] of [['npm', ['--prefix', submodule, 'install', '--no-package-lock', '--no-audit', '--no-fund']], ['npm', ['--prefix', submodule, 'run', 'build']]]) { const result = run(command, args, ['ignore', 'ignore', 'inherit']); if (result.error || result.status !== 0) finish(result) }",
"if (existsSync(source)) runIfAvailable(bun, [source, 'mcp'])",
"for (const [command, args] of [[npm, ['--prefix', submodule, 'install', '--no-package-lock', '--no-audit', '--no-fund']], [npm, ['--prefix', submodule, 'run', 'build']]]) { const result = run(command, args, ['ignore', 'ignore', 'inherit']); if (result.error || result.status !== 0) finish(result) }",
"finish(run(process.execPath, [dist, 'mcp'], 'inherit'))",
].join(";")
@@ -31,6 +35,7 @@ type LspMcpConfigOptions = {
readonly cwd?: string
readonly moduleUrl?: string
readonly exists?: (path: string) => boolean
readonly resolveExecutable?: RuntimeExecutableResolver
}
type LspCommandCandidate = {
@@ -52,29 +57,32 @@ function addAncestorCommandCandidates(
target: LspCommandCandidate[],
seenPaths: Set<string>,
pathExists: (path: string) => boolean,
resolveExecutable: RuntimeExecutableResolver,
): void {
let currentDirectory = resolve(startDirectory)
while (true) {
const distCliPath = resolve(currentDirectory, SUBMODULE_REL, DIST_CLI_REL)
if (!seenPaths.has(distCliPath)) {
const runtime = resolveJavaScriptRuntime(resolveExecutable)
seenPaths.add(distCliPath)
target.push({
command: ["node", distCliPath, "mcp"],
command: [runtime.command, distCliPath, "mcp"],
root: currentDirectory,
path: distCliPath,
exists: pathExists(distCliPath),
exists: runtime.available && pathExists(distCliPath),
})
}
const sourceCliPath = resolve(currentDirectory, SUBMODULE_REL, SOURCE_CLI_REL)
if (!seenPaths.has(sourceCliPath)) {
const runtime = resolveExecutable("bun")
seenPaths.add(sourceCliPath)
target.push({
command: ["bun", sourceCliPath, "mcp"],
command: [runtime.command, sourceCliPath, "mcp"],
root: currentDirectory,
path: sourceCliPath,
exists: pathExists(sourceCliPath),
exists: runtime.available && pathExists(sourceCliPath),
})
}
@@ -99,26 +107,37 @@ function findBootstrapRoot(candidates: readonly LspCommandCandidate[], pathExist
return candidates.find((candidate) => pathExists(resolve(candidate.root, "package.json")))?.root ?? process.cwd()
}
function createBootstrapCandidate(root: string): LspCommandCandidate {
function resolveJavaScriptRuntime(resolveExecutable: RuntimeExecutableResolver): RuntimeExecutable {
const node = resolveExecutable("node")
return node.available ? node : resolveExecutable("bun")
}
function createBootstrapCandidate(root: string, resolveExecutable: RuntimeExecutableResolver): LspCommandCandidate {
const runtime = resolveJavaScriptRuntime(resolveExecutable)
const bun = resolveExecutable("bun")
const git = resolveExecutable("git")
const npm = resolveExecutable("npm")
return {
command: ["node", "-e", LSP_BOOTSTRAP_SCRIPT, root],
command: [runtime.command, "-e", LSP_BOOTSTRAP_SCRIPT, root, git.command, npm.command, bun.command],
root,
path: resolve(root, SUBMODULE_REL, DIST_CLI_REL),
exists: true,
exists: runtime.available && git.available && npm.available,
}
}
function resolveLspCommand(options: LspMcpConfigOptions = {}): LspCommandCandidate {
const pathExists = options.exists ?? existsSync
const resolveExecutable = options.resolveExecutable ?? resolveRuntimeExecutable
const candidates: LspCommandCandidate[] = []
const seenPaths = new Set<string>()
const moduleDirectory = getModuleDirectory(options.moduleUrl ?? import.meta.url)
if (moduleDirectory) {
addAncestorCommandCandidates(moduleDirectory, candidates, seenPaths, pathExists)
addAncestorCommandCandidates(moduleDirectory, candidates, seenPaths, pathExists, resolveExecutable)
}
addAncestorCommandCandidates(options.cwd ?? process.cwd(), candidates, seenPaths, pathExists)
addAncestorCommandCandidates(options.cwd ?? process.cwd(), candidates, seenPaths, pathExists, resolveExecutable)
const distCandidate = candidates.find((candidate) => candidate.path.endsWith(DIST_CLI_REL) && candidate.exists)
if (distCandidate) {
@@ -130,7 +149,7 @@ function resolveLspCommand(options: LspMcpConfigOptions = {}): LspCommandCandida
return sourceCandidate
}
return createBootstrapCandidate(findBootstrapRoot(candidates, pathExists))
return createBootstrapCandidate(findBootstrapRoot(candidates, pathExists), resolveExecutable)
}
export function createLspMcpConfig(options: LspMcpConfigOptions = {}): LocalMcpConfig {