When a coding agent wants the type of a Python function argument today, it reads the file and guesses, or runs grep. Microsoft's Type Server Protocol (TSP) is a contract that turns that question into a single JSON-RPC request.

The announcement on the Python developer blog is dated October 21 and written by Brett Cannon. The spec lives in the Pylance repo as type-server-protocol.md. This article is my reading of those two documents. I have not connected to a real TSP server.

What it separates

Per the announcement, TSP is an open standard that extends LSP. Inside VS Code, Pylance keeps the IDE intelligence layer (completions, code understanding, Copilot Chat context), while the type checker underneath becomes an interchangeable static-analysis component. Pyright stays the default. Hooking Pyrefly into Pylance is the concrete example given, requiring Pylance 2026.4.1 or later and Pyrefly 0.60.0 or later. The post says Copilot integration and other Pylance features stay the same whichever checker is in use.

So TSP was announced as an editor-to-checker contract, not as an "AI agent API." The post does describe type checking as the foundation of static semantic intelligence for software agents, but it does not say agents call this protocol directly. Connecting agents to it is my inference from the structure.

The shape of the spec

The transport is JSON-RPC. It reuses LSP data shapes such as Position and Range, and URIs are strings. Methods carry a typeServer/ prefix. The main connection runs over stdio or Node IPC and starts with the standard LSP initialize/initialized.

Method Direction Purpose
getSupportedProtocolVersion client→server returns a semver string
getSnapshot client→server current snapshot number
snapshotChanged server→client { old, new } notification
connection client→server open or close an extra connection
getPythonSearchPaths client→server search path list
resolveImport client→server resolve an import to a document URI
getComputedType / getDeclaredType / getExpectedType client→server three type queries

The distinction among the three type queries is the most useful part in practice. Using the spec's own examples:

  • getComputedType: the type from code-flow inference. After a is narrowed to int, b in b = a + 1 is int.
  • getDeclaredType: the explicitly written type. a in def foo(a: int | str) is int | str.
  • getExpectedType: what the context expects. The argument in foo(4) expects int | str.

The three can disagree for the same name. If an agent asks "what should I pass here?", declared or expected is the right query. If it asks "what is this value right now?", it wants computed.

The snapshot is the center of the contract

A snapshot is a non-negative integer identifying a point-in-time view of analysis state. The server advances it when earlier results may be invalid, such as after file, configuration, watched-file, or dependency changes, or a cache reset. It never decreases within a session. A request carrying a stale snapshot should fail with -32802 (ServerCancelled), which tells the client to retry with a fresh one. Returned objects are valid only for the snapshot they came from unless stated otherwise, and type ids are response-local and must not be compared across responses.

For an agent this becomes a design constraint right away. Snapshots keep moving while the agent edits files, so a type result cached before an edit can be wrong after it.

Checking the retry rule against a mock

Below is a mock server and client imitating the spec's rule. It is not Pylance or Pyrefly, and I assumed LSP-style Content-Length framing: the spec page only says stdout is reserved for JSON-RPC over stdio, so that header format is my assumption. I ran it myself, and I have not checked compatibility with a real server.

STALE = -32802

class MockServer:
    def __init__(self): self.snapshot = 0
    def edit_file(self): self.snapshot += 1          # any change advances the snapshot
    def handle(self, req):
        m, p = req["method"], req.get("params", {})
        if m == "typeServer/getSnapshot":
            return {"result": self.snapshot}
        if p.get("snapshot") != self.snapshot:
            return {"error": {"code": STALE, "message": "stale snapshot"}}
        if m == "typeServer/getComputedType":
            return {"result": {"kind": 3, "id": 1, "flags": 0}}

def get_type_with_retry(server, node):
    snap = call(server, 1, "typeServer/getSnapshot")["result"]
    for attempt in range(3):
        r = call(server, 2, "typeServer/getComputedType", {"arg": node, "snapshot": snap})
        if "error" not in r:
            return r["result"], attempt
        if r["error"]["code"] != STALE:
            raise RuntimeError(r)
        snap = call(server, 1, "typeServer/getSnapshot")["result"]   # retry with a fresh snapshot
    raise RuntimeError("too many stale retries")

(call is a helper that wraps the request in Content-Length framing and hands it to the server; the full script is in the evidence folder.) After a file edit, a query with the old snapshot returns -32802, and asking again with a fresh snapshot returns a result.

old snapshot -> {'error': {'code': -32802, 'message': 'stale snapshot'}}
with retry   -> ({'kind': 3, 'id': 1, 'flags': 0}, 0)

Multi-connection, and what is still unknown

The optional multi-connection mode turns on only when both sides advertise capabilities.experimental.typeServerMultiConnection and share a transport. The only built-in transport is ipc. Extra connections may carry just six read-only requests (getSupportedProtocolVersion, getSnapshot, getPythonSearchPaths, resolveImport, and the three type queries), starting with TSP 0.4.1. Because it is a performance option, servers must still behave correctly on a single connection.

A few limits are worth remembering.

  • The version is 0.x, and the spec states minor changes may be breaking. There is a history from 0.1.0 to 0.4.1, and multi-connection sits under the experimental key.
  • Full schemas for Type, Declaration, Node, and moduleDescriptor are not on that page. The authoritative definitions are typeServerProtocol.ts, tsp.json, and tsp.schema.json.
  • The announcement gives no numbers on how much this improves coding agents, and I measured nothing. The effect needs separate verification.

An experiment design for agents

To decide whether it is worth wiring in, fix the comparison first. Run the same task set two ways.

A: the agent reads files and greps to guess types
B: the agent also has a type-query tool (retrying on snapshot mismatch)

Fixed: model, harness, tasks, effort, runs (3+ per task)
Record: pass rate, type-error rework count, tool calls, cost, time

Even if B looks better, separate whether the type queries helped or whether one more tool simply made the agent more careful. The reasoning is the same as the harness-and-effort caveat for benchmark scores.

Takeaway

TSP is a JSON-RPC contract for swapping type checkers, and the snapshot with its -32802 retry rule sits at its center. Connecting it to an agent opens the possibility of querying instead of guessing, but the benefit has not been shown with numbers. A small task-set comparison like the one above is the cheapest way to check.

The paid promotion at the end is a course on Claude Code harnesses and agent workflows. It does not teach Pylance or TSP implementation.

Related reading

Sources