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.
This commit is contained in:
18 files changed
+3611
-38
No files matched your search
+319
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in new issue
Block a user