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
- Host: GitHub
- URL: https://github.com/pradeepmouli/lspeasy
- Owner: pradeepmouli
- License: mit
- Created: 2026-01-29T18:10:14.000Z (5 months ago)
- Default Branch: develop
- Last Pushed: 2026-06-22T12:05:02.000Z (7 days ago)
- Last Synced: 2026-06-22T14:06:27.405Z (7 days ago)
- Topics: framework, jsonrpc, language-server-protocol, lsp, typescript
- Language: TypeScript
- Homepage: https://pradeepmouli.github.io/lspeasy/
- Size: 3.56 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
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.
📚 **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 | [](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).