Type Server Protocol spec defines JSON-RPC access to Python type analysis

The Type Server Protocol (TSP) is described, on a page in the microsoft/pylance-release repository, as a JSON-RPC protocol for asking a type server for Python analysis data. A type server keeps the type-analysis state for a workspace and answers requests for the protocol version, snapshots, Python search paths, import resolution and type queries (computed, declared and expected types). TSP borrows Language Server Protocol (LSP) data shapes where they help, such as Position and Range, and represents URIs as strings the way LSP does. Its methods carry the typeServer/ prefix. The authoritative definitions are three artifacts in the same folder: typeServerProtocol.ts (TypeScript interfaces and request types), tsp.json (the machine-readable protocol model) and tsp.schema.json (a JSON schema for that model).
Versioning uses semantic version strings for client/server compatibility checks. While the protocol is in the 0.x line, minor version changes may be breaking; patch changes stay backward compatible within the same minor line.
A client starts a type server and talks to it over the main JSON-RPC connection, using a transport the server supports, such as stdio or Node IPC. With stdio, stdout must be reserved for JSON-RPC messages. The server needs enough workspace and document state to answer, and clients commonly get it through normal LSP initialization and document synchronization, then send typeServer/* requests on the same connection. A typical session runs in this order: start or connect to the server; initialize shared state (commonly via the LSP initialize and initialized messages); call typeServer/getSupportedProtocolVersion and check that the returned semver string is compatible; call typeServer/getSnapshot for the first snapshot identifier; send type-query, import-resolution or search-path requests that include the current snapshot where the request shape requires it; and handle typeServer/snapshotChanged, which the server sends when earlier type results are no longer valid.
Snapshots are central. A snapshot is a non-negative integer identifying a point-in-time view of the server's analysis state, and returned type handles, declarations and symbols are valid only for the snapshot in which they came back, unless a request says otherwise. The server must advance the snapshot whenever a previously returned result may be invalid: opened-file changes, closed files, configuration changes, watched-file changes, dependency changes, or any cache reset that changes results. Snapshot identifiers must not decrease during a session, and typeServer/snapshotChanged reports the previous and new identifiers. Requests with a stale snapshot should fail with a JSON-RPC error that lets the client retry; LSP's ServerCancelled code (-32802) is the preferred error.
Positions follow LSP: lines are zero-based and character offsets are zero-based UTF-16 code units. A server that stores locations in another encoding must convert at the protocol boundary, because a wrong conversion can make the client ask about the wrong syntax node.
The three type-query requests (typeServer/getComputedType, getDeclaredType and getExpectedType) identify a source construct by an argument that is either a node (a URI plus a source range) or a declaration. A declaration is either regular, backed by a source AST node, or synthesized, created by the type checker with no source node. The three differ in which type they report. getComputedType returns the type inferred from code flow: after narrowing a to int, the computed type of b in b = a + 1 is int. getDeclaredType returns the type written in the source: for def foo(a: int | str), the declared type of a is int | str. getExpectedType returns what the surrounding context expects: for the call foo(4), the expected type of the argument is int | str. InvalidHandle is -1 and marks an invalid or unavailable handle, to be treated as a sentinel and not as a real object identifier.
Type results are JSON-serializable tagged objects. The protocol supports built-in types, declared types, functions, classes, unions, modules, type variables, overload sets, synthesized types and type references. Every type object carries a numeric kind discriminator, a numeric id and a bitfield of flags. Type objects are valid only for the snapshot of the request that produced them. Type IDs exist to connect TypeReference objects to earlier objects in the same serialized response graph, so they are response-local: they are not a stable global identity and should not be compared across responses. Type references are meant to avoid duplicating large or recursive type graphs inside one response.
TSP also has an optional multi-connection mode, framed as a performance feature. The original connection stays the main connection and owns initialization, workspace state, document synchronization, state-changing notifications and connection control. A client can open extra read-only local channels to the same server for concurrent work such as high-volume type queries or background workers that should not block the main connection. A server must still work correctly in single-connection mode, and multi-connection support must not be required for protocol correctness.
The mode is negotiated in the LSP initialize request and response. Each side advertises supportedTransports under capabilities.experimental.typeServerMultiConnection (the example uses ["ipc"], currently the only built-in transport kind for extra connections). The mode is on only if both sides advertise the capability and share at least one transport. Otherwise the session stays single-connection and the client must not send typeServer/connection. To open a channel, the client sends typeServer/connection on the main connection with type open, kind ipc and an args array. For a full-duplex IPC stream, args holds one endpoint string. If the platform offers only one-way endpoints, args holds two, named from the server's perspective: first the server input stream, then the server output stream. The server should validate the request, connect promptly and return success: true, or success: false with a short message if it cannot. To close, the client sends the same request with type close and the same args shape; the server should stop serving that channel and close the transport promptly, and the client may tear down its local transport if the server does not finish within the client's timeout budget.
The following stay main-connection-only: LSP initialization and lifecycle messages; workspace, configuration, file-watcher and document synchronization notifications; server-to-client snapshot notifications; and the typeServer/connection control request. Extra connections are TSP-only and read-only, for requests that do not mutate server state and whose results are valid for the client-supplied snapshot. Which TSP messages are allowed on an extra connection is bound to the negotiated TSP version, and a server that advertises multi-connection support for a version must support every extra-connection message defined for it. Servers should reject LSP traffic, state-changing notifications, typeServer/connection and any message not allowed for the negotiated version when it arrives on an extra connection.
Errors use standard JSON-RPC responses, with enough detail for a client log to identify the failed request but without exposing local secrets or implementation-only state. The page ends with a checklist of behaviours a compatible type server should verify: the supported-version call returns a semver string the client supports; payloads serialize and deserialize according to the artifacts; snapshots are non-negative and never go backward; document, configuration and watched-file changes invalidate snapshots when they can change results; stale-snapshot requests fail with a retryable cancellation error; position conversions use LSP zero-based UTF-16 conventions; import-resolution requests return document URIs or no result as the result type allows; and type queries return well-formed Type objects or undefined for the client's snapshot. The text available for this retelling breaks off in the next item, which concerns multi-connection behaviour.
Key facts
- TSP is a JSON-RPC protocol for asking a type server for Python analysis data: protocol version, snapshots, search paths, import resolution and type queries (computed, declared, expected).
- Every result is bound to a snapshot, a non-negative integer that must never decrease; stale requests should fail with LSP's ServerCancelled error (-32802) so the client can retry.
- Optional multi-connection mode adds read-only extra channels (currently only the ipc transport) while the main connection keeps all state-changing traffic; single-connection operation must always work.
- The protocol is on the 0.x line: minor version changes may be breaking, patch changes stay backward compatible within a minor line.
- The authoritative definitions are typeServerProtocol.ts, tsp.json and tsp.schema.json, kept in the same folder of the microsoft/pylance-release repository.
Why it matters
The page defines a wire protocol for getting Python type information out of a type analyzer, reusing LSP conventions for positions, ranges and URIs. It splits three questions that tools often blur: the type inferred from code flow, the type written in the source, and the type the context expects. It also makes staleness explicit through snapshots, so a client knows when earlier answers can no longer be trusted. The document lives in the pylance-release repository, but the visible text does not say which clients or servers implement it.
Who it affects
The document addresses two kinds of reader: authors of clients that start a type server and query it, and authors of type servers that must answer those queries. Servers are expected to implement the messages their target client requires. The visible text names no specific implementations or products beyond the repository it sits in.
How to use it
Follow the session order the page gives. Start or connect to the server and initialize shared state, commonly through LSP initialize and initialized. Call typeServer/getSupportedProtocolVersion and check the semver string, then typeServer/getSnapshot for a first snapshot. Send type, import-resolution or search-path requests with that snapshot where required, and listen for typeServer/snapshotChanged. Use multi-connection mode only if you gain from concurrent read-only work. Both sides must advertise typeServerMultiConnection with a shared transport, and the client then sends typeServer/connection with type open on the main connection. Treat the three artifacts (typeServerProtocol.ts, tsp.json, tsp.schema.json) as the source of truth for exact parameter and result shapes.
How solid is it
This is a specification page, so it states intended behaviour and not measured results. It says the three artifacts in the folder are the authoritative definitions. The protocol is on the 0.x line, so the contract can still change between minor versions. The visible text gives no current version number, release date or authors, and the available text is cut off in the conformance checklist, so the message tables and the multi-connection checklist items are not covered here.
Risks and caveats
Minor version changes may be breaking while the protocol is 0.x, so clients should check the version returned by typeServer/getSupportedProtocolVersion. Handles and type objects die with their snapshot, and type IDs are only meaningful inside one response, so caching across snapshots or comparing IDs across responses is a mistake. A wrong UTF-16 conversion can send the client to the wrong syntax node. Over stdio, anything other than JSON-RPC on stdout breaks the channel. Multi-connection mode is optional, and a server that needs it for correctness does not follow the spec.
“Multi-connection support must not be required for protocol correctness.”
— Type Server Protocol document, microsoft/pylance-release