# How Pyrefly Implements Its Language Server Protocol: A Deep Dive into the Rust LSP Architecture

> Discover how Pyrefly implements its Language Server Protocol with a pure-Rust server, parallel TSP bridge, and three-layer architecture for real-time IDE features.

- Repository: [Meta/pyrefly](https://github.com/facebook/pyrefly)
- Tags: deep-dive
- Published: 2026-05-21

---

**Pyrefly implements its Language Server Protocol (LSP) as a pure-Rust server that runs both a standard LSP event loop and a TypeScript Protocol (TSP) bridge in parallel, enabling real-time type checking and rich IDE features through a three-layer architecture.**

The facebook/pyrefly repository contains a high-performance Python type checker that replaces the original Pyright server with a Rust-based implementation. Understanding how Pyrefly implements its language server protocol reveals a sophisticated design that maintains compatibility with existing VS Code extensions while providing enhanced performance through parallel message processing and shared state management.

## Architecture Overview: The Three-Layer Design

Pyrefly’s LSP implementation consists of three tightly-coupled layers that work together to provide seamless IDE integration:

- **Core LSP Server**: Handles JSON-RPC message parsing, editor state management, and diagnostic publishing. Key types include `Server`, `ServerConnection`, `Message`, and `LspEvent` located in [`pyrefly/lib/lsp/non_wasm/server.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/lsp/non_wasm/server.rs).
- **TSP Bridge**: Exposes a JSON-RPC "type-server" that IDEs query for type information, snapshots, and import-resolution data. This layer uses `TspServer`, `TspMainConnection`, and `TspConnection` defined in [`pyrefly/lib/tsp/server.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/server.rs).
- **VS Code Integration**: A minimal TypeScript wrapper in [`lsp/src/extension.ts`](https://github.com/facebook/pyrefly/blob/main/lsp/src/extension.ts) launches the Rust binary as a child process and handles configuration quirks like disabling competing Pyright extensions.

## Entry Point and Process Creation

The language server starts when the VS Code extension spawns the `pyrefly` binary with the `--from lsp` flag. In [`main.rs`](https://github.com/facebook/pyrefly/blob/main/main.rs), the initialization code creates a **non-WASM** LSP server and enters the main event loops:

```rust
let server = Server::new(...);
let lsp_reader = MessageReader::new(...);
let tsp_reader = MessageReader::new(...);
tsp_loop(server.clone(), tsp_reader, init_info, &telemetry)?;

```

The `tsp_loop` function, defined in [`pyrefly/lib/tsp/server.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/server.rs), simultaneously runs both the LSP event loop and the TSP loop. This design allows the **TspServer** to wrap the same `Server` instance, ensuring the type-server shares the type-checker state with the LSP handler.

## Message Handling and Event Dispatch

All incoming JSON-RPC messages are decoded into the `Message` enum defined in [`pyrefly/lib/lsp/non_wasm/protocol.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/lsp/non_wasm/protocol.rs). The `dispatch_lsp_events` function in [`server.rs`](https://github.com/facebook/pyrefly/blob/main/server.rs) classifies each message:

- **Request** messages are queued as `LspEvent::LspRequest`
- **Notification** messages convert to specific variants like `DidChangeTextDocument` or `DidOpenNotebookDocument`

The `ServerConnection` forwards each `LspEvent` to `Server::process_event`, which performs three critical operations:

1. Updates internal state, including file contents and unsaved file tracking
2. Runs a **transaction** through `TransactionManager` to perform incremental type-checking
3. Publishes diagnostics via `publish_diagnostics_for_uri` to the LSP client

## Snapshot Management and Incremental Indexing

Pyrefly maintains a **snapshot version** (`current_snapshot`) that increments whenever a mutation could affect types, such as processing a `DidChangeTextDocument` notification. When the snapshot changes, `TspServer::broadcast_snapshot_changed` sends a `typeServer/snapshotChanged` notification to connected clients.

This mechanism enables editors to invalidate cached type data precisely when it becomes stale, ensuring that autocomplete and hover information always reflect the current state of the codebase.

## TypeScript Protocol (TSP) Bridge

The TSP bridge reuses the core server logic while exposing a limited set of JSON-RPC methods defined in `tsp_types::TSPRequests`, including `GetDeclaredTypeRequest` and `ResolveImportRequest`. The `TspConnection::dispatch_tsp_request` method matches incoming requests and forwards them to handlers in `pyrefly/lib/tsp/requests/*.rs`.

Each handler follows a consistent pattern:

```rust
// Deserialize parameters
let params: GetDeclaredTypeParams = serde_json::from_value(params)?;
// Query the core server
let result = server.inner().resolve_func_def_range(...)?;
// Send response
send_ok(id, result);

```

Because both the LSP and TSP servers share the same `TspServer` instance, snapshots, diagnostics, and indexing remain **automatically synchronized** across both protocols.

## Multi-Connection Support and IPC

Pyrefly implements the experimental **`typeServerMultiConnection`** capability, allowing the main connection to open additional IPC channels through `ConnectionRequest` with `type_: "open"`. These extra connections (`TspExtraConnection`) can serve TSP queries but cannot manage LSP lifecycle events.

This architecture mirrors Pyright’s "type-server-multi-connection" feature, enabling editors to issue concurrent type queries without blocking the main LSP thread that handles critical user interactions like autocomplete.

## Telemetry and Progress Reporting

Both the LSP and TSP loops forward telemetry events to a shared `Telemetry` implementation from `pyrefly_util::telemetry`. The `LspProgressSubscriber` in [`server.rs`](https://github.com/facebook/pyrefly/blob/main/server.rs) translates internal recheck progress into LSP `$/progress` notifications, providing users with responsive progress bars during large-scale type checking operations.

## Diagnostic Rendering with Markdown Support

Pyrefly detects client capabilities through `diagnostic_markdown_support` and conditionally rewrites diagnostic messages into markdown-compatible formats using `apply_diagnostic_markup` and `format_diagnostic_message_for_markdown` (lines 652-698 in [`pyrefly/lib/lsp/non_wasm/server.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/lsp/non_wasm/server.rs)). This ensures rich diagnostic rendering in VS Code while maintaining plain-text compatibility with other editors.

## Running the LSP Server

To start the language server manually for debugging or integration with other editors:

```bash

# Build the optimized binary

cargo build --release

# Launch the LSP server

./target/release/pyrefly --from lsp

```

For VS Code integration, the TypeScript wrapper handles process management automatically, including disabling conflicting extensions like Windsurf Pyright:

```typescript
import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
  // Prevent conflicts with other Pyright-based extensions
  await disableWindsurfPyrightIfInstalled();
}

```

## Summary

- Pyrefly implements a **pure-Rust LSP** with parallel LSP and TSP event loops sharing a single `Server` instance.
- The architecture separates concerns into **core LSP handling**, **TypeScript Protocol bridging**, and **VS Code integration** layers.
- **Snapshot versioning** ensures type information stays synchronized across all connections.
- **Multi-connection IPC** support allows concurrent type queries without blocking the main LSP thread.
- All message handling occurs through strongly-typed Rust enums (`Message`, `LspEvent`) with explicit dispatch logic in [`server.rs`](https://github.com/facebook/pyrefly/blob/main/server.rs).

## Frequently Asked Questions

### How does Pyrefly handle concurrent requests from multiple IDE clients?

Pyrefly uses the `typeServerMultiConnection` experimental capability to open extra IPC channels via `ConnectionRequest`. These `TspExtraConnection` instances can handle TSP queries concurrently while the main `ServerConnection` manages LSP lifecycle events, preventing blocking during intensive type-checking operations.

### What is the difference between the LSP and TSP protocols in Pyrefly?

The **LSP protocol** handles standard editor operations like text synchronization, diagnostics, and file watching, while the **TSP (TypeScript Protocol)** bridge exposes type-specific queries such as `GetDeclaredTypeRequest` and `ResolveImportRequest`. Both protocols share the same underlying `Server` state but communicate through separate JSON-RPC channels managed by `tsp_loop`.

### How does Pyrefly ensure diagnostics are up-to-date after file changes?

When `Server::process_event` handles a `DidChangeTextDocument` notification, it increments the `current_snapshot` version and triggers `TspServer::broadcast_snapshot_changed`. This notification invalidates client-side caches, ensuring subsequent type queries reflect the latest file contents.

### Can Pyrefly be used with editors other than VS Code?

Yes. While the repository includes a VS Code extension in [`lsp/src/extension.ts`](https://github.com/facebook/pyrefly/blob/main/lsp/src/extension.ts), the core Rust binary speaks standard LSP when launched with `--from lsp`. Any LSP-compatible editor can connect to the server, though the TSP bridge provides enhanced type information for clients that support the TypeScript Protocol extensions.