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

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.
  • 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.
  • VS Code Integration: A minimal TypeScript wrapper in 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, the initialization code creates a non-WASM LSP server and enters the main event loops:

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, 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. The dispatch_lsp_events function in 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:

// 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 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). 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:


# 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:

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.

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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →