Merge pull request #4279 from MoerAI/fix/migrate-orphan-lsp-config-key

fix(migration): drop orphan 'lsp' config key so users see LSP moved to .opencode/lsp.json (fixes #4225)
This commit is contained in:
YeonGyu-Kim
2026-05-23 01:53:24 +09:00
committed by GitHub
3 changed files with 78 additions and 24 deletions
+13 -24
View File
@@ -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
@@ -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<string, unknown> = {
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<string, unknown>
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<string, unknown> = {
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<string, Record<string, unknown>>).sisyphus.model).toBe(
"anthropic/claude-opus-4-7",
)
})
})
+20
View File
@@ -105,6 +105,26 @@ 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`. 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<string, unknown>)
: []
log(
"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
needsWrite = true
}
if (copy.experimental && typeof copy.experimental === "object") {
const experimental = copy.experimental as Record<string, unknown>
if ("hashline_edit" in experimental) {