Files
lsp-mcp/src/Model.ts
T
EFLKumo 3dc7ca6c80 lsp-mcp: headless-editor MCP server over a language server
Effect 4 MCP server (stdio/HTTP) bridging one LSP process: on-demand document
sync with LRU and disk change detection, push/pull diagnostics reconciliation,
capability-gated tools, diagnostics resources, tests against a fixture server.
2026-09-19 09:16:51 +08:00

320 lines
8.8 KiB
TypeScript

/**
* The agent-facing vocabulary of the server.
*
* Everything an agent sends or receives is described here as `Schema`, so the
* same definitions drive MCP tool input/output schemas and the values the
* `lsp/` bridge produces. Positions are 1-based (line and column), the way
* agents and editors display them; the bridge converts to LSP's 0-based form.
*/
import { Option, Predicate, Schema, SchemaGetter } from "effect";
/** An optional field whose key is omitted, rather than set to `undefined`, in JSON output. */
const optional = <S extends Schema.Top>(schema: S) =>
Schema.optional(schema).pipe(
Schema.encodeTo(Schema.optionalKey(schema), {
decode: SchemaGetter.passthrough(),
encode: SchemaGetter.transformOptional(
Option.filter(Predicate.isNotUndefined),
),
}),
);
const positive = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1));
export const File = Schema.String.annotate({
description: "File path, absolute or relative to the workspace root",
});
export const Line = positive.annotate({ description: "1-based line number" });
export const Column = positive.annotate({
description:
"1-based column, counted in UTF-16 code units (plain characters for ASCII)",
});
export const Where = Schema.Struct({
file: File,
line: Line,
column: optional(
positive.annotate({
description:
"1-based column. Omit to place the cursor on `symbol`, or on the first non-blank character of the line",
}),
),
symbol: optional(
Schema.String.annotate({
description:
"Identifier on the line to place the cursor at; used when `column` is omitted",
}),
),
});
export type Where = typeof Where.Type;
export const Range = Schema.Struct({
line: Line,
column: Column,
endLine: Line,
endColumn: Column,
});
export type Range = typeof Range.Type;
export const Location = Schema.Struct({
file: Schema.String,
...Range.fields,
preview: Schema.String.annotate({
description: "Trimmed source text of the starting line",
}),
});
export type Location = typeof Location.Type;
export const Hover = Schema.Struct({
contents: Schema.String.annotate({ description: "Markdown" }),
range: optional(Range),
});
export type Hover = typeof Hover.Type;
export const GotoKind = Schema.Literals([
"definition",
"typeDefinition",
"implementation",
"declaration",
]);
export const Goto = Schema.Struct({
...Where.fields,
kind: optional(
GotoKind.annotate({
description: "What to navigate to (default: definition)",
}),
),
});
export type Goto = typeof Goto.Type;
export const References = Schema.Struct({
...Where.fields,
includeDeclaration: optional(
Schema.Boolean.annotate({ description: "Default: true" }),
),
});
export type References = typeof References.Type;
export interface DocumentSymbol extends Range {
readonly name: string;
readonly kind: string;
readonly detail?: string | undefined;
readonly container?: string | undefined;
readonly children?: ReadonlyArray<DocumentSymbol> | undefined;
}
export const DocumentSymbol: Schema.Codec<DocumentSymbol> = Schema.Struct({
name: Schema.String,
kind: Schema.String,
detail: optional(Schema.String),
container: optional(Schema.String),
...Range.fields,
children: optional(
Schema.Array(
Schema.suspend((): Schema.Codec<DocumentSymbol> => DocumentSymbol),
),
),
});
export const WorkspaceSymbol = Schema.Struct({
name: Schema.String,
kind: Schema.String,
container: optional(Schema.String),
file: Schema.String,
line: optional(Line),
column: optional(Column),
});
export type WorkspaceSymbol = typeof WorkspaceSymbol.Type;
export const WorkspaceQuery = Schema.Struct({
query: Schema.String.annotate({
description:
"Symbol name or fragment; matching is fuzzy and server-defined",
}),
limit: optional(positive.annotate({ description: "Default: 50" })),
});
export type WorkspaceQuery = typeof WorkspaceQuery.Type;
export const Severity = Schema.Literals([
"error",
"warning",
"information",
"hint",
]);
export type Severity = typeof Severity.Type;
export const Diagnostic = Schema.Struct({
file: Schema.String,
...Range.fields,
severity: Severity,
code: optional(Schema.String),
source: optional(Schema.String),
message: Schema.String,
related: optional(
Schema.Array(
Schema.Struct({
file: Schema.String,
line: Line,
column: Column,
message: Schema.String,
}),
),
),
});
export type Diagnostic = typeof Diagnostic.Type;
export const DiagnosticsQuery = Schema.Struct({
file: optional(
File.annotate({
description:
"Restrict to one file; omit for every file the server has reported on",
}),
),
severity: optional(
Severity.annotate({
description:
"Least severe level to include (default: hint, i.e. everything)",
}),
),
});
export type DiagnosticsQuery = typeof DiagnosticsQuery.Type;
export const Completions = Schema.Struct({
...Where.fields,
prefix: optional(
Schema.String.annotate({
description: "Keep only items whose label starts with this",
}),
),
limit: optional(positive.annotate({ description: "Default: 20" })),
});
export type Completions = typeof Completions.Type;
export const Completion = Schema.Struct({
label: Schema.String,
kind: Schema.String,
detail: optional(Schema.String),
documentation: optional(Schema.String),
});
export type Completion = typeof Completion.Type;
export const Signature = Schema.Struct({
label: Schema.String,
documentation: optional(Schema.String),
parameters: Schema.Array(Schema.String),
activeParameter: optional(Schema.Int),
});
export const SignatureHelp = Schema.Struct({
signatures: Schema.Array(Signature),
active: Schema.Int,
});
export type SignatureHelp = typeof SignatureHelp.Type;
export const TextEdit = Schema.Struct({
...Range.fields,
newText: Schema.String,
});
export const FileEdits = Schema.Struct({
file: Schema.String,
edits: Schema.Array(TextEdit),
});
export const WorkspaceEdit = Schema.Struct({
applied: Schema.Boolean.annotate({
description: "Whether the edits were written to disk",
}),
files: Schema.Array(FileEdits),
operations: Schema.Array(Schema.String).annotate({
description: "File creations, renames, deletions and executed commands",
}),
});
export type WorkspaceEdit = typeof WorkspaceEdit.Type;
const apply = optional(
Schema.Boolean.annotate({
description:
"Write the resulting edits to disk (default: false, preview only)",
}),
);
export const Rename = Schema.Struct({
...Where.fields,
newName: Schema.String,
apply,
});
export type Rename = typeof Rename.Type;
export const Selection = Schema.Struct({
file: File,
line: Line,
column: optional(Column.annotate({ description: "Default: start of line" })),
endLine: optional(Line.annotate({ description: "Default: same as line" })),
endColumn: optional(
Column.annotate({ description: "Default: end of endLine" }),
),
});
export type Selection = typeof Selection.Type;
export const CodeActions = Schema.Struct({
...Selection.fields,
kind: optional(
Schema.String.annotate({
description:
"Only actions of this kind prefix, e.g. quickfix, refactor, refactor.extract, source.organizeImports",
}),
),
});
export type CodeActions = typeof CodeActions.Type;
export const CodeAction = Schema.Struct({
title: Schema.String,
kind: optional(Schema.String),
preferred: Schema.Boolean,
fixes: Schema.Array(Schema.String).annotate({
description: "Messages of the diagnostics this action addresses",
}),
});
export type CodeAction = typeof CodeAction.Type;
export const ApplyCodeAction = Schema.Struct({
...CodeActions.fields,
title: Schema.String.annotate({
description:
"Exact title of an action returned by code_actions for the same selection",
}),
});
export type ApplyCodeAction = typeof ApplyCodeAction.Type;
export const Format = Schema.Struct({
file: File,
tabSize: optional(positive.annotate({ description: "Default: 4" })),
insertSpaces: optional(
Schema.Boolean.annotate({ description: "Default: true" }),
),
apply,
});
export type Format = typeof Format.Type;
export const CallDirection = Schema.Literals(["incoming", "outgoing"]);
export const CallHierarchy = Schema.Struct({
...Where.fields,
direction: optional(
CallDirection.annotate({
description:
"incoming: who calls this; outgoing: what this calls (default: incoming)",
}),
),
});
export type CallHierarchy = typeof CallHierarchy.Type;
export const Call = Schema.Struct({
name: Schema.String,
kind: Schema.String,
detail: optional(Schema.String),
file: Schema.String,
line: Line,
column: Column,
sites: Schema.Array(Schema.Struct({ line: Line, column: Column })).annotate({
description: "Call sites, located in the caller's file",
}),
});
export type Call = typeof Call.Type;