Restart a crashed language server per --restart never|always|N

The process is supervised: on exit after a successful initialize it is
spawned again with exponential backoff (500ms to 30s), open documents are
re-sent as didOpen before the new session is announced, and requests caught
by the crash wait for it and retry. A crash more than a minute after the
previous one starts a fresh streak; reaching the streak limit (default 3)
ends restarting for good, after which every request fails with an LspError
saying so. Restarts and give-ups are logged and forwarded to the MCP client.

Writes to the child's stdin go through a PassThrough so a broken pipe ends
the session instead of surfacing as an unhandled rejection.
This commit is contained in:
EFLKumo committed 2026-09-20 15:36:30 +08:00
1 parent a89f862f73
commit 1463527870
6 files changed
+441 -146

No files matched your search

+22
View File
@@ -10,6 +10,14 @@ import { Argument, Command, Flag } from "effect/unstable/cli";
import * as Server from "./Server.ts";
const Json = Flag.withSchema(Schema.fromJsonString(Schema.Json));
const crashes = { message: "expected a whole number of crashes" };
const Restart = Schema.Union([
Schema.Literals(["never", "always"]),
Schema.NumberFromString.check(
Schema.isInt(crashes),
Schema.isGreaterThanOrEqualTo(0, crashes),
),
]);
const command = Command.make(
"lsp-mcp",
@@ -58,6 +66,14 @@ const command = Command.make(
),
Flag.withDefault(32),
),
restart: Flag.String("restart").pipe(
Flag.withSchema(Restart),
Flag.withMetavar("never|always|N"),
Flag.withDescription(
"When the language server crashes: never start it again, always start it again, or give up after N crashes in a row (default 3)",
),
Flag.withDefault(3),
),
command: Argument.String("server").pipe(
Argument.withDescription("Language server executable"),
),
@@ -75,6 +91,7 @@ const command = Command.make(
settings,
languages,
capacity,
restart,
command,
args,
}) {
@@ -89,6 +106,7 @@ const command = Command.make(
root: path.resolve(root),
initializationOptions: Option.getOrUndefined(initOptions),
settings: Option.getOrUndefined(settings),
restart,
},
documents: { capacity, languages },
}),
@@ -111,6 +129,10 @@ const command = Command.make(
command: `lsp-mcp --settings '{"gopls":{"staticcheck":true}}' -- gopls`,
description: "Go with workspace settings",
},
{
command: "lsp-mcp --restart always -- clangd",
description: "Keep bringing the server back, however often it crashes",
},
]),
);