From a7429cc22382100ad979564f8a0ed07a4fccec3b Mon Sep 17 00:00:00 2001 From: MoerAI Date: Fri, 22 May 2026 16:06:19 +0900 Subject: [PATCH 1/2] fix(migration): drop orphan 'lsp' config key so users see LSP moved to .opencode/lsp.json (fixes #4225) The lsp config block was removed from OhMyOpenCodeConfigSchema when LSP tools were migrated from native plugin tools to the lsp tier-1 MCP server (packages/lsp-tools-mcp). Zod v4 strips unknown keys silently on safeParse, so a v3-era oh-my-opencode.jsonc with 'lsp': { typescript: { command: ... } } continues to live in the file unchanged while doing absolutely nothing. The user reporting #4225 saw their custom LSP servers stop working with zero indication that the configuration site moved.\n\nAdd a migrator that removes the orphan lsp key during migrateConfigFile, mirroring the existing omo_agent -> sisyphus_agent migration immediately above and the 'Removed obsolete hooks from disabled_hooks' precedent below. A single log line records the configPath and the list of dropped server keys so the user has a paper trail in oh-my-opencode.log, and needsWrite is flipped so the cleanup persists to disk (with a timestamped backup) the next time the plugin loads.\n\nReproduction (clean upstream/dev, BEFORE fix):\n needsWrite=false\n inMemory.lsp=\n persisted.lsp=\n\nVerification (AFTER fix):\n needsWrite=true\n inMemory.lsp=undefined\n persisted.lsp=undefined\n\nbun test src/shared/migration/ -> 26/26 pass (24/24 pre-existing + 2 new regression tests). bun run typecheck -> exit 0. --- src/shared/migration/config-migration.test.ts | 45 +++++++++++++++++++ src/shared/migration/config-migration.ts | 18 ++++++++ 2 files changed, 63 insertions(+) diff --git a/src/shared/migration/config-migration.test.ts b/src/shared/migration/config-migration.test.ts index 5c41f8435..84e2d1370 100644 --- a/src/shared/migration/config-migration.test.ts +++ b/src/shared/migration/config-migration.test.ts @@ -199,3 +199,48 @@ describe("migrateConfigFile backup skipping", () => { expect(backupFiles.length).toBe(1) }) }) + +describe("migrateConfigFile orphan lsp key", () => { + test("removes the obsolete 'lsp' key from rawConfig and from the persisted file", () => { + // given - a v3-era config with a populated lsp block that the v4 schema silently strips + const workdir = createWorkdir() + const configPath = join(workdir, "oh-my-opencode.json") + const rawConfig: Record = { + lsp: { + typescript: { command: ["typescript-language-server", "--stdio"] }, + rust: { command: ["rust-analyzer"] }, + }, + } + writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n") + + // when + const needsWrite = migrateConfigFile(configPath, rawConfig) + + // then - the in-memory config and the persisted file have both lost the lsp key + expect(needsWrite).toBe(true) + expect(rawConfig.lsp).toBeUndefined() + const persistedConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record + expect(persistedConfig.lsp).toBeUndefined() + }) + + test("leaves the config alone when no 'lsp' key is present", () => { + // given - a config that never had an lsp block + const workdir = createWorkdir() + const configPath = join(workdir, "oh-my-opencode.json") + const rawConfig: Record = { + agents: { + sisyphus: { model: "anthropic/claude-opus-4-7" }, + }, + } + writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n") + + // when + const needsWrite = migrateConfigFile(configPath, rawConfig) + + // then - no rewrite triggered by the lsp migrator, agents block untouched + expect(needsWrite).toBe(false) + expect((rawConfig.agents as Record>).sisyphus.model).toBe( + "anthropic/claude-opus-4-7", + ) + }) +}) diff --git a/src/shared/migration/config-migration.ts b/src/shared/migration/config-migration.ts index 5c0ed2d87..f5c1e957f 100644 --- a/src/shared/migration/config-migration.ts +++ b/src/shared/migration/config-migration.ts @@ -105,6 +105,24 @@ export function migrateConfigFile( needsWrite = true } + // The legacy `lsp` config key was retired when LSP moved from native plugin + // tools to the `lsp` MCP server backed by `packages/lsp-tools-mcp`. Custom + // LSP servers are now configured via `.opencode/lsp.json` (project) or + // `~/.codex/lsp-client.json` (user). The Zod schema strips unknown keys + // silently, so without this migration a stale `lsp` block lingers in the + // user's config file with no signal that it has stopped doing anything. + if (copy.lsp !== undefined) { + const droppedServers = copy.lsp && typeof copy.lsp === "object" + ? Object.keys(copy.lsp as Record) + : [] + log( + "Removed obsolete 'lsp' config key from config file. LSP servers are now configured via .opencode/lsp.json -- see docs/reference/configuration.md for the new location.", + { configPath, droppedServers }, + ) + delete copy.lsp + needsWrite = true + } + if (copy.experimental && typeof copy.experimental === "object") { const experimental = copy.experimental as Record if ("hashline_edit" in experimental) { From 6062df8262ad6e31055420c393f5a9b8a1f5abe4 Mon Sep 17 00:00:00 2001 From: MoerAI Date: Fri, 22 May 2026 18:10:12 +0900 Subject: [PATCH 2/2] fix(migration): make 'lsp' migration guidance self-contained and update stale docs (addresses codex P2 on #4279) The migration log message previously pointed users to docs/reference/configuration.md for the new LSP config location, but that doc section still showed the obsolete plugin-level 'lsp' block. A user following the guidance would re-add the same 'lsp' key, see it stripped again on next startup, and never reach a usable config.\n\nFix both sides: rewrite the log message so it is self-contained (states the new path .opencode/lsp.json and the consumer directly) and rewrite the LSP section in docs/reference/configuration.md to describe the actual current architecture (LSP served by the 'lsp' MCP server, reading server map from .opencode/lsp.json via LSP_TOOLS_MCP_PROJECT_CONFIG, schema lives in packages/lsp-tools-mcp).\n\nVerification: bun test src/shared/migration/ -> 26/26 pass. bun run typecheck -> exit 0. Manual probe -> migration still strips lsp from both in-memory and persisted file. --- docs/reference/configuration.md | 37 +++++++++--------------- src/shared/migration/config-migration.ts | 14 +++++---- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index f5fa6753a..424fb0e89 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -628,34 +628,23 @@ Built-in MCPs (enabled by default): `websearch` (Exa AI), `context7` (library do ### LSP -Configure Language Server Protocol integration: +LSP tools are served by the built-in `lsp` MCP server (see [MCPs](#mcps)). The +previous top-level `"lsp"` block in the plugin config is no longer read and is +automatically stripped on next startup; existing configs containing it are +silently migrated (see `src/shared/migration/config-migration.ts`). + +To configure custom language servers, create `.opencode/lsp.json` at the project +root. The MCP server is launched with `LSP_TOOLS_MCP_PROJECT_CONFIG=.opencode/lsp.json` +and reads the server map from that file. The schema lives in the +`packages/lsp-tools-mcp` submodule (upstream: +[code-yeongyu/lsp-tools-mcp](https://github.com/code-yeongyu/lsp-tools-mcp)). + +To disable the LSP MCP entirely: ```json -{ - "lsp": { - "typescript-language-server": { - "command": ["typescript-language-server", "--stdio"], - "extensions": [".ts", ".tsx"], - "priority": 10, - "env": { "NODE_OPTIONS": "--max-old-space-size=4096" }, - "initialization": { - "preferences": { "includeInlayParameterNameHints": "all" } - } - }, - "pylsp": { "disabled": true } - } -} +{ "disabled_mcps": ["lsp"] } ``` -| Option | Type | Description | -| ---------------- | ------- | ------------------------------------ | -| `command` | array | Command to start LSP server | -| `extensions` | array | File extensions (e.g. `[".ts"]`) | -| `priority` | number | Priority when multiple servers match | -| `env` | object | Environment variables | -| `initialization` | object | Init options passed to server | -| `disabled` | boolean | Disable this server | - --- ## Advanced diff --git a/src/shared/migration/config-migration.ts b/src/shared/migration/config-migration.ts index f5c1e957f..3ba81fca0 100644 --- a/src/shared/migration/config-migration.ts +++ b/src/shared/migration/config-migration.ts @@ -106,17 +106,19 @@ export function migrateConfigFile( } // The legacy `lsp` config key was retired when LSP moved from native plugin - // tools to the `lsp` MCP server backed by `packages/lsp-tools-mcp`. Custom - // LSP servers are now configured via `.opencode/lsp.json` (project) or - // `~/.codex/lsp-client.json` (user). The Zod schema strips unknown keys - // silently, so without this migration a stale `lsp` block lingers in the - // user's config file with no signal that it has stopped doing anything. + // tools to the `lsp` MCP server backed by `packages/lsp-tools-mcp`. The + // server now reads its server map from `.opencode/lsp.json` in the project + // root (path is hard-coded in `src/mcp/lsp.ts` via the + // `LSP_TOOLS_MCP_PROJECT_CONFIG` env var passed to the stdio MCP). The Zod + // schema strips unknown keys silently, so without this migration a stale + // `lsp` block lingers in the user's config file with no signal that it has + // stopped doing anything. if (copy.lsp !== undefined) { const droppedServers = copy.lsp && typeof copy.lsp === "object" ? Object.keys(copy.lsp as Record) : [] log( - "Removed obsolete 'lsp' config key from config file. LSP servers are now configured via .opencode/lsp.json -- see docs/reference/configuration.md for the new location.", + "Removed obsolete 'lsp' config key from oh-my-opencode config. Custom LSP servers are now configured in .opencode/lsp.json at the project root (consumed by the 'lsp' MCP server). Move any server definitions there to restore them.", { configPath, droppedServers }, ) delete copy.lsp