Brand logo
HomeCloud ComputingSecuritySoftware DevelopmentAI ApplicationsStartupsGeneral
  1. Home
  2. /
  3. softwaredevelopment
  4. /
  5. python-type-server-protocol-tsp-overview

ItInsightsWorld SoftwareDevelopment: Turning the Type Server Protocol (TSP) into Working Python Analysis

Autocomplete, hover tooltips, quick fixes, import organization: all of these product features rest on one question, namely what type a Python construct has at a given position in a given file, right now. The Type Server Protocol (TSP) is the channel that answers it. Implemented as a JSON-RPC interface, it lets a client query a type server that holds the type-analysis state of a workspace, covering protocol-version checks, snapshot identifiers, Python search paths, import resolution, and lookups of computed, declared, and expected types.

Teams already integrating the Language Server Protocol will find the design familiar rather than alien. TSP reuses LSP data shapes and conventions, including LSP Position and LSP Range, and encodes URIs as strings the same way LSP does; only the naming differs, because every method carries the typeServer/ prefix. Normative definitions sit next to the protocol itself: typeServerProtocol.ts holds the TypeScript interfaces and request types, tsp.json holds the machine-readable protocol model, and tsp.schema.json holds the JSON schema for that model.

Compatibility Policy: What Each Version Bump Costs You

Client and server check compatibility with semantic version strings, and the blast radius depends on which digit moves. While TSP remains in the 0.x line, a minor version bump can break compatibility, whereas patch bumps stay backward compatible within the same minor line. A client that pins behavior to a remembered version string therefore needs the handshake described below instead of an assumption baked into code.

VersionProtocol changeConsequence for a client/server pair
0.1.0Initial protocol release.Baseline request set only.
0.2.0Additional request types and fields appeared.A client built against 0.1.0 cannot assume the additions exist.
0.3.0The protocol moved to more elaborate type shapes.Type-result parsing written for the earlier shapes does not carry over, since a minor bump may break compatibility.
0.4.0Adoption of the Type union and stub-shaped type payloads.Clients have to dispatch on the kind discriminator described later in this guide.
0.4.1Multi-connection negotiation plus the typeServer/connection control request.Extra read-only channels become negotiable, while servers must still behave correctly in single-connection mode.

Step by Step: Running a Standard Session

The sequence below is the path most integrations take. Missing step 3 or step 6 is where sessions start returning answers about code that no longer exists.

  1. Start the type server or connect to one that is already running.
  2. Bring the workspace and document state into agreement between client and server, commonly with the LSP initialize and initialized messages.
  3. Call typeServer/getSupportedProtocolVersion and confirm the returned semver string matches the protocol version the client expects.
  4. Call typeServer/getSnapshot to obtain the first snapshot identifier.
  5. Send type-query, import-resolution, or search-path requests, attaching the current snapshot whenever the request shape requires it.
  6. Listen for typeServer/snapshotChanged, which the server emits once earlier type results stop being valid.

Connection Topology: One Owner, Zero or More Readers

The client launches or reaches the type server and speaks to it over a main JSON-RPC connection carried by a supported transport such as stdio or Node IPC. Choosing stdio imposes one operational rule: stdout has to stay reserved for JSON-RPC protocol messages, so any stray log write on that stream corrupts the session.

