Python client for the Cursor SDK bridge.
Project description
cursor-sdk
Python client for the Cursor SDK bridge.
The Python SDK mirrors the public TypeScript SDK concepts from
@cursor/sdk, but the main interface is Python-first: simple Agent
helpers, explicit clients when needed, snake_case option helpers, typed
dataclasses, and normal iteration for streams and pages. It talks to a
local cursor-sdk-bridge process, which embeds the TypeScript SDK and
exposes the stable sdk.v1 Connect protocol.
Public beta
The Python SDK is in public beta. APIs may change before general availability.
Overview
The SDK wraps local and cloud runtimes behind one interface. You write the same code regardless of where the agent runs.
| Runtime | What it does | When to use |
|---|---|---|
| Local | Runs the agent through the local SDK bridge. Files come from disk. | Dev scripts and CI checks against a working tree. |
| Cloud (Cursor-hosted) | Runs in an isolated VM with your repo cloned in. Cursor runs the VMs. | When the caller does not have the repo, you want many agents in parallel, or runs need to survive the caller disconnecting. |
| Cloud (self-hosted) | Same shape, but targets a self-hosted pool. | Same reasons as Cursor-hosted, plus code, secrets, and build artifacts must stay in your environment. |
Runtime is picked by which key you pass to Agent.create() (local or
cloud). Use the same CURSOR_API_KEY for either.
Authentication
Set CURSOR_API_KEY (or pass api_key) before creating an agent.
The SDK accepts user API keys and service account API keys for both local and cloud runs. Team Admin API keys are not yet supported.
export CURSOR_API_KEY="your-key"
Installation
pip install cursor-sdk
Platform-specific wheels bundle cursor-sdk-bridge and its Node
runtime, so no separate bridge install is needed on macOS arm64/x64,
Linux arm64/x64, or Windows x64.
For source installs, custom bridge builds, or unsupported platforms,
set CURSOR_SDK_BRIDGE_BIN to a bridge launcher on disk. To connect
to an already-running bridge, pass CURSOR_SDK_BRIDGE_URL and
CURSOR_SDK_BRIDGE_TOKEN to Client(...).
Preview access
The package is installable publicly, but API access is allowlisted
during the preview. If your team is not enabled for
sdk_python_preview_access, Cursor.me() and agent endpoints raise
IntegrationNotConnectedError with a link to request access:
https://cursor.com/sdk/python.
Quick start
import os
from cursor_sdk import Agent, LocalAgentOptions
agent = Agent.create(
model="composer-2",
local=LocalAgentOptions(cwd=os.getcwd()),
)
try:
run = agent.send("Summarize what this repository does")
for message in run.messages():
print(message)
result = run.wait()
print(result.status, result.result)
finally:
agent.close()
Agent.create and explicit-client client.agents.create resolve
api_key in this order:
- The
api_key=keyword argument, if you pass one explicitly. - The
CURSOR_API_KEYenvironment variable, when env-var fallback is enabled (the default for local-bridge clients started by the SDK itself; opt in for remote bridges withClient(allow_api_key_env_fallback=True)).
It raises ConfigurationError if neither is set, or if model is
missing — Agent.create() with no arguments is rejected, not a
silent no-op.
There is no module-level cursor_sdk.api_key = "..." global. Use
CURSOR_API_KEY or the api_key= argument.
Async usage
The async client mirrors the sync surface and is recommended for servers,
bots, and concurrent agent orchestration. It is powered by
httpx.AsyncClient.
import asyncio
from cursor_sdk import AsyncClient, LocalAgentOptions
async def main() -> None:
async with await AsyncClient.launch_bridge(workspace=".") as client:
agent = await client.agents.create(
model="composer-2",
local=LocalAgentOptions(cwd="."),
)
run = await agent.send("Summarize what this repository does")
async for message in run.messages():
print(message)
result = await run.wait()
print(result.status)
asyncio.run(main())
AsyncAgent, AsyncClient, AsyncRun, and AsyncCursor are exported
from both cursor_sdk and cursor_sdk.asyncio. There is no global
async default client. Instantiate AsyncClient explicitly (or use
AsyncClient.launch_bridge(...) as an async context manager) so each
event loop owns its own handle. Sync and async clients should not be
mixed in the same code path.
Explicit client lifecycle
The sync helpers (Agent.* and Cursor.*) start or reuse a module-level
bridge client and close it automatically at process exit. Use
CursorClient when you need explicit lifecycle control, a custom bridge
endpoint, custom HTTP options, or multiple workspaces in one process:
from pathlib import Path
from cursor_sdk import CursorClient, LocalAgentOptions
with CursorClient.launch_bridge(workspace=Path.cwd()) as client:
agent = client.agents.create(
model="composer-2",
local=LocalAgentOptions(cwd=Path.cwd()),
)
run = agent.send("Summarize what this repository does")
result = run.wait()
print(result.status, result.result)
Custom HTTP clients
Both clients accept a custom httpx client for proxies, transports, and
other advanced httpx configuration:
import httpx
from cursor_sdk import CursorClient, DefaultHttpxClient
client = CursorClient(
bridge.endpoint,
http_client=DefaultHttpxClient(
proxy="http://my.test.proxy.example.com",
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
),
)
from cursor_sdk.asyncio import AsyncClient, DefaultAsyncHttpxClient
async_client = AsyncClient(
endpoint=bridge.endpoint,
http_client=DefaultAsyncHttpxClient(proxy="http://my.proxy.example.com"),
)
DefaultHttpxClient / DefaultAsyncHttpxClient retain the SDK's defaults
for timeout and follow_redirects. Plain httpx.Client /
httpx.AsyncClient will use httpx's defaults instead.
Configuring timeouts per call
Both clients expose with_options(...), mirroring the OpenAI SDK. You can
override timeouts and retry behavior for a small group of calls:
short = client.with_options(timeout=5.0, max_retries=2)
short.agents.create(...)
# Async equivalent:
short = async_client.with_options(timeout=5.0, max_retries=2)
await short.agents.create(...)
with_options returns a shallow copy that shares the underlying transport
with the original. Closing one does not close the other.
Creating agents
from cursor_sdk import (
Agent,
CloudAgentOptions,
CloudRepository,
LocalAgentOptions,
)
agent = Agent.create(
model="composer-2",
local=LocalAgentOptions(cwd="/path/to/repo"),
)
cloud_agent = Agent.create(
model="composer-2",
cloud=CloudAgentOptions(
repos=[
CloudRepository(
url="https://github.com/your-org/your-repo",
starting_ref="main",
),
],
auto_create_pr=True,
),
)
agent.agent_id is populated immediately. Local agents get an agent-
ID; cloud agents get a bc- ID. agent.model is a typed
ModelSelection, so agent.model.id and agent.model.params work
directly.
Session environment variables
For cloud agents, pass env_vars when a run needs short-lived
credentials or other values that should live only with that agent.
agent = Agent.create(
model="composer-2",
cloud=CloudAgentOptions(
repos=[CloudRepository(url="https://github.com/your-org/your-repo")],
env_vars={
"STAGING_API_TOKEN": os.environ["STAGING_API_TOKEN"],
},
),
)
These values are encrypted at rest, injected into the cloud agent's
shell, and deleted with the agent. env_vars cannot be used with a
caller-supplied agent_id. Variable names cannot start with CURSOR_.
Model parameters
Use model.params to pass per-model options such as reasoning effort or
max mode. Parameter IDs and values vary by model. Use
Cursor.models.list() to discover supported parameters and preset
variants for your account.
from cursor_sdk import Agent, Cursor, LocalAgentOptions, ModelSelection, ModelParameterValue
models = Cursor.models.list()
composer = next((m for m in models if m.id == "composer-2"), None)
print(composer.parameters if composer else [])
agent = Agent.create(
model=ModelSelection(
id="composer-2",
params=[ModelParameterValue(id="thinking", value="high")],
),
local=LocalAgentOptions(cwd=os.getcwd()),
)
Passing raw wire dicts (advanced)
The SDK still accepts raw dicts in addition to typed dataclasses, which can be convenient for short scripts or for piping in externally-supplied JSON. Snake_case keys are normalized, and bridge-shaped camelCase keys remain available as an escape hatch:
agent = Agent.create(
{
"api_key": os.environ["CURSOR_API_KEY"],
"model": {"id": "composer-2"},
"local": {"cwd": os.getcwd()},
}
)
The dataclass form is preferred for application code — IDE autocomplete, type checking, and consistency with the rest of the Python surface all work better against the typed shapes.
SDKAgent
The handle returned by Agent.create() and Agent.resume().
run = agent.send("Find the bug in src/auth.py")
agent.reload()
artifacts = agent.list_artifacts()
content = agent.download_artifact(artifacts[0].path)
agent.close()
Use with for automatic cleanup:
with Agent.create(model="composer-2", local=LocalAgentOptions(cwd=os.getcwd())) as agent:
run = agent.send("Explain this repository")
run.wait()
When you use the static Agent.* or Cursor.* helpers without passing a
client=, the SDK starts or reuses a module-level bridge client. It is
closed automatically at process exit, and you can close it explicitly with
close_default_client().
Agent.prompt()
One-shot convenience: creates an agent, sends a single prompt, waits for the run to finish, and disposes.
result = Agent.prompt(
"What does the auth middleware do?",
AgentOptions(
model="composer-2",
local=LocalAgentOptions(cwd=os.getcwd()),
),
)
Sending messages
Each agent.send() returns a Run. The agent retains conversation
context across runs; the run is the unit of work for one prompt.
run = agent.send("Find the bug in src/auth.py")
for message in run.messages():
if getattr(message, "type", "") == "assistant":
print(message.message)
run2 = agent.send("Fix it and add a regression test")
run2.wait()
To send images alongside text:
run = agent.send(
{
"text": "What's in this screenshot?",
"images": [{"data": base64_png, "mimeType": "image/png"}],
}
)
Waiting without streaming
result = run.wait()
print(result.status)
print(result.result)
print(result.model)
print(result.duration_ms)
print(result.git)
Cancelling a run
run.cancel()
Reading run state
print(run.id)
print(run.status)
stop = run.on_did_change_status(lambda status: print(status))
stop()
turns = run.conversation()
Per-run model override
run = agent.send(
"Plan the refactor",
SendOptions(
model=ModelSelection(
id="composer-2",
params=[ModelParameterValue(id="thinking", value="high")],
),
),
)
The model passed to agent.send() overrides the selection for that
run and becomes sticky on the agent. Subsequent sends without an
override continue to use the new model.
Streaming raw deltas
Pass on_delta and on_step callbacks in SendOptions. The Python SDK
sets the bridge's enableDeltas / enableSteps flags and calls the
callbacks as matching stream events arrive.
from cursor_sdk import SendOptions
run = agent.send(
"Refactor the utils module",
SendOptions(
on_delta=lambda update: print(update.text)
if update.type == "text-delta"
else None,
on_step=lambda step: print(step.type),
),
)
run.wait()
Stream events
run.messages() and its older alias run.stream() yield typed SDK
message dataclasses. Discriminate on message.type. All messages
include agent_id and run_id when the runtime provides them.
run.events() yields lower-level RunStreamEvent envelopes. Use it
when you need offsets, terminal result envelopes, or raw interaction
updates.
Supported message dataclasses:
SDKSystemMessageSDKUserMessageEventSDKAssistantMessageSDKThinkingMessageSDKToolUseMessageSDKStatusMessageSDKTaskMessageSDKRequestMessage
Tool payload fields are unstable. Treat args and result as
object/dict and parse defensively.
Interaction updates
InteractionUpdate is the raw delta type passed to on_delta. The
Python SDK exposes typed dataclasses for the documented update variants,
including TextDeltaUpdate, ThinkingDeltaUpdate,
ToolCallStartedUpdate, ToolCallCompletedUpdate,
PartialToolCallUpdate, TokenDeltaUpdate, StepStartedUpdate,
StepCompletedUpdate, TurnEndedUpdate, SummaryUpdate, and shell
output deltas. Unknown future update types fall back to
UnknownInteractionUpdate.
The concrete update / step subclasses live in cursor_sdk.events:
from cursor_sdk.events import TextDeltaUpdate, ToolCallStartedUpdate
if isinstance(update, TextDeltaUpdate):
print(update.text)
They remain importable from cursor_sdk too for backward
compatibility, but new code should import from cursor_sdk.events so
the package root stays focused on the high-level surface.
Conversation types
run.conversation() returns a typed per-turn view parsed from the bridge
conversation JSON:
ConversationTurnAgentConversationTurnShellConversationTurnConversationStepAssistantMessageThinkingMessageShellCommandShellOutput
Resuming agents
agent = Agent.resume("bc-abc123")
run = agent.send("Also update the changelog")
run.wait()
Inline MCP servers are not persisted across resume. Pass them again on resume if the run needs those servers.
Inspecting agents and runs
from cursor_sdk import CursorClient
with CursorClient.launch_bridge(workspace=os.getcwd()) as client:
agents = client.agents.list(runtime="local", cwd=os.getcwd())
for agent_info in agents.auto_paging_iter():
print(agent_info.agent_id)
info = client.agents.get(agents.items[0].agent_id)
runs = client.agents.list_runs(info.agent_id)
run = client.agents.get_run(runs.items[0].id)
Use Agent.get(agent_id).list_messages() to read the message history of a
specific agent.
List endpoints return ListResult[T]. You can use .items and
.next_cursor directly, iterate the current page with for item in page,
or iterate all pages with .auto_paging_iter(). Async list endpoints
return AsyncListResult[T]; async for item in page walks the current
page, and async for item in page.auto_paging_iter() walks every page
in the result set — symmetric with the sync API.
Cloud agent lifecycle
Cloud agents stay in your team's workspace until you archive or delete them. Archived agents remain readable and can be restored. Each operation has two forms:
# By id (no agent handle required):
Agent.archive(agent_id)
Agent.unarchive(agent_id)
Agent.delete(agent_id)
# On an existing agent handle:
agent.archive()
agent.unarchive()
agent.delete()
The earlier Agent.archive_agent(...) / Agent.unarchive_agent(...) /
Agent.delete_agent(...) and Agent.lifecycle.archive(...) /
Agent.lifecycle.unarchive(...) / Agent.lifecycle.delete(...)
spellings still work but emit DeprecationWarning and will be removed
in a future release.
The Cursor namespace
Account-level and catalog reads. All methods take optional api_key and
otherwise fall back to CURSOR_API_KEY.
from cursor_sdk import Cursor
me = Cursor.me()
models = Cursor.models.list()
repositories = Cursor.repositories.list()
MCP servers
Agents can pick up MCP servers from inline definitions, project/user settings, plugins, and dashboard-managed configuration depending on the runtime.
from cursor_sdk import (
Agent,
AgentOptions,
HttpMcpServerConfig,
LocalAgentOptions,
McpAuth,
StdioMcpServerConfig,
)
agent = Agent.create(
AgentOptions(
model="auto",
local=LocalAgentOptions(cwd=os.getcwd()),
mcp_servers={
"docs": HttpMcpServerConfig(
url="https://example.com/mcp",
auth=McpAuth(client_id="client-id", scopes=["read", "write"]),
),
"filesystem": StdioMcpServerConfig(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", os.getcwd()],
cwd=os.getcwd(),
),
},
)
)
Flat dicts ({"type": "http", "url": ...} / {"type": "stdio", "command": ...})
are still accepted as a quick-script convenience. The nested bridge wire
form ({"http": {...}} / {"stdio": {...}}) is also accepted as an
escape hatch for callers that already have serialized bridge config.
The SDK also exposes HttpMcpServerConfig, SseMcpServerConfig, and
StdioMcpServerConfig helper dataclasses.
Subagents
Define named subagents inline:
from cursor_sdk import AgentDefinition
agent = Agent.create(
{
"apiKey": os.environ["CURSOR_API_KEY"],
"model": {"id": "composer-2"},
"local": {"cwd": os.getcwd()},
"agents": {
"code-reviewer": AgentDefinition(
description="Expert code reviewer for quality and security.",
prompt="Review code for bugs, security issues, and proven approaches.",
model="inherit",
).to_json()
},
}
)
Subagents committed to .cursor/agents/*.md are also picked up through
the normal Cursor configuration layers.
Hooks
Hooks are file-based only. There is no programmatic hook callback. Hooks are a project policy boundary, not a per-run knob.
Artifacts
artifacts = agent.list_artifacts()
for artifact in artifacts:
print(artifact.path, artifact.size_bytes)
content = agent.download_artifact(artifacts[0].path)
Artifact support is runtime-dependent. Local SDK agents currently return
no artifacts and throw for download_artifact.
Configuration reference
The Python SDK accepts raw bridge dictionaries or helper dataclasses.
Raw dictionaries use the bridge JSON/proto field names (apiKey,
mcpServers, autoCreatePr, startingRef, etc.). The TypeScript-doc
spelling autoCreatePR is also accepted and normalized to the bridge
wire field. Dataclasses use Python snake_case fields.
Helper dataclasses:
AgentOptionsCloudAgentOptionsLocalAgentOptionsSendOptionsLocalSendOptionsAgentDefinitionModelSelectionModelParameterValueHttpMcpServerConfigSseMcpServerConfigStdioMcpServerConfigUserMessageSDKImageSDKImageDimension
Errors
All SDK errors extend CursorAgentError. Use is_retryable to drive
retry logic.
class CursorAgentError(Exception):
is_retryable: bool
code: str | None
cause: BaseException | None
proto_error_code: str | None
Error subclasses:
AuthenticationErrorRateLimitErrorConfigurationErrorIntegrationNotConnectedErrorNetworkErrorUnknownAgentErrorUnsupportedRunOperationError
IntegrationNotConnectedError exposes provider and help_url.
Known limitations
- Tool-call payload schemas are intentionally not strongly typed.
- Inline MCP servers are not persisted across resume.
- Artifact download is not implemented for local agents.
local.settingSourcesdoes not apply to cloud agents.- Hooks are file-based only.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cursor_sdk-0.1.1.tar.gz.
File metadata
- Download URL: cursor_sdk-0.1.1.tar.gz
- Upload date:
- Size: 1.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fe96c3629b42288de36f9f09308ea0cf4f98c607fcc8d37802894705eff3704
|
|
| MD5 |
2aec34bb118c8390f2e50886772fc67a
|
|
| BLAKE2b-256 |
5f4ef788cb68e1d9173c39703a0d813eb81eefd04bfa91bb2c412020e250a228
|
File details
Details for the file cursor_sdk-0.1.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.
File metadata
- Download URL: cursor_sdk-0.1.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- Upload date:
- Size: 60.3 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c02c6a4bf5466305c14bce1c3d0725c7c67fd27b41f3c1c1ebfacf160ed2e3d
|
|
| MD5 |
70e8c710d3ed727f971220177068a496
|
|
| BLAKE2b-256 |
2c8f75b8f6d7d63451d7151f13961b6aedf36d61f01d8f76e45bce840e594ce0
|
File details
Details for the file cursor_sdk-0.1.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.
File metadata
- Download URL: cursor_sdk-0.1.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
- Upload date:
- Size: 59.8 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd864ea2cb470f927660771aeefc9d5a78dfb21ed5aa3344f83919b322734855
|
|
| MD5 |
73c1aa7c5393cf620171954692c25bb5
|
|
| BLAKE2b-256 |
a077417b566ea6e999378d87e1d9746c76ad6b2eda201f63aa92bf87763778d9
|
File details
Details for the file cursor_sdk-0.1.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: cursor_sdk-0.1.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 50.0 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82242db3e7c57b2178c1bdd91d65707b1f0e7fb6c71bdb7087c6294be62ba2f0
|
|
| MD5 |
460c00a30a9f9b39215716de135a2844
|
|
| BLAKE2b-256 |
306a66a861024166bca6aab52c5e4cdc3a878124cc59e73a0a683a7d6e8b9751
|