An open API service indexing awesome lists of open source software.

https://github.com/pradeepmouli/lspeasy

TypeScript SDK for building Language Server Protocol clients and servers
https://github.com/pradeepmouli/lspeasy

framework jsonrpc language-server-protocol lsp typescript

Last synced: about 9 hours ago
JSON representation

TypeScript SDK for building Language Server Protocol clients and servers

Awesome Lists containing this project

README

          

# lspeasy

> A TypeScript SDK for building [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) clients and servers that run anywhere JavaScript runs — Node, browsers, web workers, or VS Code extensions — with a capability-aware, strongly-typed API.

> **⚠️ Pre-1.0 software** — APIs are subject to change between minor versions. Pin to exact versions in production. See the [CHANGELOG](./CHANGELOG.md) for breaking changes between releases.


npm version
ci
license
node

📚 **Documentation:**

## Overview

The [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) standardizes how editors and IDEs talk to language tooling — hover, completion, diagnostics, symbol navigation, and dozens of other features all flow over a single JSON-RPC connection. Implementing a server or client against LSP directly is deceptively involved: you need JSON-RPC framing, message validation, cancellation tokens, progress reporting, capability negotiation, the full lifecycle handshake, and correct handling of roughly a hundred request and notification types.

`lspeasy` is a set of small, focused TypeScript packages that wrap all of that in a modern, ESM-first API. Handlers are registered against a typed, capability-aware namespace — `server.textDocument.onHover(...)` — so the editor-facing surface mirrors the spec and advertised capabilities are enforced at both compile time and runtime. Transports are swappable: run the same server over stdio for a classic editor plugin, over a web worker for a browser playground, over WebSockets for a remote tooling backend, or over TCP for diagnostics.

Compared to `vscode-languageserver` (which is tightly coupled to Node and the VS Code extension model), `lspeasy` is runtime-agnostic, tree-shakeable, browser-friendly, and exposes a middleware pipeline for logging, tracing, and request rewriting without monkey-patching the dispatcher.

## Features

- **Capability-aware handler registration** — `server.textDocument.onHover(...)` is only callable after `registerCapabilities({ hoverProvider: true })`; mismatches are caught at both compile time and at the dispatcher.
- **Full JSON-RPC 2.0 core** — schemas, framing, request/notification/response types, and typed error codes, all validated with Zod.
- **Swappable transports** — `StdioTransport`, `TcpTransport`, `IpcTransport`, `WebSocketTransport`, `DedicatedWorkerTransport`, `SharedWorkerTransport`; write your own against the `Transport` interface.
- **Runs anywhere** — Node.js-specific transports live under `@lspeasy/core/node`; the root export is browser-safe so the same code ships to a VS Code extension and a web playground.
- **Typed client API** — `client.textDocument.hover(...)`, `client.workspace.symbol(...)`, and the full request surface with request/response types pulled from the LSP spec.
- **Composable middleware** — `composeMiddleware(...)` plus `createScopedMiddleware({ methods, direction })` for logging, tracing, metrics, or request mutation without touching the core dispatcher.
- **Lifecycle + progress + cancellation** — initialize/initialized/shutdown/exit are handled for you, with built-in partial-result and work-done progress senders and `CancellationToken` support.
- **Zod-validated messages** — every inbound message is parsed against a schema, so malformed peers surface as typed errors instead of runtime crashes.
- **Tree-shakeable, ESM-only** — pay for what you import; no CommonJS compatibility shims dragging extra code into browser bundles.

## Install

```bash
# Build a server
pnpm add @lspeasy/server @lspeasy/core

# Build a client
pnpm add @lspeasy/client @lspeasy/core
```

Requires **Node.js ≥ 20**. For WebSocket **server** mode or Node < 22.4, also install `ws` (optional peer): `pnpm add ws`.

## Quick Start

A minimal LSP server over stdio that responds to `textDocument/hover`:

```typescript
import { LSPServer } from '@lspeasy/server';
import { StdioTransport } from '@lspeasy/core/node';

const server = new LSPServer({ name: 'hello-lsp', version: '0.1.0' });

server.registerCapabilities({ hoverProvider: true });

server.textDocument.onHover(async (params) => ({
contents: {
kind: 'markdown',
value: `**Hovered** line ${params.position.line}, character ${params.position.character}`
}
}));

await server.listen(new StdioTransport());
```

Wire it into VS Code, Neovim, Helix, or any LSP-aware editor by spawning the script with `--stdio`.

## Usage

### Writing a client

```typescript
import { LSPClient } from '@lspeasy/client';
import { StdioTransport } from '@lspeasy/core/node';
import { spawn } from 'node:child_process';

const proc = spawn('my-language-server', ['--stdio']);
const transport = new StdioTransport({ input: proc.stdout, output: proc.stdin });

const client = new LSPClient({ name: 'my-client', version: '1.0.0' });
await client.connect(transport);

const hover = await client.textDocument.hover({
textDocument: { uri: 'file:///example.ts' },
position: { line: 0, character: 6 }
});

await client.disconnect();
```

### Transports

`@lspeasy/core` ships several built-in transports. Import Node-only transports from the `/node` subpath so browser bundles stay clean.