The server also needs enough workspace and document state to honor the TSP requests it supports. In practice clients acquire that state through ordinary LSP initialization and document synchronization, and then issue typeServer/* requests on the same JSON-RPC connection.

Deciding Whether Multi-Connection Is Worth the Complexity

Multi-connection mode is an optional performance capability, not a correctness requirement. It adds local channels to the same type server for read-only TSP requests while the main connection keeps ownership of mutable workspace state. Reserve it for workloads that genuinely benefit from parallel read-only activity, such as heavy type-query traffic or background workers that must not block the main JSON-RPC connection. A server still has to behave correctly in single-connection mode.

Negotiating the Capability

Negotiation happens inside the normal LSP initialize request and response. A client able to open extra TSP channels advertises its supported transport kinds under capabilities.experimental.typeServerMultiConnection; a server able to serve them returns the same capability shape in the initialize result. The mode turns on only when both sides advertise typeServerMultiConnection and their supportedTransports lists share at least one transport. If either side omits the capability, or the transport lists do not intersect, the session stays single-connection and the client must not send typeServer/connection. The built-in transport kind available for extra connections is ipc, and this negotiation is separate from the main connection, which may use stdio or Node IPC.

Opening an Extra Channel

After initialization and transport negotiation finish, the client opens an extra channel by sending typeServer/connection on the main connection, with type set to open, kind set to ipc, and args naming one or more endpoints the server should connect to. The server must validate the request, connect promptly to the supplied endpoint or endpoints, and return a result with success set to true when it accepts. When the channel cannot be opened, it should return success set to false and add a short message wherever that helps logs.

IPC Endpoint Arguments

An ipc endpoint is expected to provide a full-duplex JSON-RPC stream when the platform IPC mechanism supports that form, in which case args carries one endpoint string. If the platform exposes only one-way endpoints, args carries two endpoint strings, named from the type server viewpoint: the first is the server input stream, the second the server output stream.

Closing an Extra Channel

Closing is the mirror image of opening: the client sends typeServer/connection on the main connection with the same transport kind and endpoint identity and type set to close, reusing the args shape that opened the channel, that is one endpoint for a full-duplex IPC stream or the same input/output pair for a split stream. The server should stop serving the matching extra channel and close the transport promptly; if it does not finish within the client timeout budget, the client may tear down its local transport.

Traffic Rules per Channel

All state-changing traffic belongs to the main connection. These remain main-connection-only:

  • LSP initialization and lifecycle messages.
  • Workspace, configuration, file-watcher, and document synchronization notifications.
  • Server-to-client snapshot notifications.
  • The typeServer/connection control request.

Extra connections are TSP-only and read-only: they may carry requests that do not mutate server state and whose results stay valid for the snapshot the client supplied. Which messages qualify is tied to the negotiated TSP version. A server that reports support for a TSP version and advertises multi-connection support for that session has to support every extra-connection TSP message defined for that version.

TSP versionExtra-connection message
0.4.1typeServer/getSupportedProtocolVersion
0.4.1typeServer/getSnapshot
0.4.1typeServer/getPythonSearchPaths
0.4.1typeServer/resolveImport
0.4.1typeServer/getComputedType
0.4.1typeServer/getDeclaredType
0.4.1typeServer/getExpectedType

Servers should reject LSP traffic, state-changing notifications, typeServer/connection, and any TSP message not allowed for the negotiated TSP version when those arrive on an extra connection.

Snapshots: The Validity Clock behind Every Result

A snapshot is a non-negative integer naming a point-in-time view of the type server analysis state. Type handles, declarations, symbols, and other returned objects are valid only for the snapshot in which they were returned, unless a request explicitly says otherwise. The server must advance the snapshot whenever a previously returned type-analysis result may become invalid; typical invalidating events are opened-file changes, closed files, configuration changes, watched-file changes, dependency changes, and any cache reset that changes analysis results.

  • typeServer/getSnapshot returns the current snapshot identifier.
  • Identifiers must not decrease during a session.
  • typeServer/snapshotChanged reports both the previous and the new identifier.
  • A request carrying a stale snapshot should fail with a JSON-RPC error that lets the client retry with a fresh snapshot; the preferred error in this case is the LSP ServerCancelled code -32802.

Positions and Ranges: Normalize at the Protocol Boundary

TSP follows LSP position conventions. Lines are zero-based, character offsets are zero-based UTF-16 code units, and a range identifies a source span through a start position and an end position. When a type server stores source locations in another encoding, it has to convert them at the protocol boundary, because a faulty conversion makes the client ask about the wrong syntax node.

Nodes, Declarations, and Handle Sentinels

The three type-query requests, typeServer/getComputedType, typeServer/getDeclaredType, and typeServer/getExpectedType, identify a source construct through an arg that is either a node or a declaration. A node is a URI plus a source range. A declaration describes where and how a symbol is defined, and may be a regular declaration backed by a source AST node or a synthesized declaration created by the type checker, which has no source node.

InvalidHandle is -1 and stands for an invalid or unavailable handle. Both sides should treat it as a sentinel value, never as a valid object identifier.

Anatomy of a Type Result

Type results are JSON-serializable tagged objects. Each one carries a discriminator that tells the client how to read the remaining fields, and every type object includes a numeric kind discriminator, a numeric id, and a bitfield of flags. The protocol covers built-in types, declared types, functions, classes, unions, modules, type variables, overload sets, synthesized types, and type references.

kindTypeWhat it represents
0BuiltInunknown, any, unbound, ellipsis, never and similar cases.
1DeclaredBase form for source-declared types, rarely used directly.
2FunctionFunctions and methods produced by def statements.
3ClassClasses and their instances.
4UnionUnion forms such as int | str | None.
5ModuleModule objects; import os yields a Module type for os.
6TypeVarType variables such as T, P, and Ts.
7OverloadedFunctions carrying multiple @overload signatures.
8SynthesizedTypes synthesized by the type checker.
9TypeReferenceReference to another type by id, used for deduplication and cycles.

Two scoping rules decide what your cache may hold: type objects are snapshot-bound and type ids are response-local.

  • A returned type object is valid only for the snapshot used by the request that produced it.
  • Type ids connect TypeReference objects to earlier type objects inside the same serialized response graph.
  • A type id is not a stable global identity for a snapshot and should not be compared across separate responses.
  • Type references exist so a large or recursive type graph is not duplicated within one response.

The Complete Message Surface

The current protocol is intentionally small, with exact parameters and result shapes defined in the protocol artifacts.

MessageDirectionParamsResult
typeServer/getSupportedProtocolVersionclient requestnonestring (semver)
typeServer/getSnapshotclient requestnonenumber
typeServer/snapshotChangedserver notification{ old: number, new: number }notification
typeServer/connectionclient request{ type: open or close, kind, optional args }{ success, optional message }
typeServer/getPythonSearchPathsclient request{ fromUri, snapshot }string array or undefined
typeServer/resolveImportclient request{ sourceUri, moduleDescriptor, snapshot }string or undefined
typeServer/getComputedTypeclient request{ arg: Declaration or Node, snapshot }Type or undefined
typeServer/getDeclaredTypeclient request{ arg: Declaration or Node, snapshot }Type or undefined
typeServer/getExpectedTypeclient request{ arg: Declaration or Node, snapshot }Type or undefined

A type server should implement the messages its integrating client needs and return undefined only when the protocol result type permits that outcome.

Computed, Declared, Expected: Three Views of One Syntax Node

The three type-query requests differ only in which type they report for the same node or declaration, and confusing them is a frequent source of wrong hover text and wrong quick fixes.

RequestWhat it reportsWorked example
typeServer/getComputedTypeThe type inferred from code flow.After narrowing a to int, the computed type of b in b = a + 1 is int.
typeServer/getDeclaredTypeThe type explicitly written in the source.For def foo(a: int | str), the declared type of a is int | str.
typeServer/getExpectedTypeThe type the surrounding context expects.For the call foo(4), the expected type of the argument is int | str.

Branching on Error Codes

Standard JSON-RPC error responses carry the protocol failures, and a client should branch on the code rather than on message text.

SituationError codeHandling guidance
Stale snapshot-32802 (ServerCancelled)Fetch a fresh snapshot and retry the request.
Invalid parameters-32602 (InvalidParams)Required fields are missing or malformed, so this is a client-side bug to fix.
Internal failure-32603 (InternalError)The server failed unexpectedly while handling a valid request.

Error responses should carry enough detail for a client log to identify the failed request, but they should not expose local secrets or implementation-only state.

Common Mistakes Seen in Integration Reviews

  • Reusing a returned type handle after a snapshot change, when validity lasts only for the snapshot that produced it.
  • Comparing type ids across separate responses, or promoting a response-local id into a global cache key.
  • Assuming extra channels exist because a capability object was seen, instead of requiring both sides to advertise typeServerMultiConnection plus at least one shared transport.
  • Routing LSP lifecycle traffic, document synchronization, snapshot notifications, or typeServer/connection over an extra connection.
  • Treating InvalidHandle, which is -1, as an ordinary identifier.
  • Serving UTF-8 offsets across the wire instead of converting to the zero-based UTF-16 convention, which sends queries to the wrong syntax node.
  • Blocking the main JSON-RPC connection with background type queries that a read-only channel could have carried.
  • Skipping typeServer/getSupportedProtocolVersion and discovering a 0.x minor bump only through runtime failures.

Compatibility Verification Checklist

Run this list before calling an integration done.

  1. typeServer/getSupportedProtocolVersion returns a semver string the client supports.
  2. Every request and notification payload serializes and deserializes according to the protocol artifacts.
  3. typeServer/getSnapshot returns a non-negative snapshot, and snapshots never move backward.
  4. Document, configuration, and watched-file changes invalidate snapshots whenever they can change returned analysis data.
  5. Requests bearing a stale snapshot fail with a retryable cancellation error.
  6. Position and range conversions follow LSP zero-based UTF-16 conventions.
  7. Import-resolution requests return document URIs or no result, as the protocol result type allows.
  8. Queries for computed, declared, and expected types return well-formed Type objects, or undefined, for the snapshot the client supplied.
  9. Multi-connection support is advertised only when the server can actually serve the negotiated extra transport.
  10. Extra connections accept every TSP request allowed for the negotiated TSP version, and reject LSP traffic, state-changing notifications, typeServer/connection, and TSP requests not allowed for that version.

FAQ for Developers Landing This Integration

Do we have to speak LSP before TSP? The server only needs enough workspace and document state to satisfy the TSP requests it supports. Most clients get that state through ordinary LSP initialization and document synchronization, then send typeServer/* requests over the same JSON-RPC connection.

Can we ignore snapshot bookkeeping? No. Type handles, declarations, and symbols are valid only inside the snapshot that returned them, and stale-snapshot requests should fail with a retryable error, preferably the LSP ServerCancelled code -32802.

Is multi-connection mode required for a correct server? It is an optional performance capability. A server must still behave correctly in single-connection mode, and multi-connection support cannot be treated as necessary for protocol correctness.

Our hover results point at the wrong expression. Where should we look first? At the protocol boundary. Lines and character offsets are zero-based, characters count UTF-16 code units, so any other internal encoding must be converted before the request goes out.

May we cache a type id as a stable key? Type ids are response-local: they link TypeReference objects to earlier type objects within one serialized response graph and should not be compared across separate responses.

Which requests are safe on an extra connection? Only read-only TSP requests defined for the negotiated version, which for 0.4.1 are typeServer/getSupportedProtocolVersion, typeServer/getSnapshot, typeServer/getPythonSearchPaths, typeServer/resolveImport, and the three type queries.

Action Recommendations for the Next Sprint

  1. Make the version handshake mandatory in your client path, and treat every minor bump inside the 0.x line as potentially breaking.
  2. Wire snapshot advancement into the write path: opened-file changes, closed files, configuration changes, watched-file changes, dependency changes, and analysis-affecting cache resets.
  3. Ship single-connection correctness first, then adopt ipc extra channels only for heavy type-query traffic or background workers that must not block the main JSON-RPC connection.
  4. Add boundary conversion tests for positions and ranges, asserting zero-based lines and UTF-16 character offsets.
  5. Implement retry logic keyed on -32802, and separate handling paths for -32602 (fix the caller) and -32603 (server fault).
  6. Guard every code path against InvalidHandle, which is -1, and keep error logging descriptive without exposing local secrets or implementation-only state.
  7. Automate the compatibility verification list, including the negative test that an extra connection refuses state-changing traffic.

Protocol behavior, version numbering, and message names in this guide reflect the current TSP definition, with the underlying content aligned to Pylance 2026.4.1. For a delivery team the practical summary is short: negotiate the version, respect the snapshot clock, keep mutations on the main connection, and let read-only channels handle the parallel load.

Brand logo

IT Insights

Cloud & Infrastructure Practice Media for Enterprise Technology Decision-Makers

Cloud ComputingSecuritySoftware DevelopmentAI ApplicationsStartupsGeneralAI Data News Global

© 2026 IT Insights.

Cookie Notice

We use cookies to improve your experience and analyze website traffic. Read our Privacy Policy to learn more.