chore(omo-claude): re-sync vendored lsp with upstream lazy-mcp backend

sync-components pulled the upstream codex-lsp lazy-start MCP backend (lazy-*
modules + cli.ts) into the vendored lsp component. Rebuilds + F3 cache boot verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
YeonGyu-Kim
2026-05-29 13:50:17 +09:00
parent a548707699
commit 6f9dfb080d
6 changed files with 629 additions and 2 deletions
@@ -2,8 +2,8 @@
import { argv, stderr } from "node:process";
import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
import { runMcpStdioServer } from "@code-yeongyu/lsp-tools-mcp/dist/mcp.js";
import { runPostToolUseHookCli } from "./codex-hook.js";
import { runLazyLspMcpServer } from "./lazy-lsp-mcp.js";
async function main(): Promise<void> {
const [command = "mcp", subcommand = ""] = argv.slice(2);
@@ -15,7 +15,7 @@ async function main(): Promise<void> {
}
if (command === "mcp") {
await runMcpStdioServer();
await runLazyLspMcpServer();
return;
}
@@ -0,0 +1,71 @@
import { createRequire } from "node:module";
import { env, execPath, stderr } from "node:process";
import { LSP_MCP_TOOLS } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
import {
createLazyMcpProxy,
DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS,
type LazyMcpBackendProcessConfig,
type McpToolDescriptor,
resolveLazyLspBackendConfig,
} from "./lazy-mcp-proxy.js";
import { createStdioLazyMcpBackend } from "./lazy-mcp-stdio-backend.js";
import { type LazyMcpLifecycleLog, runLazyMcpStdioServer } from "./lazy-mcp-stdio-server.js";
const require = createRequire(import.meta.url);
const BACKEND_CONFIG_ENV = "CODEX_LSP_LAZY_BACKEND";
const IDLE_TIMEOUT_ENV = "CODEX_LSP_LAZY_IDLE_TIMEOUT_MS";
export interface LazyLspIdleTimeoutResolution {
readonly value: number;
readonly warning?: string;
}
export async function runLazyLspMcpServer(
input: NodeJS.ReadableStream = process.stdin,
output: NodeJS.WritableStream = process.stdout,
): Promise<void> {
const fallback = defaultLazyLspBackendConfig();
const resolved = resolveLazyLspBackendConfig(env[BACKEND_CONFIG_ENV], fallback);
if (resolved.warning !== undefined) stderr.write(`${resolved.warning}\n`);
const idleTimeout = resolveLazyLspIdleTimeoutMs(env[IDLE_TIMEOUT_ENV], DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS);
if (idleTimeout.warning !== undefined) stderr.write(`${idleTimeout.warning}\n`);
const log: LazyMcpLifecycleLog = (event, fields = {}) => {
stderr.write(`[codex-lsp lazy-mcp] ${event} ${JSON.stringify(fields)}\n`);
};
const proxy = createLazyMcpProxy({
backend: createStdioLazyMcpBackend(resolved.config),
idleTimeoutMs: idleTimeout.value,
log,
serverName: "lsp",
serverVersion: "0.2.0",
toolDescriptors: lspToolDescriptors(),
});
await runLazyMcpStdioServer(proxy, input, output, { log });
}
export function defaultLazyLspBackendConfig(): LazyMcpBackendProcessConfig {
return {
command: execPath,
args: [require.resolve("@code-yeongyu/lsp-tools-mcp/dist/cli.js"), "mcp"],
};
}
export function resolveLazyLspIdleTimeoutMs(
rawValue: string | undefined,
fallback: number,
): LazyLspIdleTimeoutResolution {
if (rawValue === undefined || rawValue.trim() === "") return { value: fallback };
const parsed = Number(rawValue);
if (Number.isInteger(parsed) && parsed >= 0) return { value: parsed };
return { value: fallback, warning: `Ignoring malformed lazy MCP idle timeout: ${rawValue}` };
}
export function lspToolDescriptors(): readonly McpToolDescriptor[] {
return LSP_MCP_TOOLS.map((tool) => ({
name: tool.name,
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema,
}));
}
@@ -0,0 +1,78 @@
export const DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS = 10 * 60_000;
export type JsonRpcId = string | number | null;
export type LazyMcpTimer = {
unref?: () => void;
};
export interface LazyMcpClock {
setTimeout(callback: () => void, delayMs: number): LazyMcpTimer;
clearTimeout(timer: LazyMcpTimer): void;
}
export interface TextContent {
readonly type: "text";
readonly text: string;
}
export interface McpToolDescriptor {
readonly name: string;
readonly title?: string;
readonly description?: string;
readonly inputSchema: unknown;
}
export interface JsonRpcRequest {
readonly jsonrpc?: "2.0";
readonly id?: JsonRpcId;
readonly method?: string;
readonly params?: unknown;
}
export interface JsonRpcError {
readonly code: number;
readonly message: string;
readonly data?: unknown;
}
export interface JsonRpcResult {
readonly capabilities?: Record<string, unknown>;
readonly serverInfo?: Record<string, unknown>;
readonly protocolVersion?: string;
readonly tools?: readonly McpToolDescriptor[];
readonly content?: readonly TextContent[];
readonly isError?: boolean;
readonly [key: string]: unknown;
}
export interface JsonRpcResponse {
readonly jsonrpc: "2.0";
readonly id: JsonRpcId;
readonly result?: JsonRpcResult;
readonly error?: JsonRpcError;
}
export function successResponse(id: JsonRpcId, result: JsonRpcResult): JsonRpcResponse {
return { jsonrpc: "2.0", id, result };
}
export function errorResponse(id: JsonRpcId, code: number, message: string, data?: unknown): JsonRpcResponse {
return { jsonrpc: "2.0", id, error: data === undefined ? { code, message } : { code, message, data } };
}
export function jsonRpcId(value: unknown): JsonRpcId {
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
}
export function requestedProtocolVersion(params: unknown): string {
if (!isRecord(params) || typeof params["protocolVersion"] !== "string") return "2024-11-05";
return params["protocolVersion"];
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function messageFromError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
@@ -0,0 +1,264 @@
import {
DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS,
errorResponse,
isRecord,
type JsonRpcId,
type JsonRpcRequest,
type JsonRpcResponse,
jsonRpcId,
type LazyMcpClock,
type LazyMcpTimer,
type McpToolDescriptor,
messageFromError,
requestedProtocolVersion,
successResponse,
} from "./lazy-mcp-protocol.js";
export type {
JsonRpcRequest,
JsonRpcResponse,
LazyMcpClock,
LazyMcpTimer,
McpToolDescriptor,
} from "./lazy-mcp-protocol.js";
export { DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS } from "./lazy-mcp-protocol.js";
export interface LazyMcpConnection {
readonly closed: Promise<void>;
request(request: JsonRpcRequest): Promise<JsonRpcResponse | undefined>;
stop(): Promise<void>;
}
export interface LazyMcpBackend {
start(): Promise<LazyMcpConnection>;
}
export interface LazyMcpBackendProcessConfig {
readonly command: string;
readonly args: readonly string[];
readonly cwd?: string;
readonly env?: Readonly<Record<string, string>>;
}
export interface LazyMcpBackendConfigResolution {
readonly config: LazyMcpBackendProcessConfig;
readonly warning?: string;
}
export interface LazyMcpProxy {
handleRequest(input: unknown): Promise<JsonRpcResponse | undefined>;
stopActiveBackend(): Promise<void>;
hasActiveBackend(): boolean;
}
export interface LazyMcpProxyOptions {
readonly backend: LazyMcpBackend;
readonly clock?: LazyMcpClock;
readonly idleTimeoutMs?: number;
readonly log?: (event: string, fields?: Record<string, string | number | boolean | null>) => void;
readonly serverName?: string;
readonly serverVersion?: string;
readonly toolDescriptors: readonly McpToolDescriptor[];
}
const defaultClock: LazyMcpClock = {
setTimeout: (callback, delayMs) => createDefaultTimer(callback, delayMs),
clearTimeout: (timer) => {
if (isDefaultTimer(timer)) clearTimeout(timer.nodeTimer);
},
};
export function createLazyMcpProxy(options: LazyMcpProxyOptions): LazyMcpProxy {
return new LazyMcpProxyState(options);
}
export function resolveLazyLspBackendConfig(
rawConfig: string | undefined,
fallback: LazyMcpBackendProcessConfig,
): LazyMcpBackendConfigResolution {
if (rawConfig === undefined || rawConfig.trim() === "") return { config: fallback };
try {
const parsed: unknown = JSON.parse(rawConfig);
if (isBackendProcessConfig(parsed)) return { config: parsed };
return malformedConfig(fallback, "config shape is invalid");
} catch (error) {
return malformedConfig(fallback, messageFromError(error));
}
}
class LazyMcpProxyState implements LazyMcpProxy {
private activeConnection: LazyMcpConnection | undefined;
private idleTimer: LazyMcpTimer | undefined;
private starting: Promise<LazyMcpConnection> | undefined;
private readonly backend: LazyMcpBackend;
private readonly clock: LazyMcpClock;
private readonly idleTimeoutMs: number;
private readonly log: NonNullable<LazyMcpProxyOptions["log"]>;
private readonly serverName: string;
private readonly serverVersion: string;
private readonly toolDescriptors: readonly McpToolDescriptor[];
constructor(options: LazyMcpProxyOptions) {
this.backend = options.backend;
this.clock = options.clock ?? defaultClock;
this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS;
this.log = options.log ?? (() => {});
this.serverName = options.serverName ?? "lsp";
this.serverVersion = options.serverVersion ?? "0.1.0";
this.toolDescriptors = options.toolDescriptors;
}
async handleRequest(input: unknown): Promise<JsonRpcResponse | undefined> {
if (!isRecord(input)) return errorResponse(null, -32600, "Invalid Request");
const id = jsonRpcId(input["id"]);
const method = input["method"];
if (method === "notifications/initialized") return undefined;
if (method === "ping") return successResponse(id, {});
if (method === "initialize") return this.initialize(id, input["params"]);
if (method === "tools/list") return successResponse(id, { tools: this.toolDescriptors });
if (method === "resources/list") return successResponse(id, { resources: [] });
if (method === "resources/templates/list") return successResponse(id, { resourceTemplates: [] });
if (method === "tools/call") return this.handleToolCall(id, input);
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
}
async stopActiveBackend(): Promise<void> {
this.clearIdleTimer();
const connection = this.activeConnection;
this.activeConnection = undefined;
if (connection !== undefined) {
await connection.stop();
this.log("lazy_backend_stopped");
}
}
hasActiveBackend(): boolean {
return this.activeConnection !== undefined;
}
private initialize(id: JsonRpcId, params: unknown): JsonRpcResponse {
return successResponse(id, {
capabilities: { tools: { listChanged: false } },
serverInfo: { name: this.serverName, version: this.serverVersion },
protocolVersion: requestedProtocolVersion(params),
});
}
private async handleToolCall(id: JsonRpcId, request: Record<string, unknown>): Promise<JsonRpcResponse> {
try {
const connection = await this.getConnection();
const response = await connection.request({
jsonrpc: "2.0",
id,
method: "tools/call",
params: request["params"],
});
this.armIdleTimer();
return response === undefined
? errorResponse(id, -32603, "Lazy MCP backend returned no response")
: withId(response, id);
} catch (error) {
return successResponse(id, {
content: [{ type: "text", text: messageFromError(error) }],
isError: true,
});
}
}
private async getConnection(): Promise<LazyMcpConnection> {
if (this.activeConnection !== undefined) return this.activeConnection;
if (this.starting !== undefined) return this.starting;
const starting = this.startBackend();
this.starting = starting;
return starting;
}
private async startBackend(): Promise<LazyMcpConnection> {
try {
this.log("lazy_backend_starting");
const connection = await this.backend.start();
await connection.request({
jsonrpc: "2.0",
id: "lazy-mcp-initialize",
method: "initialize",
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: this.serverName } },
});
this.activeConnection = connection;
this.observeClose(connection);
this.log("lazy_backend_started");
return connection;
} finally {
this.starting = undefined;
}
}
private observeClose(connection: LazyMcpConnection): void {
void connection.closed.then(
() => {
if (this.activeConnection !== connection) return;
this.activeConnection = undefined;
this.clearIdleTimer();
this.log("lazy_backend_stopped");
},
(error: unknown) => {
this.log("lazy_backend_close_error", { message: messageFromError(error) });
},
);
}
private armIdleTimer(): void {
this.clearIdleTimer();
if (this.idleTimeoutMs <= 0) return;
const timer = this.clock.setTimeout(() => {
this.log("lazy_backend_idle_timeout", { idle_timeout_ms: this.idleTimeoutMs });
void this.stopActiveBackend().catch((error: unknown) => {
this.log("lazy_backend_idle_stop_error", { message: messageFromError(error) });
});
}, this.idleTimeoutMs);
timer.unref?.();
this.idleTimer = timer;
}
private clearIdleTimer(): void {
if (this.idleTimer === undefined) return;
this.clock.clearTimeout(this.idleTimer);
this.idleTimer = undefined;
}
}
function malformedConfig(fallback: LazyMcpBackendProcessConfig, reason: string): LazyMcpBackendConfigResolution {
return { config: fallback, warning: `Ignoring malformed lazy MCP backend config: ${reason}` };
}
function isBackendProcessConfig(value: unknown): value is LazyMcpBackendProcessConfig {
if (!isRecord(value) || typeof value["command"] !== "string" || !isStringArray(value["args"])) return false;
const cwd = value["cwd"];
if (cwd !== undefined && typeof cwd !== "string") return false;
const env = value["env"];
return env === undefined || (isRecord(env) && Object.values(env).every((entry) => typeof entry === "string"));
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
}
function withId(response: JsonRpcResponse, id: JsonRpcId): JsonRpcResponse {
if (response.id === id) return response;
if (response.error !== undefined) return { jsonrpc: "2.0", id, error: response.error };
if (response.result !== undefined) return { jsonrpc: "2.0", id, result: response.result };
return { jsonrpc: "2.0", id };
}
interface DefaultTimer extends LazyMcpTimer {
readonly nodeTimer: ReturnType<typeof setTimeout>;
}
function createDefaultTimer(callback: () => void, delayMs: number): DefaultTimer {
const nodeTimer = setTimeout(callback, delayMs);
return { nodeTimer, unref: () => nodeTimer.unref() };
}
function isDefaultTimer(timer: LazyMcpTimer): timer is DefaultTimer {
return isRecord(timer) && "nodeTimer" in timer;
}
@@ -0,0 +1,158 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { once } from "node:events";
import { createInterface } from "node:readline";
import {
isRecord,
type JsonRpcId,
type JsonRpcRequest,
type JsonRpcResponse,
type JsonRpcResult,
jsonRpcId,
messageFromError,
} from "./lazy-mcp-protocol.js";
import type { LazyMcpBackend, LazyMcpBackendProcessConfig, LazyMcpConnection } from "./lazy-mcp-proxy.js";
const FORCE_KILL_AFTER_MS = 1_000;
interface PendingRequest {
readonly originalId: JsonRpcId;
readonly reject: (error: Error) => void;
readonly resolve: (response: JsonRpcResponse | undefined) => void;
}
export function createStdioLazyMcpBackend(config: LazyMcpBackendProcessConfig): LazyMcpBackend {
return {
start: async () => startStdioConnection(config),
};
}
async function startStdioConnection(config: LazyMcpBackendProcessConfig): Promise<LazyMcpConnection> {
const child = spawnBackend(config);
return new StdioLazyMcpConnection(child);
}
class StdioLazyMcpConnection implements LazyMcpConnection {
readonly closed: Promise<void>;
private closedState = false;
private nextRequestId = 1;
private readonly pending = new Map<string, PendingRequest>();
constructor(private readonly child: ChildProcessWithoutNullStreams) {
this.closed = new Promise((resolve) => {
const finish = (error?: Error) => {
if (this.closedState) return;
this.closedState = true;
this.rejectPending(error ?? new Error("Lazy MCP backend exited"));
resolve();
};
child.once("exit", () => finish());
child.once("error", (error) => finish(error));
});
this.consumeStdout();
child.stderr.on("data", (chunk: Buffer) => {
process.stderr.write(chunk);
});
}
async request(request: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {
if (this.closedState) throw new Error("Lazy MCP backend is not running");
const upstreamId = `lazy-${this.nextRequestId}`;
this.nextRequestId++;
const upstreamRequest = { ...request, id: upstreamId };
const response = new Promise<JsonRpcResponse | undefined>((resolve, reject) => {
this.pending.set(upstreamId, { originalId: request.id ?? null, resolve, reject });
});
await this.writeLine(`${JSON.stringify(upstreamRequest)}\n`);
return response;
}
async stop(): Promise<void> {
if (this.closedState) return;
this.child.kill("SIGTERM");
const forceKill = setTimeout(() => {
if (!this.closedState) this.child.kill("SIGKILL");
}, FORCE_KILL_AFTER_MS);
forceKill.unref();
try {
await this.closed;
} finally {
clearTimeout(forceKill);
}
}
private consumeStdout(): void {
const lines = createInterface({ input: this.child.stdout, crlfDelay: Number.POSITIVE_INFINITY });
void (async () => {
try {
for await (const line of lines) {
this.handleLine(line);
}
} catch (error) {
this.rejectPending(new Error(`Lazy MCP backend stdout failed: ${messageFromError(error)}`));
}
})();
}
private handleLine(line: string): void {
if (!line.trim()) return;
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch (error) {
this.rejectPending(new Error(`Lazy MCP backend emitted invalid JSON: ${messageFromError(error)}`));
return;
}
if (!isRecord(parsed)) return;
const id = jsonRpcId(parsed["id"]);
const pending = id === null ? undefined : this.pending.get(String(id));
if (pending === undefined) return;
this.pending.delete(String(id));
pending.resolve(withOriginalId(parsed, pending.originalId));
}
private async writeLine(line: string): Promise<void> {
if (this.child.stdin.write(line)) return;
await once(this.child.stdin, "drain");
}
private rejectPending(error: Error): void {
for (const pending of this.pending.values()) {
pending.reject(error);
}
this.pending.clear();
}
}
function spawnBackend(config: LazyMcpBackendProcessConfig): ChildProcessWithoutNullStreams {
const env = config.env === undefined ? process.env : { ...process.env, ...config.env };
const stdio: ["pipe", "pipe", "pipe"] = ["pipe", "pipe", "pipe"];
if (config.cwd === undefined) {
return spawn(config.command, [...config.args], { env, stdio });
}
return spawn(config.command, [...config.args], { cwd: config.cwd, env, stdio });
}
function withOriginalId(value: Record<string, unknown>, id: JsonRpcId): JsonRpcResponse | undefined {
const jsonrpc = value["jsonrpc"];
if (jsonrpc !== "2.0") return undefined;
const result = value["result"];
const error = value["error"];
if (isRecord(error) && typeof error["code"] === "number" && typeof error["message"] === "string") {
return { jsonrpc, id, error: optionalErrorData(error) };
}
return isJsonRpcResult(result) ? { jsonrpc, id, result } : { jsonrpc, id };
}
function optionalErrorData(error: Record<string, unknown>): { code: number; message: string; data?: unknown } {
const code = error["code"];
const message = error["message"];
if (typeof code !== "number" || typeof message !== "string") return { code: -32603, message: "Invalid MCP error" };
if (!("data" in error)) return { code, message };
return { code, message, data: error["data"] };
}
function isJsonRpcResult(value: unknown): value is JsonRpcResult {
return isRecord(value);
}
@@ -0,0 +1,56 @@
import { createInterface } from "node:readline";
import { errorResponse, isRecord, type JsonRpcResponse, jsonRpcId, messageFromError } from "./lazy-mcp-protocol.js";
import type { LazyMcpProxy } from "./lazy-mcp-proxy.js";
export type LazyMcpLifecycleLog = (event: string, fields?: Record<string, string | number | boolean | null>) => void;
export interface LazyMcpStdioServerOptions {
readonly log?: LazyMcpLifecycleLog;
}
const noopLog: LazyMcpLifecycleLog = () => {};
export async function runLazyMcpStdioServer(
proxy: LazyMcpProxy,
input: NodeJS.ReadableStream = process.stdin,
output: NodeJS.WritableStream = process.stdout,
options: LazyMcpStdioServerOptions = {},
): Promise<void> {
const log = options.log ?? noopLog;
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
log("lazy_proxy_stdio_started", { cwd: process.cwd() });
try {
for await (const line of lines) {
if (!line.trim()) continue;
const response = await handleLine(proxy, line, log);
if (response !== undefined) output.write(`${JSON.stringify(response)}\n`);
}
} finally {
await proxy.stopActiveBackend();
log("lazy_proxy_stdio_stopped");
}
}
async function handleLine(
proxy: LazyMcpProxy,
line: string,
log: LazyMcpLifecycleLog,
): Promise<JsonRpcResponse | undefined> {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch (error) {
const message = messageFromError(error);
log("lazy_proxy_parse_error", { message });
return errorResponse(null, -32700, "Parse error", message);
}
const id = isRecord(parsed) ? jsonRpcId(parsed["id"]) : null;
const method = isRecord(parsed) && typeof parsed["method"] === "string" ? parsed["method"] : null;
log("lazy_proxy_request", { id: id === null ? null : String(id), method });
const response = await proxy.handleRequest(parsed);
if (response !== undefined) {
log("lazy_proxy_response", { id: String(response.id), method, is_error: response.error !== undefined });
}
return response;
}