The public home of the Cursor SDK bridge protocol: the stable sdk.v1
protobuf contract that lets you drive Cursor agents
from any language, without depending on the TypeScript
(@cursor/sdk) or Python
(cursor-sdk) SDKs directly.
The bridge is a small local server that embeds the TypeScript SDK
(@cursor/sdk) as a library and exposes its full surface — creating agents,
sending messages, streaming runs, custom tools, artifacts — over
Connect/gRPC-Web using the protobuf definitions in
this repository. An adapter is anything that spawns
the bridge and speaks sdk.v1 to it: an SDK for a new language, a service
integration, or a one-off script.
┌────────────────-┐ spawn + Connect RPCs ┌────────────────────┐ HTTPS ┌─────────────┐
│ your adapter │ ──────────────────────► │ cursor-sdk-bridge │ ─────────► │ Cursor API │
│ (any language) │ ◄────────────────────── │ (local process) │ │ │
└────────────────-┘ callback RPCs (tools, └────────────────────┘ └─────────────┘
custom stores)
You can use this repository to create adapters in languages like Go, Rust, Java, and more. The way to do it: point a coding agent at this repository and tell it to follow Agent: start here below — a complete milestone-by-milestone build plan that takes the agent from codegen to a full, verified SDK for your language.
- Building an adapter for a new language? Give a Cursor agent this repository and the Agent: start here guide.
- Scripting agents from TypeScript or Python? Use the official SDKs —
@cursor/sdkon npm orcursor-sdkon PyPI. You do not need this repository.
| Path | Contents |
|---|---|
proto/sdk/v1/ |
The sdk.v1 protobuf contract. Generated — do not edit. Regenerated automatically on every SDK release. |
proto/manifest.json |
Release metadata: protocol ("sdk.v1"), sdkVersion, and the source commit. |
docs/ |
The protocol guide: lifecycle, services, streaming, errors, versioning. |
examples/ |
Minimal adapters in other languages, each with its own buf.gen.yaml. |
Note:
proto/is owned by Cursor's release automation and rewritten on every release. Every release pushes an annotated tagvX.Y.Zmatching the released@cursor/sdknpm /cursor-sdkPyPI version and publishes a GitHub release with the standalone bridge archives attached. Pull requests must never touchproto/.
Download cursor-sdk-bridge-standalone-<os>-<arch>.tar.gz for your platform
(os linux|darwin|win32, arch x64|arm64, win32 is x64 only) from this
repository's latest release
— every release attaches the standalone bridge archives and a
SHA256SUMS.txt. The same bridge is embedded in the cursor-sdk Python
wheels on PyPI. See docs/protocol.md for the archive
layout and the spawn-and-handshake lifecycle.
docs/protocol.md— spawn-and-handshake lifecycle, authentication, CLI flags, distributiondocs/services.md— the role of each service, including the adapter-implemented callback servicesdocs/streaming.md— run stream semantics: envelopes, offsets, resume, keepalivesdocs/errors.md— the structured error model fromsdk_errors.protodocs/smoke-test.md— a curl-only smoke test of every core RPC: the "is it me or the bridge?" oracledocs/versioning.md— tag policy and thesdk.v1compatibility promise
This section is the build plan for coding agents (and humans) building a
full Cursor SDK for a new language on top of the sdk.v1 bridge protocol —
codegen, managed bridge lifecycle, client/agent/run API design, streaming,
errors, and the adapter-side callback services.
An adapter spawns cursor-sdk-bridge and speaks the sdk.v1 Connect
protocol to it. The end state of this guide is not a demo script but a real
SDK: a library another developer can install and use to script Cursor agents
without knowing the bridge exists. Read docs/protocol.md
first; examples/python-adapter/ is a working
miniature of the target shape — one module per architecture-table component,
over a hand-rolled transport that keeps the wire format visible — and
docs/streaming.md / docs/errors.md
cover streams and failures.
Seven files under proto/sdk/v1/, package sdk.v1, self-contained apart from
Google well-known types:
| File | Role |
|---|---|
sdk_agent_service.proto |
SdkAgentService — create/resume agents, send messages, stream runs, artifacts, usage. |
sdk_cursor_service.proto |
SdkCursorService — client-level operations (identity, models, repositories). |
sdk_bridge_control_service.proto |
SdkBridgeControlService — bridge lifecycle (ping, version, shutdown). |
sdk_custom_tool_callback_service.proto |
SdkCustomToolCallbackService — implemented by the adapter; the bridge calls back into it to execute user-defined custom tools. |
sdk_store_callback_service.proto |
SdkStoreCallbackService — implemented by the adapter for custom agent stores. |
sdk_messages.proto |
Shared messages, enums, and the run-stream envelope. |
sdk_errors.proto |
Structured error details and the stable error-code taxonomy. |
Cursor's official SDKs converge on the same shape. Aim for it, adapted to your language's idioms:
| Component | Responsibility |
|---|---|
| Bridge manager | Locate the bridge (env override → bundled/downloaded archive), spawn it, perform the ready-line handshake, expose the endpoint, shut it down (RPC → wait → kill). One managed bridge per client, created lazily on first use; also allow attaching to an externally supplied endpoint. |
| Transport | Connect-over-HTTP/1.1 client: unary POSTs and server-stream framing, bearer auth on every request, translation of Connect errors into your error types. Generated stubs or hand-rolled (see examples/python-adapter/). |
Client |
Owns the bridge manager + transport. Typed low-level methods mirroring SdkAgentService (create_agent, send, wait_live_run, observe_run, cancel_run, list_agents, ...). Everything else builds on it. |
Agent handle |
create(options) / resume(id) / get / list constructors; send(message) -> Run; close, archive, delete; custom-tool registration. Holds agent_id + model. |
Run handle |
The streaming surface: iterate events; convenience accessors (assistant text iterator, blocking wait() → result, terminal text()); observe(after_offset) for resume; cancel(). Tracks the last seen offset. |
Cursor catalog |
me(), models(), repositories() from SdkCursorService. |
| Errors | One base error plus a taxonomy mapped from Connect codes + SdkErrorDetails.sdk_error_code (auth, not-found, rate-limit, busy, validation, ...). Preserve request_id, retry_after, rate_limit on the error object. |
| Callback servers | Optional loopback Connect servers implementing SdkCustomToolCallbackService and SdkStoreCallbackService, so users can define tools and stores in your language. |
A north-star usage sketch (translate to your language):
client = Client() # spawns/attaches the bridge lazily
agent = client.agents.create(model="composer-2", local={"cwd": ["/repo"]})
run = agent.send("Summarize this repository.")
for text in run.iter_text():
print(text)
result = run.wait()
agent.close()
client.close() # shuts the bridge downPlus a one-liner for the simplest case (prompt(...): create → send → wait →
close) and a context-manager/defer/RAII form so the bridge can never leak.
- Pin the contract to the latest release of this repository: the newest
vX.Y.Ztag. Get the protos fromproto/sdk/v1/at that tag (if you are working outside a checkout of this repo, vendor them into your project — the bridge archive for the same version also ships an identicalproto/sdk/v1/). Never edit anything underproto/— it is generated. - The target language needs (a) a protobuf runtime and (b) an HTTP/1.1
client. A Connect client library is ideal
but not required — every RPC is
POST http://<host>:<port>/sdk.v1.<Service>/<Method>with a protobuf (content-type: application/proto) or JSON (application/json) body. Classic gRPC will not work: the bridge serves HTTP/1.1 only. - Running a real turn needs a
CURSOR_API_KEY(cursor.com/dashboard) — set it in the bridge's environment and pass it explicitly asoptions.api_key/ per-callapi_key(see Milestone 4 anddocs/protocol.md).
Work through the milestones below in order, and keep a runnable demo/test at every milestone — each one builds on a verified previous layer.
Copy examples/python-adapter/buf.gen.yaml as a template: point inputs at
your copy of the protos (directory: ../../proto when working inside this
repository) and swap the plugins for the target language's protobuf + Connect
plugins. For compiled languages, use buf's managed mode to override
language package options — the published protos carry Cursor-internal values for
go_package, java_package, and friends. Only sdk/v1/*.proto and Google
well-known types are involved; no other dependencies. Commit the
buf.gen.yaml, gitignore the gen/ output.
If the language has no Connect plugin, generate plain protobuf messages and
hand-write the tiny HTTP layer (unary = one POST; server streams = the
Connect streaming envelope: 1-byte flags + 4-byte big-endian length frames,
end-of-stream flag 0x02 carrying a JSON EndStreamResponse with any error).
examples/python-adapter/cursor_adapter/_transport.py does exactly this in
~100 lines.
- Locate the bridge: an env override such as
CURSOR_SDK_BRIDGE_BINfirst, then your package's bundled/downloaded archive. Standalone archives are attached to this repo's GitHub releases (cursor-sdk-bridge-standalone-<os>-<arch>.tar.gzon https://github.com/cursor/sdk-bridge/releases/latest, os:linux|darwin|win32, arch:x64|arm64) — download from the release for thevX.Y.Ztag you pinned. Each unpacks flat: the executable isbin/cursor-sdk-bridge,.exeon Windows. - Spawn with
CURSOR_API_KEYin the environment,--workspace <dir>for local agents, andCURSOR_SDK_CLIENT_LANGUAGE=<language>for attribution. - Handshake: capture stderr, scan for the literal prefix
cursor-sdk-bridge ready(trailing space), parse the JSON after it, validateschemaVersion == 1,transport == "tcp",protocol == "connect", ignore unknown fields. Apply a ~30s startup timeout; if the process exits first, surface its captured stderr. Keep draining stderr forever afterwards — a full pipe blocks the bridge. Never log the raw discovery line (older bridges inline the token). - Read the bearer token from the
authTokenFilepath, trimmed. - Shutdown:
SdkBridgeControlService.Shutdown(or SIGTERM), wait ~5s, then kill. Make this run on client close and on interpreter/process exit so a crashed caller cannot leak bridges. - Support attaching to an already-running bridge (explicit URL + token) — useful for tests and for hosts that manage the process themselves.
- Send
Authorization: Bearer <token>on every request — unary and streaming (a common bug: interceptor APIs often cover only unary). Missing/wrong token ⇒UNAUTHENTICATED. - Verify with
SdkBridgeControlService.Ping, thenGetVersion(expectprotocol_version == "sdk.v1"; capabilities gate optional features). - When an RPC fails and you suspect your own encoding or transport, run the
same RPC via the curl-only sequence in
docs/smoke-test.mdbefore bisecting your code — it isolates adapter bugs from bridge/key/environment problems in one pass. - Build the error layer now, not last: decode
sdk.v1.SdkErrorDetailsfrom failed RPCs (docs/errors.mdhas the taxonomy and wire encoding) and mapsdk_error_code+ Connect code onto your language's exception/error hierarchy. Expose the fullrequest_id,retry_after, andrate_limit. Parse protobuf JSON with unknown-field tolerance everywhere.
SdkAgentService.CreateAgentwithoptions.local.cwd = ["<workspace>"], an explicitoptions.model— local agents require one; discover IDs viaSdkCursorService.ListModels(catalog calls require a per-callapi_key; there is no env fallback) — and an explicitoptions.api_key. Always setoptions.api_key: the bridge'sCURSOR_API_KEYenv var is not a substitute — not every operation falls back to it on every bridge build, and without the option step 2 can fail withInvalid User API Key.SdkAgentService.Sendwith theagent_idand aUserMessage{text}; wrap the server stream in yourRunhandle perdocs/streaming.md:- dispatch on the
envelopeoneof; ignore messages with no case set (keepalives) and unknown cases; sdk_message: dispatch ontype(system,assistant,tool_call,status, ...); payloads are JSON objects (google.protobuf.Struct). On failure the human-readable reason arrives in thestatuspayload'smessage— surface it, sinceRunStreamResult.error_codecan be empty;- track the last non-empty
offset;resultthendoneend the run; - a dropped stream does not cancel the run —
Run.observe()resumes viaObserveRun+after_offset(only pass offsets that came fromObserveRunitself; liveSendoffsets are a different numbering — seedocs/streaming.md), andwait()falls back toWaitLiveRun.
- dispatch on the
- Layer the conveniences on the raw event stream: assistant-text iterator,
blocking
wait(), terminaltext(),cancel().
Fill out the rest of SdkAgentService on Client/Agent: ResumeAgent,
GetAgent/ListAgents (pagination cursors), ArchiveAgent/Unarchive/
Delete/Close, ListRuns/GetRun/GetRunConversation,
ListAgentMessages, artifacts (ListArtifacts + chunked
DownloadArtifact), GetUsage (cloud only), and the Cursor catalog
(Me, ListModels, ListRepositories). These are mechanical once
milestones 1–4 work.
These invert direction: the SDK runs a loopback Connect server and the
bridge authenticates to it with a bearer token the SDK chooses. Validate that
token on every callback, exactly as the bridge validates yours. Gotchas that
cost real debugging time (details in docs/services.md):
callback POSTs may use chunked transfer-encoding (decode it — minimal HTTP
servers often don't); store outputs must be the bare record, not the wrapped
input envelope; tool results are Structs, so scalar returns need wrapping
in an object.
- Custom tools — implement
SdkCustomToolCallbackService.CallCustomTool(execute the named user function with the Struct args, return a Struct result). Declare tool metadata inLocalAgentOptions.custom_toolson CreateAgent; register the server via--tool-callback-url/--tool-callback-auth-tokenorSdkBridgeControlService.SetToolCallback. Design the user-facing API as "register a function with a schema", not "implement an RPC service". - Custom stores — implement
SdkStoreCallbackService.CallStore(substoresagents|runs|runEvents|checkpoints; methodsget|create|update|delete|list|append; checkpoint blobs are base64). Launch the bridge withLocalAgentStoreConfig{type:"custom"}(e.g.--local-store '{"type":"custom"}') plus--store-callback-url/--store-callback-auth-token(launch-time only).
Functional (run against a real bridge):
- Handshake: ready line parsed, token read from file,
Pingsucceeds; startup timeout and exit-before-ready both produce useful errors. - A request without
Authorizationfails withUNAUTHENTICATED, and it maps to your auth error type. - One full turn through the public API (
client → agent → run): stream yields events, terminal result observed, against a realCURSOR_API_KEY. - Keepalive frames (empty envelope) are ignored; a >15s tool pause does
not break the stream; unknown envelope cases and
SdkMessage.types are skipped silently. - Bridge exits cleanly on client close; killed on timeout; no orphan process after the host program exits or crashes.
- Failed RPCs surface
sdk_error_codeand the fullrequest_id.
API quality (review against the architecture table):
- A newcomer can run one prompt in ≤5 lines without touching proto types.
- Raw proto/transport types do not leak into the public API surface.
-
Runsupports both incremental consumption and fire-and-wait(). - Errors are catchable by class, not by string matching.
- The bridge process is invisible in the happy path and controllable (endpoint attach, custom binary path) when needed.