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. Afterais narrowed toint,binb = a + 1isint.getDeclaredType: the explicitly written type.aindef foo(a: int | str)isint | str.getExpectedType: what the context expects. The argument infoo(4)expectsint | 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 theexperimentalkey. - Full schemas for
Type,Declaration,Node, andmoduleDescriptorare not on that page. The authoritative definitions aretypeServerProtocol.ts,tsp.json, andtsp.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
- Terminal-Bench 4.0 Update: How to Read the New AI Scoreboard
- How to Read AI Benchmarks: Scores, Effort, Harness, and Cost Together