
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@lspeasy/client
Advanced tools
Connect to Language Server Protocol servers with a simple, type-safe client API.
@lspeasy/client provides a high-level LSP client with:
textDocument.* and workspace.* methodsnpm install @lspeasy/client @lspeasy/core vscode-languageserver-protocol
# or
pnpm add @lspeasy/client @lspeasy/core vscode-languageserver-protocol
# or
yarn add @lspeasy/client @lspeasy/core vscode-languageserver-protocol
import { LSPClient } from '@lspeasy/client';
import { StdioTransport } from '@lspeasy/core';
import { spawn } from 'child_process';
// Spawn language server
const serverProcess = spawn('typescript-language-server', ['--stdio']);
// Create transport
const transport = new StdioTransport({
input: serverProcess.stdout,
output: serverProcess.stdin
});
// Create client
const client = new LSPClient({
name: 'My Client',
version: '1.0.0',
transport
});
// Connect to server (sends initialize + initialized)
await client.connect(transport);
// Use high-level API
const hover = await client.textDocument.hover({
textDocument: { uri: 'file:///path/to/file.ts' },
position: { line: 10, character: 5 }
});
console.log('Hover:', hover?.contents);
// Disconnect
await client.disconnect();
Declare client capabilities:
const client = new LSPClient({
name: 'Advanced Client',
version: '1.0.0',
transport,
capabilities: {
textDocument: {
hover: {
contentFormat: ['markdown', 'plaintext']
},
completion: {
completionItem: {
snippetSupport: true,
commitCharactersSupport: true
}
}
},
workspace: {
applyEdit: true,
workspaceEdit: {
documentChanges: true
}
}
}
});
// Hover
const hover = await client.textDocument.hover({
textDocument: { uri: 'file:///test.ts' },
position: { line: 0, character: 0 }
});
// Completion
const completion = await client.textDocument.completion({
textDocument: { uri: 'file:///test.ts' },
position: { line: 5, character: 10 }
});
// Go to Definition
const definition = await client.textDocument.definition({
textDocument: { uri: 'file:///test.ts' },
position: { line: 10, character: 15 }
});
// Find References
const references = await client.textDocument.references({
textDocument: { uri: 'file:///test.ts' },
position: { line: 20, character: 5 },
context: { includeDeclaration: false }
});
// Document Symbols
const symbols = await client.textDocument.documentSymbol({
textDocument: { uri: 'file:///test.ts' }
});
// Open document
await client.textDocument.didOpen({
textDocument: {
uri: 'file:///test.ts',
languageId: 'typescript',
version: 1,
text: 'console.log("Hello");'
}
});
// Change document
await client.textDocument.didChange({
textDocument: {
uri: 'file:///test.ts',
version: 2
},
contentChanges: [
{
text: 'console.log("Hello, World!");'
}
]
});
// Save document
await client.textDocument.didSave({
textDocument: { uri: 'file:///test.ts' },
text: 'console.log("Hello, World!");'
});
// Close document
await client.textDocument.didClose({
textDocument: { uri: 'file:///test.ts' }
});
// Workspace symbols
const symbols = await client.workspace.symbol({
query: 'MyClass'
});
// Configuration (if server requests it)
// Server will call this via client.onRequest('workspace/configuration')
// Workspace folders
await client.workspace.didChangeWorkspaceFolders({
event: {
added: [{ uri: 'file:///new/folder', name: 'New Folder' }],
removed: []
}
});
// File watching
await client.workspace.didChangeWatchedFiles({
changes: [
{
uri: 'file:///test.ts',
type: 2 // Changed
}
]
});
// Send any request
const result = await client.sendRequest<ParamsType, ResultType>(
'custom/method',
{ /* params */ }
);
// Send any notification
await client.sendNotification<ParamsType>(
'custom/notification',
{ /* params */ }
);
import { CancellationTokenSource } from '@lspeasy/core';
const source = new CancellationTokenSource();
// Send cancellable request
const { promise, cancel } = client.sendRequestCancellable(
'textDocument/hover',
params,
source.token
);
// Cancel after 5 seconds
setTimeout(() => {
source.cancel();
// or use the returned cancel function
// cancel();
}, 5000);
try {
const result = await promise;
} catch (error) {
if (error.message.includes('cancelled')) {
console.log('Request was cancelled');
}
}
const client = new LSPClient({
capabilities: {
workspace: {
didChangeWatchedFiles: { dynamicRegistration: true }
}
},
dynamicRegistration: {
allowUndeclaredDynamicRegistration: false
}
});
const runtime = client.getRuntimeCapabilities();
console.log(runtime.dynamicRegistrations);
client/registerCapability and client/unregisterCapability are handled automatically.-32602.allowUndeclaredDynamicRegistration: true for compatibility-mode acceptance.const result = await client.sendRequestWithPartialResults('workspace/symbol', { query: 'My' }, {
token: 'symbols-1',
onPartial: (batch) => console.log('partial batch', batch)
});
if (result.cancelled) {
console.log(result.partialResults);
} else {
console.log(result.finalResult);
}
await client.notebookDocument.didOpen(params);
await client.notebookDocument.didChange(params);
await client.notebookDocument.didSave(params);
await client.notebookDocument.didClose(params);
// Connected to server
client.onConnected(() => {
console.log('Connected to language server');
});
// Disconnected from server
client.onDisconnected(() => {
console.log('Disconnected from language server');
});
// Connection errors
client.onError((error) => {
console.error('Client error:', error);
});
Use waitForNotification when you need the next matching server notification as a Promise.
const diagnostics = await client.waitForNotification('textDocument/publishDiagnostics', {
timeout: 5000,
filter: (params) => params.uri === 'file:///example.ts'
});
console.log(diagnostics.diagnostics);
Notes:
timeout is required.const client = new LSPClient({
name: 'health-aware-client',
version: '1.0.0',
heartbeat: {
enabled: true,
interval: 30000,
timeout: 10000
}
});
const stateSubscription = client.onConnectionStateChange((event) => {
console.log('state', event.previous, '->', event.current, event.reason);
});
const healthSubscription = client.onConnectionHealthChange((health) => {
console.log('last sent', health.lastMessageSent);
console.log('last received', health.lastMessageReceived);
});
const health = client.getConnectionHealth();
console.log(health.state);
stateSubscription.dispose();
healthSubscription.dispose();
// Diagnostics from server
client.onNotification('textDocument/publishDiagnostics', (params) => {
console.log(`Diagnostics for ${params.uri}:`, params.diagnostics);
});
// Show message from server
client.onNotification('window/showMessage', (params) => {
console.log(`Server message (${params.type}): ${params.message}`);
});
// Log message from server
client.onNotification('window/logMessage', (params) => {
console.log(`Server log (${params.type}): ${params.message}`);
});
Handle requests from server to client:
// Configuration request
client.onRequest('workspace/configuration', async (params) => {
return [
{ enable: true },
{ maxProblems: 100 }
];
});
// Apply workspace edit
client.onRequest('workspace/applyEdit', async (params) => {
// Apply the edit
applyWorkspaceEdit(params.edit);
return { applied: true };
});
// Show message request (with actions)
client.onRequest('window/showMessageRequest', async (params) => {
// Show dialog to user
const choice = await showDialog(params.message, params.actions);
return choice;
});
When handling server-to-client requests:
-32601 (method not found).-32603 (internal error).import { LSPClient } from '@lspeasy/client';
import { WebSocketTransport } from '@lspeasy/core';
// Connect over WebSocket with automatic reconnection
const transport = new WebSocketTransport({
url: 'ws://localhost:3000',
enableReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 1000,
maxReconnectDelay: 30000,
reconnectBackoffMultiplier: 2
});
const client = new LSPClient({
name: 'WebSocket Client',
version: '1.0.0',
transport
});
// Handle reconnection
transport.onClose(() => {
console.log('Connection lost, attempting to reconnect...');
});
// Connect
await client.connect();
Implement a simple document tracker:
class DocumentTracker {
private documents = new Map<string, { version: number; content: string }>();
async open(client: LSPClient, uri: string, languageId: string, content: string): Promise<void> {
this.documents.set(uri, { version: 1, content });
await client.textDocument.didOpen({
textDocument: {
uri,
languageId,
version: 1,
text: content
}
});
}
async change(client: LSPClient, uri: string, newContent: string): Promise<void> {
const doc = this.documents.get(uri);
if (!doc) return;
const newVersion = doc.version + 1;
this.documents.set(uri, { version: newVersion, content: newContent });
await client.textDocument.didChange({
textDocument: { uri, version: newVersion },
contentChanges: [{ text: newContent }]
});
}
async close(client: LSPClient, uri: string): Promise<void> {
this.documents.delete(uri);
await client.textDocument.didClose({
textDocument: { uri }
});
}
get(uri: string): string | undefined {
return this.documents.get(uri)?.content;
}
}
// Usage
const tracker = new DocumentTracker();
await tracker.open(client, 'file:///test.ts', 'typescript', 'console.log();');
await tracker.change(client, 'file:///test.ts', 'console.log("Hello");');
await tracker.close(client, 'file:///test.ts');
const diagnostics = new Map<string, Diagnostic[]>();
client.onNotification('textDocument/publishDiagnostics', (params) => {
diagnostics.set(params.uri, params.diagnostics);
// Display diagnostics
for (const diagnostic of params.diagnostics) {
console.log(`${params.uri}:${diagnostic.range.start.line + 1}: ${diagnostic.message}`);
}
});
// Get diagnostics for a file
function getDiagnostics(uri: string): Diagnostic[] {
return diagnostics.get(uri) || [];
}
import { LSPClient } from '@lspeasy/client';
import { MockTransport } from '@lspeasy/core/test/utils';
describe('LSP Client', () => {
it('should send hover request', async () => {
const transport = new MockTransport();
const client = new LSPClient({
name: 'Test Client',
version: '1.0.0',
transport
});
await client.connect();
// Send hover request
const hoverPromise = client.textDocument.hover({
textDocument: { uri: 'file:///test.ts' },
position: { line: 0, character: 0 }
});
// Simulate server response
const request = transport.sentMessages.find(m => m.method === 'textDocument/hover');
transport.simulateMessage({
jsonrpc: '2.0',
id: request.id,
result: {
contents: 'Test hover'
}
});
const hover = await hoverPromise;
expect(hover?.contents).toBe('Test hover');
});
});
Ensure the client is initialized before sending requests:
await client.connect();
// Now safe to send requests
Subscribe to disconnection events and handle gracefully:
client.onDisconnected(() => {
console.log('Server disconnected');
// Attempt to reconnect or notify user
});
Prefer high-level methods over low-level sendRequest:
// Good
const hover = await client.textDocument.hover(params);
// Less type-safe
const hover = await client.sendRequest('textDocument/hover', params);
Always disconnect when done:
try {
await client.connect();
// Use client
} finally {
await client.disconnect();
}
Use cancellation tokens for long-running operations:
const source = new CancellationTokenSource();
const { promise } = client.sendRequestCancellable(method, params, source.token);
// Cancel if needed
setTimeout(() => source.cancel(), 5000);
See API.md for complete API documentation.
See ARCHITECTURE.md for system architecture details.
MIT
FAQs
Connect to LSP servers with typed client API
We found that @lspeasy/client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.