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

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:

  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.1.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.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (60.3 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

cursor_sdk-0.1.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (59.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

cursor_sdk-0.1.1-py3-none-macosx_11_0_arm64.whl (50.0 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

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

Hashes for cursor_sdk-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6fe96c3629b42288de36f9f09308ea0cf4f98c607fcc8d37802894705eff3704
MD5 2aec34bb118c8390f2e50886772fc67a
BLAKE2b-256 5f4ef788cb68e1d9173c39703a0d813eb81eefd04bfa91bb2c412020e250a228

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for cursor_sdk-0.1.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 5c02c6a4bf5466305c14bce1c3d0725c7c67fd27b41f3c1c1ebfacf160ed2e3d
MD5 70e8c710d3ed727f971220177068a496
BLAKE2b-256 2c8f75b8f6d7d63451d7151f13961b6aedf36d61f01d8f76e45bce840e594ce0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for cursor_sdk-0.1.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 cd864ea2cb470f927660771aeefc9d5a78dfb21ed5aa3344f83919b322734855
MD5 73c1aa7c5393cf620171954692c25bb5
BLAKE2b-256 a077417b566ea6e999378d87e1d9746c76ad6b2eda201f63aa92bf87763778d9

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for cursor_sdk-0.1.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 82242db3e7c57b2178c1bdd91d65707b1f0e7fb6c71bdb7087c6294be62ba2f0
MD5 460c00a30a9f9b39215716de135a2844
BLAKE2b-256 306a66a861024166bca6aab52c5e4cdc3a878124cc59e73a0a683a7d6e8b9751

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