Skip to main content

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

Supported on macOS arm64/x64, Linux arm64/x64, and Windows x64. See https://cursor.com/docs/sdk/python for the full documentation.

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:

  1. The api_key= keyword argument, if you pass one explicitly.
  2. The CURSOR_API_KEY environment variable, when env-var fallback is enabled (the default for local-bridge clients started by the SDK itself; opt in for remote bridges with Client(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:

  • SDKSystemMessage
  • SDKUserMessageEvent
  • SDKAssistantMessage
  • SDKThinkingMessage
  • SDKToolUseMessage
  • SDKStatusMessage
  • SDKTaskMessage
  • SDKRequestMessage

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:

  • ConversationTurn
  • AgentConversationTurn
  • ShellConversationTurn
  • ConversationStep
  • AssistantMessage
  • ThinkingMessage
  • ShellCommand
  • ShellOutput

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:

  • AgentOptions
  • CloudAgentOptions
  • LocalAgentOptions
  • SendOptions
  • LocalSendOptions
  • AgentDefinition
  • ModelSelection
  • ModelParameterValue
  • HttpMcpServerConfig
  • SseMcpServerConfig
  • StdioMcpServerConfig
  • UserMessage
  • SDKImage
  • SDKImageDimension

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:

  • AuthenticationError
  • RateLimitError
  • ConfigurationError
  • IntegrationNotConnectedError
  • NetworkError
  • UnknownAgentError
  • UnsupportedRunOperationError

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.settingSources does 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

cursor_sdk-0.1.3.tar.gz (1.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

cursor_sdk-0.1.3-py3-none-win_amd64.whl (48.6 MB view details)

Uploaded Python 3Windows x86-64

cursor_sdk-0.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (61.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

cursor_sdk-0.1.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (61.2 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

cursor_sdk-0.1.3-py3-none-macosx_11_0_x86_64.whl (54.2 MB view details)

Uploaded Python 3macOS 11.0+ x86-64

cursor_sdk-0.1.3-py3-none-macosx_11_0_arm64.whl (52.6 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file cursor_sdk-0.1.3.tar.gz.

File metadata

  • Download URL: cursor_sdk-0.1.3.tar.gz
  • Upload date:
  • Size: 1.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.0 CPython/3.12.3

File hashes

Hashes for cursor_sdk-0.1.3.tar.gz
Algorithm Hash digest
SHA256 be6990490239796d4b6b92bde753e60bc15e6cd647d95cbab94b37c34524fdfc
MD5 27e7a9e6149419f34c6b33c7a3c84c7b
BLAKE2b-256 903270bafc55e2816cbc39600464c47ee06005aa74e4796f8293dfc4b595d27b

See more details on using hashes here.

File details

Details for the file cursor_sdk-0.1.3-py3-none-win_amd64.whl.

File metadata

  • Download URL: cursor_sdk-0.1.3-py3-none-win_amd64.whl
  • Upload date:
  • Size: 48.6 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.0 CPython/3.12.3

File hashes

Hashes for cursor_sdk-0.1.3-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 e5d11d58841a061107f7118140adeb4671bf75d4e16e67d3cebf45970482b18c
MD5 bd10da2440d73195094505915e993655
BLAKE2b-256 a10a4ba22b596215d329d3257570a03625c2e6e3b0d6d1027a4056d11eec3c97

See more details on using hashes here.

File details

Details for the file cursor_sdk-0.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for cursor_sdk-0.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 4664910b5b489bcea68f03c5a95d3c2fa094e3b1b838c3280be5a91534db2a70
MD5 8d40595a83d5d61cf7be32ccb1121b92
BLAKE2b-256 c6403d1b10d3fea16316e98031395881e2b8194aa4604edcf6d4c61a3c532c8b

See more details on using hashes here.

File details

Details for the file cursor_sdk-0.1.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for cursor_sdk-0.1.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 9656a14975d97f835c5597832e72234aee7f103cf2786d662d255433b75cf943
MD5 d4b718a045cdf057cc44703319e16760
BLAKE2b-256 2d4cb028310003ae21ec5a08ec2a1bc8f0415c4212831f659f14a0f13c321b82

See more details on using hashes here.

File details

Details for the file cursor_sdk-0.1.3-py3-none-macosx_11_0_x86_64.whl.

File metadata

File hashes

Hashes for cursor_sdk-0.1.3-py3-none-macosx_11_0_x86_64.whl
Algorithm Hash digest
SHA256 957c38f7bba151452b91a0556bec34b438191c7498443b8138810a950e7effc5
MD5 1419cacbf5f2167d629a3ce5b7dfefc0
BLAKE2b-256 16894e1b05446d688c7f95adf0da7916167975708f0df08dc7cddc36a1e550ff

See more details on using hashes here.

File details

Details for the file cursor_sdk-0.1.3-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for cursor_sdk-0.1.3-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 129b31f9ca2a4e952bf3d06addd9ea2151abe737d488263e90dc064b091ac1e9
MD5 e57f3a1e5910662db60703550b6aa783
BLAKE2b-256 5c4c90afeb0eb24357a4838f85834248a1548fbe204288d5598e4e223c399bde

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page