| Transport | Import | Notes |
|-----------|--------|-------|
| `StdioTransport` | `@lspeasy/core/node` | stdin/stdout, child processes |
| `TcpTransport` | `@lspeasy/core/node` | TCP client/server with optional reconnect |
| `SocketTransport` | `@lspeasy/core/node` | Unix domain socket or TCP; used by the proxy daemon |
| `IpcTransport` | `@lspeasy/core/node` | Node parent/child IPC |
| `WebSocketTransport` | `@lspeasy/core` | Native `globalThis.WebSocket` (Node ≥22.4 or browsers) |
| `DedicatedWorkerTransport` | `@lspeasy/core` | Browser dedicated worker |
| `SharedWorkerTransport` | `@lspeasy/core` | Browser shared worker |

### Middleware

`@lspeasy/core/middleware` provides a composable pipeline for logging, tracing, or mutating requests on the fly:

```typescript
import { composeMiddleware, createScopedMiddleware } from '@lspeasy/core/middleware';
import type { Middleware } from '@lspeasy/core/middleware';

const logging: Middleware = async (ctx, next) => {
console.log(`${ctx.direction} ${ctx.method}`);
await next();
};

const textDocOnly = createScopedMiddleware(
{ methods: /^textDocument\//, direction: 'clientToServer' },
async (ctx, next) => {
ctx.metadata.startedAt = Date.now();
await next();
}
);

const middleware = composeMiddleware(logging, textDocOnly);
```

## How it works

At the lowest layer, `@lspeasy/core` models JSON-RPC 2.0 messages (request / notification / response / error) as Zod-validated types and handles LSP's Content-Length framing. A `Transport` is just a bidirectional message pipe — everything from stdio to a `SharedWorker` implements the same interface, so the server and client layers are entirely transport-agnostic.

`@lspeasy/server` layers on lifecycle management (initialize / initialized / shutdown / exit), a capability proxy that gates handler registration on advertised capabilities, a message dispatcher that routes to typed handlers, and helpers for work-done progress and partial results. `@lspeasy/client` is the symmetric counterpart: a typed request surface that mirrors the spec and handles correlation, cancellation, and response validation.

## Packages

| Package | Description |
|---|---|
| [`@lspeasy/core`](packages/core) | JSON-RPC 2.0, framing, transports, LSP protocol types, middleware pipeline |
| [`@lspeasy/server`](packages/server) | Server class with lifecycle, capability-aware handler registration, progress/cancellation |
| [`@lspeasy/client`](packages/client) | Client with typed `textDocument.*` / `workspace.*` request API |
| [`@lspeasy/middleware`](packages/middleware) | Shared middleware building blocks |
| [`@lsproxy/cli`](apps/cli) | Language-agnostic CLI (`lspeasy` bin) — builds subcommands at runtime from the server's advertised capabilities |
| [`@lsproxy/proxy`](apps/proxy) | Per-root Unix socket daemon — holds warm LSP connections so each CLI invocation reconnects in milliseconds |

## CLI

`@lsproxy/cli` is a language-agnostic CLI that connects to any LSP server and exposes
its capabilities as typed subcommands — hover, rename, formatting, code actions, symbol
search, and more. The command surface is built at runtime from the server's advertised
capabilities, so it works out of the box with any LSP server.

```bash
pnpm add -g @lsproxy/cli # install once
lspeasy textDocument hover src/foo.ts 12:7
lspeasy textDocument rename src/foo.ts 12:7 newName
lspeasy textDocument formatting src/foo.ts
lspeasy workspace symbol MyClass
lspeasy call textDocument/semanticTokens/full --params '{"textDocument":{"uri":"file:///…"}}'
```

Positions are **1-based** `line:col` (editor-style). Write-side commands (`rename`, `formatting`,
code actions that produce edits) apply changes to disk automatically; pass `--dry-run` to preview.

Server discovery reads `lsp.json` walking up from `--root`, or use `--server ` to override.

```json
{
"lspServers": {
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact" }
}
}
}
```

See [`apps/cli/README.md`](apps/cli/README.md) for the full flag reference.

### Proxy daemon

`@lsproxy/proxy` runs as a background daemon per project root, holding warm LSP server
connections in a pool. The CLI connects to it over a Unix domain socket — subsequent
invocations skip the `initialize` handshake and respond in milliseconds instead of seconds.

```bash
# first call — spawns the daemon automatically, ~1-3s
lspeasy textDocument hover src/foo.ts 1:1
# subsequent calls — reconnects via socket, <100ms
lspeasy textDocument hover src/foo.ts 2:5

lspeasy --no-proxy textDocument hover src/foo.ts 1:1 # bypass daemon
```

The daemon exits automatically after 30 minutes of idle time.

### Claude Code plugin

This repo ships a thin Claude Code plugin (`lsp-refactor`) that routes rename, move-file,
and move-symbol requests through the language server instead of hand edits.

```
/plugin marketplace add pradeepmouli/lspeasy
/plugin install lsp-refactor@lspeasy
```

The plugin lives in [`.claude-plugin/`](.claude-plugin) and [`skills/lsp-refactor/`](skills/lsp-refactor).

## Related Projects

| Library | Relationship | npm |
|---|---|---|
| [rune-langium](https://github.com/pradeepmouli/rune-langium) | DSL toolchain powered by lspeasy's LSP server | [![npm](https://img.shields.io/npm/v/@rune-langium/core?style=flat-square)](https://www.npmjs.com/package/@rune-langium/core) |

## Contributing

```bash
pnpm install
pnpm build
pnpm test
pnpm lint
```

See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.

## License

MIT — see [LICENSE](./LICENSE).