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:
+33
-14
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user