fix(mcp): bootstrap lsp when cli is unavailable

Keep the built-in lsp MCP registered even when the submodule CLI artifact is missing.

The fallback command initializes the lsp-tools-mcp submodule, prefers the source CLI without dirtying the checkout with dist output, and keeps npm build as a last resort when Bun cannot run the source entrypoint.

Plan: plans/fix-lsp-mcp-missing-cli.md
This commit is contained in:
YeonGyu-Kim
2026-05-18 20:18:51 +09:00
parent f6fba0b154
commit 6fe2f72c76
5 changed files with 114 additions and 36 deletions
+27 -18
View File
@@ -1,13 +1,11 @@
/// <reference types="bun-types" />
import { afterEach, describe, expect, it, mock } from "bun:test"
import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { clearPluginConfigFileDetectionCache } from "../../../shared/jsonc-parser"
const originalCwd = process.cwd()
const originalOpenCodeConfigDir = process.env.OPENCODE_CONFIG_DIR
const temporaryDirectories: string[] = []
function createTemporaryDirectory(prefix: string): string {
@@ -16,16 +14,14 @@ function createTemporaryDirectory(prefix: string): string {
return directory
}
afterEach(() => {
mock.restore()
clearPluginConfigFileDetectionCache()
process.chdir(originalCwd)
function createLspDistCli(workspaceDirectory: string): void {
const lspDistDirectory = join(workspaceDirectory, "packages", "lsp-tools-mcp", "dist")
mkdirSync(lspDistDirectory, { recursive: true })
writeFileSync(join(lspDistDirectory, "cli.js"), "#!/usr/bin/env node\n", "utf-8")
}
if (originalOpenCodeConfigDir === undefined) {
delete process.env.OPENCODE_CONFIG_DIR
} else {
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
}
afterEach(() => {
clearPluginConfigFileDetectionCache()
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
@@ -39,36 +35,49 @@ describe("getInstalledLspServers", () => {
const workspaceDirectory = createTemporaryDirectory("omo-tools-lsp-workspace-")
const projectConfigDirectory = join(workspaceDirectory, ".opencode")
mkdirSync(projectConfigDirectory, { recursive: true })
createLspDistCli(workspaceDirectory)
writeFileSync(
join(projectConfigDirectory, "oh-my-openagent.json"),
JSON.stringify({ disabled_mcps: ["lsp"] }),
"utf-8",
)
process.env.OPENCODE_CONFIG_DIR = userConfigDirectory
process.chdir(workspaceDirectory)
clearPluginConfigFileDetectionCache()
const { getInstalledLspServers } = await import(`./tools-lsp?t=${Date.now()}-disabled`)
// when
const servers = getInstalledLspServers()
const servers = getInstalledLspServers({ configDirectory: userConfigDirectory, cwd: workspaceDirectory })
// then
expect(servers).toEqual([])
})
it("returns bundled lsp server info when lsp MCP uses bootstrap fallback", async () => {
// given
const userConfigDirectory = createTemporaryDirectory("omo-tools-lsp-user-")
const workspaceDirectory = createTemporaryDirectory("omo-tools-lsp-bootstrap-")
clearPluginConfigFileDetectionCache()
const { getInstalledLspServers } = await import(`./tools-lsp?t=${Date.now()}-bootstrap`)
// when
const servers = getInstalledLspServers({ configDirectory: userConfigDirectory, cwd: workspaceDirectory })
// then
expect(servers).toEqual([{ id: "lsp-tools-mcp", extensions: ["*"] }])
})
it("returns bundled lsp server info when MCP is enabled", async () => {
// given
const userConfigDirectory = createTemporaryDirectory("omo-tools-lsp-user-")
const workspaceDirectory = createTemporaryDirectory("omo-tools-lsp-enabled-")
process.env.OPENCODE_CONFIG_DIR = userConfigDirectory
process.chdir(workspaceDirectory)
createLspDistCli(workspaceDirectory)
clearPluginConfigFileDetectionCache()
const { getInstalledLspServers } = await import(`./tools-lsp?t=${Date.now()}-enabled`)
// when
const servers = getInstalledLspServers()
const servers = getInstalledLspServers({ configDirectory: userConfigDirectory, cwd: workspaceDirectory })
// then
expect(servers).toEqual([{ id: "lsp-tools-mcp", extensions: ["*"] }])
+11 -7
View File
@@ -7,7 +7,10 @@ type OmoConfigForDoctor = {
disabled_mcps?: string[]
}
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
type InstalledLspServersOptions = {
readonly configDirectory?: string
readonly cwd?: string
}
function readOmoConfig(configDirectory: string): OmoConfigForDoctor | null {
const detected = detectPluginConfigFile(configDirectory)
@@ -23,10 +26,11 @@ function readOmoConfig(configDirectory: string): OmoConfigForDoctor | null {
}
}
function isLspMcpDisabled(): boolean {
const userConfigDirectory = getOpenCodeConfigDir({ binary: "opencode" })
function isLspMcpDisabled(options: InstalledLspServersOptions): boolean {
const userConfigDirectory = options.configDirectory ?? getOpenCodeConfigDir({ binary: "opencode" })
const projectConfigDirectory = join(options.cwd ?? process.cwd(), ".opencode")
const userConfig = readOmoConfig(userConfigDirectory)
const projectConfig = readOmoConfig(PROJECT_CONFIG_DIR)
const projectConfig = readOmoConfig(projectConfigDirectory)
const disabledMcps = new Set<string>([
...(userConfig?.disabled_mcps ?? []),
@@ -36,12 +40,12 @@ function isLspMcpDisabled(): boolean {
return disabledMcps.has("lsp")
}
export function getInstalledLspServers(): Array<{ id: string; extensions: string[] }> {
if (isLspMcpDisabled()) {
export function getInstalledLspServers(options: InstalledLspServersOptions = {}): Array<{ id: string; extensions: string[] }> {
if (isLspMcpDisabled(options)) {
return []
}
const lspMcpConfig = createLspMcpConfig()
const lspMcpConfig = createLspMcpConfig({ cwd: options.cwd })
return lspMcpConfig.enabled ? [{ id: "lsp-tools-mcp", extensions: ["*"] }] : []
}