cursor-core
Unofficial Python client for Cursor’s agent.v1.AgentService/Run
endpoint. Import name is cursor_core.
The IDE chat path is Connect-RPC bidirectional streaming over HTTP/2.
httpx cannot do true full-duplex (it finishes the request body before
reading the response), so this library drives the H2 connection with
h2 and answers mid-stream exec, KV, wait, and checkpoints on the same
stream.
Not affiliated with Cursor. The protocol is undocumented and can change when the desktop app or CLI updates. MIT licensed.
Install
Python 3.11+. Runtime: h2, httpx, certifi, protobuf>=5.29,<6.
pip install -e ".[dev]"
There is no PyPI release yet. Point pip at this repo (or a git URL).
Authentication
CursorClient() with no token uses load_auth():
- First file that has
accessToken/access_token(top-level orauthInfo):~/.config/cursor/auth.json,~/.cursor/auth.json,~/.cursor/cli-config.json - else pass
CursorClient(access_token=...)orRunConfig.access_token
The token is re-read from disk on each request so an agent login or
desktop refresh is picked up without restarting. Expired JWTs raise
AuthError. This library does not implement OAuth refresh.
Never commit tokens or auth.json.
How it works
Layers, bottom to top:
| Module | Role |
|---|---|
cursor_core.framing |
Connect-RPC 5-byte frames + schema-free protobuf |
cursor_core.wire |
AgentServerMessage parse; exec arg/result codecs |
cursor_core.auth |
endpoints + credential load |
cursor_core.models |
live catalogue + gpt-5.5/medium/272k-style ids |
cursor_core.engine |
run_turn — the H2 loop |
cursor_core.client |
CursorClient / AgentSession |
A Run looks like this:
- Client sends
AgentClientMessage{f1: AgentRunRequest} - Server heartbeats / acks; may ask for request context
- Server asks the client to run a tool (
ExecServerMessage) - Client replies on the same stream with
ExecClientMessage - Text / reasoning deltas; more exec / KV / wait as needed
TurnEnded, then a Connect trailer (flag0x02)
If you persist Checkpoint.state (conversation_state blob) plus KV
blobs, the next Run can send only the new user message. If Cursor
rejects the checkpoint, retry that turn with the full history.
A stream that closes before turn_ended is end_reason=dropped. Stall
and turn clocks pause while a local tool or wait is in flight.
Quickstart
import asyncio
from cursor_core import CursorClient, RunConfig
async def main():
client = CursorClient()
models = await client.list_models()
print(models["default_id"])
session = client.session()
async for kind, payload in session.run(
[{"role": "user", "content": "What is 2+2?"}],
model="auto",
run_config=RunConfig(chat_only=True),
):
if kind == "text":
print(payload, end="", flush=True)
asyncio.run(main())
chat_only=True tells the model not to poke the local filesystem.
Without it, Cursor will request read / grep / shell / … and the
engine needs an exec_handler (or you accept the built-in default,
which serves grep/glob against workspace_root and rejects the rest).
Hosting a turn
Prefer AgentSession if you want conversation continuity. Prefer
run_turn if you already own IDs, rules, and MCP blobs.
from cursor_core import AgentSession, Checkpoint, CursorClient, RunConfig, run_turn
client = CursorClient()
session = AgentSession(client, checkpoint=Checkpoint())
async def exec_handler(exec_type, server_msg) -> list[bytes]:
# exec_type is "read" / "shell" / "write" / "grep" / "edit" / "mcp" / …
# Return protobuf bodies for ExecClientMessage (see cursor_core.wire).
from cursor_core.wire import build_read_error_response
return [build_read_error_response(
server_msg.exec_id, server_msg.exec_id_str,
getattr(server_msg, "read_path", ""), "not implemented",
)]
async for kind, payload in session.run(
[{"role": "user", "content": "open README.md and summarise it"}],
exec_handler=exec_handler,
):
print(kind, payload[:80])
# session.checkpoint.state / .conversation_id / .blobs → persist, reuse
Yielded (kind, payload):
| kind | payload |
|---|---|
text |
token |
reasoning |
thinking token (if stream_reasoning) |
tool |
UI label for a tool card |
checkpoint |
base64 conversation_state blob |
end_reason |
clean / stall / timeout / cancel / error / dropped |
error |
message |
native_fallback |
Cursor rejected the checkpoint; retry with history |
Injected callbacks (all optional):
exec_handler(exec_type, server_msg) -> list[bytes]— tool results, framed as protobuf bodies. Default: local grep/glob, reject everything else.kv_handler(op, blob_id, blob_data) -> bytes | None— Cursor’s blob store (set/get). Default: in-memory, copied toRunConfig.session_out["blob_store"]at the end.approval_handler(kind, query_id) -> bool— web search/fetch gate. Default: approve.
RunConfig.steer_queue is (message_id, text) follow-ups applied at
the next tool boundary (AgentClientMessage field 4).
Timeouts
Env vars set defaults; RunConfig.timeouts overrides per turn.
| Var | Default | Meaning |
|---|---|---|
CURSOR_TURN_TIMEOUT_SEC |
1200 | hard ceiling; 0 = off |
CURSOR_STALL_SEC |
60 | silence including heartbeats |
CURSOR_REASONING_STALL_SEC |
60 | no visible output while thinking |
CURSOR_TOOL_STALL_SEC |
90 | silence cap while a Cursor-side tool runs |
CURSOR_STALL_RETRIES |
1 | reopen after a stall |
CURSOR_WAIT_CAP_MS |
30 min | native await / wait exec cap |
CURSOR_WORKSPACE_ROOT |
. |
default grep/glob root |
CURSOR_DEBUG_WIRE |
off | log every inbound field to stderr |
Tests
Offline only. They patch the H2 loop or parse recorded frames — no Cursor account required.
pip install -e ".[dev]"
pytest
Redacted wire fixtures live in tests/fixtures/cursor_frames/.
Forgejo Actions: sonarqube.yml runs coverage, scanner, and the
quality gate. Repo secrets: SONAR_HOST_URL, SONAR_TOKEN. Project
key kenzim_cursor-core. There is no separate build/lint workflow.
Layout
| Path | What |
|---|---|
src/cursor_core/ |
public package |
tests/ |
framing, wire, engine (mocked H2) |
tests/fixtures/cursor_frames/ |
redacted Run frames |
.forgejo/workflows/sonarqube.yml |
SonarQube on Forgejo Actions |
Metadata
Release files for cursor-core 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cursor_core-0.1.0.tar.gz | 67.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cursor_core-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.9 kB
Release files / cursor_core-0.1.0.tar.gz
| Download URL | cursor_core-0.1.0.tar.gz |
|---|---|
| Size | 67.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f9b06ff7e0e1c54741144997e657316b6c49fc44d6747f05ab87c695fd71c04
|
|
BLAKE2b-256 checksum How to use checksums |
5f1fd2701e36afc3889bf6e5147a4f47424cb6678b6e311f20ee01d3935c1565
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / cursor_core-0.1.0-py3-none-any.whl
| Download URL | cursor_core-0.1.0-py3-none-any.whl |
|---|---|
| Size | 55.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6db21ea06ac9d492e218eddb2e44909ae3d31013fb9aa167a7b3e3bf8af1df7f
|
|
BLAKE2b-256 checksum How to use checksums |
9f455f63c9d5a109d687a5211b03b2eac20a62eb41f5d7d74dd6b79b0cb73510
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log