name: sdk
description: >-
Guide users building apps, scripts, CI pipelines, or automations on top of the
Cursor SDK - TypeScript (@cursor/sdk) or Python (cursor-sdk /
cursor_sdk). Use when the user mentions integrating, installing, or writing
code against the Cursor SDK; says Agent.create, Agent.prompt,
Agent.resume, agent.send, run.stream, run.messages,
CursorAgentError, @cursor/sdk, cursor-sdk, or cursor_sdk; asks to run
Cursor agents programmatically from a script, CI/CD pipeline, GitHub Action,
backend service, or other code outside the Cursor IDE; wants to pick between
local and cloud runtime, configure MCP servers for an SDK agent, or handle
streaming, cancellation, or errors; or is wiring Cursor into an automation,
bot, or REST /v1/agents migration. Use eagerly rather than answering from
memory; the SDK surface evolves and this skill is the source of truth for the
external packages.
Cursor SDK
The Cursor SDK runs Cursor agents programmatically. Two language variants share the same concepts:
- TypeScript (
@cursor/sdk, npm) - docs at cursor.com/docs/sdk/typescript - Python (
cursor-sdk, pip) - docs at cursor.com/docs/sdk/python
Both are in public beta and follow the same Agent → Run model across local (runs on the caller's machine against cwd) and cloud (runs on a Cursor-hosted VM against a cloned repo) runtimes.
Use this skill to help someone bootstrap a working integration quickly and avoid the traps that bite new users. The canonical docs cover the full reference; this skill adds decision-making, failure-mode prevention, and ready-to-extend patterns on top.
Pick the language
Decide before writing any code. The wrong choice means rewriting the whole integration. Order:
- The user named it.
@cursor/sdk,cursor-sdk,cursor_sdk,npm install,pip install,import { Agent } from "@cursor/sdk",from cursor_sdk import- go with what they named. - The codebase signals it.
pyproject.toml/requirements.txt/uv.lock/.pyfiles → Python.package.json/tsconfig.json/.tsfiles → TypeScript. If the integration will live in a polyglot repo, ask which subdirectory it goes into. - No signal either way. Ask one short question and wait: "TypeScript or Python?" Don't pick for them.
The rest of this skill shows TypeScript and Python side by side. Concepts are identical; syntax differs - camelCase vs snake_case, async-by-default vs sync-default-with-async-mirror, await using vs with.
Voice and Posture
This skill helps the user build with the SDK. It is not the place to validate, congratulate, or sell the SDK as a choice. The user's intent is the input; your job is execution.
- When the user names the SDK explicitly (says "Cursor SDK",
@cursor/sdk,cursor-sdk,Agent.create,Agent.prompt, etc.): assume they know what the SDK is and have decided to use it. Skip framing, skip pep talk, go straight to producing the integration. No "good news", no "the SDK is perfect for this", no "this is almost exactly the pattern X is designed for". - When the user describes a problem the SDK fits but doesn't name it ("I want a bot that reviews my PRs", "I want a script that asks Cursor questions about my repo"): the SDK isn't yet a confirmed choice. Surface it as a question, briefly, then wait: "The Cursor SDK is what I'd reach for here - want me to design it that way, or do you have a different runtime in mind?" If they confirm, proceed. If they push back or want options, give options.
- In either case, don't restate the user's intent back to them. They know what they want. Get to the design.
Avoid these specific openers (and their close cousins):
- "Good news: this is exactly the pattern..."
- "The SDK is built for this shape."
- "Great, you've come to the right place."
- "This is almost exactly the X the SDK is designed for."
- Any lede that compliments the user's choice or restates their goal in flattering terms.
Prefer:
- Open with the design decision or the first thing they need to know.
- If you genuinely have a design choice to flag (local vs cloud, prompt vs send, sync vs stream, sync Python vs async Python), name it in one sentence and explain why.
The Three Invocation Patterns
Almost every SDK integration collapses to one of these three shapes. Pick the one that fits the job, don't mix them.
1. Agent.prompt(...) - one-shot
Use for fire-and-forget scripts, GitHub Actions steps, or any "send this prompt, get a result, exit" flow. No streaming, no follow-ups, no cleanup to remember. If you're reaching for this and then immediately resuming, you wanted pattern 2 instead.
TypeScript:
import { Agent } from "@cursor/sdk";
const result = await Agent.prompt("Refactor src/utils.ts for readability", {
apiKey: process.env.CURSOR_API_KEY!,
model: { id: "composer-2.5" },
local: { cwd: process.cwd() },
});
console.log(result.status, result.result);
Python:
import os
from cursor_sdk import Agent, AgentOptions, LocalAgentOptions
result = Agent.prompt(
"Refactor src/utils.py for readability",
AgentOptions(
api_key=os.environ["CURSOR_API_KEY"],
model="composer-2.5",
local=LocalAgentOptions(cwd=os.getcwd()),
),
)
print(result.status, result.result)
2. Agent.create(...) + agent.send(...) - durable with follow-ups
Use when you need streaming, multi-turn conversation, or lifecycle operations (cancel, status listener). This is the shape of most non-trivial integrations.
TypeScript:
import { Agent } from "@cursor/sdk";
await using agent = await Agent.create({
apiKey: process.env.CURSOR_API_KEY!,
model: { id: "composer-2.5" },
local: { cwd: process.cwd() },
});
const run = await agent.send("Find the bug in src/auth.ts");
for await (const event of run.stream()) {
if (event.type === "assistant") {
for (const block of event.message.content) {
if (block.type === "text") process.stdout.write(block.text);
}
}
}
await run.wait();
// Follow-up keeps full conversation context.
const run2 = await agent.send("Now write a regression test for it");
await run2.wait();
Python:
import os
from cursor_sdk import Agent, LocalAgentOptions
with Agent.create(
model="composer-2.5",
api_key=os.environ["CURSOR_API_KEY"],
local=LocalAgentOptions(cwd=os.getcwd()),
) as agent:
run = agent.send("Find the bug in src/auth.py")
for message in run.messages():
if message.type == "assistant":
for block in message.message.content:
if block.type == "text":
print(block.text, end="")
run.wait()
# Follow-up keeps full conversation context.
run2 = agent.send("Now write a regression test for it")
run2.wait()
The Python SDK is sync by default. For servers, bots, and concurrent orchestration, use AsyncClient.launch_bridge(...) as an async context manager and AsyncAgent. There's no global async default client - instantiate one per event loop, and never mix sync and async clients in the same code path.
3. Agent.resume(...) - pick up an existing agent later
Use across process boundaries: a cron that continues last night's cleanup, a webhook that extends a user's agent, an interactive CLI that reloads conversation state. Runtime is auto-detected from the ID prefix - bc- is cloud, anything else is local.
TypeScript:
await using agent = await Agent.resume(previousAgentId, {
apiKey: process.env.CURSOR_API_KEY!,
});
const run = await agent.send("Also update the changelog");
await run.wait();
Python:
import os
from cursor_sdk import Agent, AgentOptions
with Agent.resume(
previous_agent_id,
AgentOptions(api_key=os.environ["CURSOR_API_KEY"]),
) as agent:
run = agent.send("Also update the changelog")
run.wait()
Inline MCP servers are not persisted across resume - they often carry secrets and live in memory only. Pass them again on the resume call in either language.
Top Five Traps
These trip up almost every new integration. They're all easy to prevent once you know about them.
1. Picking the wrong runtime by accident
AgentOptions doesn't require local or cloud; the SDK selects local when neither is set. The trap: if you intended a cloud agent and forgot the cloud field, you get a local agent silently - no error, just a local agent ID and a local executor.
- TypeScript: pass
cloud: { repos: [...] }for cloud,local: { cwd }for local - even though local is the default. - Python: pass
cloud=CloudAgentOptions(repos=[...])orlocal=LocalAgentOptions(cwd=...).
Always set one of the two explicitly. The cost is one line; the cost of not noticing for an hour is much more.
2. Two kinds of failure, one instinct to conflate them
A thrown CursorAgentError means the run never executed (auth, config, network). A returned result.status == "error" means the run did execute and failed. Different fixes, different exit codes, different observability.
TypeScript:
import { Agent, CursorAgentError } from "@cursor/sdk";
try {
const run = await agent.send(prompt);
const result = await run.wait();
if (result.status === "error") {
// Run started but failed mid-flight. Inspect transcript, git state, tool outputs.
console.error("run failed: " + result.id);
process.exit(2);
}
} catch (err) {
if (err instanceof CursorAgentError) {
// Didn't start. Auth, config, network. Fix environment, retry.
console.error("startup failed: " + err.message + ", retryable=" + err.isRetryable);
process.exit(1);
}
throw err;
}
Python:
import sys
from cursor_sdk import CursorAgentError
try:
run = agent.send(prompt)
result = run.wait()
if result.status == "error":
# Run started but failed mid-flight. Inspect transcript, git state, tool outputs.
print("run failed: " + result.id, file=sys.stderr)
sys.exit(2)
except CursorAgentError as err:
# Didn't start. Auth, config, network. Fix environment, retry.
print(
"startup failed: " + err.message + ", retryable=" + str(err.is_retryable),
file=sys.stderr,
)
sys.exit(1)
3. Forgetting to dispose leaks resources
The SDK holds handles to local executors, persisted run stores, and HTTP clients. Skipping disposal leaks child processes, open databases, and (in long-running services) memory.
- TypeScript:
await using agent = await Agent.create(...)is the cleanest path. Otherwise wrap intry/finallywithawait agent[Symbol.asyncDispose]().Agent.prompt(...)disposes for you. - Python (sync):
with Agent.create(...) as agent:is the cleanest path. Otherwise callagent.close()in afinally. Long-running processes that rely on the module-level default client should callclose_default_client()at shutdown. - Python (async): nest two async context managers -
async with await AsyncClient.launch_bridge(...) as client:thenasync with await client.agents.create(...) as agent:.Agent.prompt(...)disposes for you.
4. Streaming is optional but wait() is almost always required
The stream is how you observe; wait() is how you get the terminal result. You can skip streaming, but skipping wait() means you can't tell whether the run finished, errored, or was cancelled, and you'll leak the run's internal watchers. Always call wait(). If you don't want live output, call wait() alone.
- TypeScript:
run.stream()is an async iterable ofSDKMessage.await run.wait()returnsRunResult. - Python:
run.messages()yields typed SDK messages;run.events()yields lower-level envelopes.run.wait()returnsRunResult. Async equivalents areasync for ...andawait run.wait(). Convenience helpers:run.text()blocks onwait()and returns the final assistant text;run.iter_text()streams text-only chunks.run.stream()is an alias forrun.messages().
5. Not every run operation is supported on every runtime
Run exposes a handful of operations - stream/messages, wait, cancel, conversation - and detached or rehydrated runs (handles you got from Agent.getRun(...) after the live event store closed) may not support all of them. Guard with supports(...):
if (run.supports("cancel")) await run.cancel();
if (run.supports("conversation")) console.log(await run.conversation());
if run.supports("cancel"):
run.cancel()
if run.supports("conversation"):
print(run.conversation())
run.unsupportedReason(op) (TypeScript) / run.unsupported_reason(op) (Python) tells you why.
Local vs Cloud, in one sentence each
- Local - runs on the caller's machine against
cwd, reuses their environment and credentials. Good for dev loops and CI that already has a repo checkout. - Cloud - runs on a Cursor-hosted VM against a freshly cloned repo. Good for long jobs, fire-and-forget automation, and opening real PRs (
autoCreatePR: truein TypeScript;auto_create_pr=Truein Python).
Auth, minimum viable
export CURSOR_API_KEY="cursor_..." # user API key or team service-account key
Both SDKs read CURSOR_API_KEY when no key is passed explicitly. User keys live at Cursor Dashboard → Integrations; team service-account keys live in Team Settings → Service accounts. Team Admin API keys are not yet supported.
If you're seeing 401s, the usual suspects are: key pasted with surrounding whitespace, key minted against a different environment, or the key belongs to a user without repo access for a cloud run.
Model Selection
Don't hardcode unusual model IDs without confirming the caller has access - model lists evolve.
TypeScript:
import { Cursor } from "@cursor/sdk";
const models = await Cursor.models.list({ apiKey: process.env.CURSOR_API_KEY! });
Python:
from cursor_sdk import Cursor
models = Cursor.models.list() # falls back to CURSOR_API_KEY
composer-2.5 is the current default for most integrations. { id: "auto" } (TS) / model="auto" (Python) lets the server pick. Cursor.models.list() returns valid IDs, per-model parameter definitions (reasoning effort, max mode), and preset variants for the calling account.
Model is required for local in both SDKs. For cloud, TypeScript falls back to a server-resolved default when omitted; Python documents it as required across both runtimes. Pass one regardless to keep behavior predictable.
MCP Servers
Both SDKs use the same conceptual model: HTTP transport (with static headers or OAuth auth) or stdio (with command / args / env). Pass servers inline on Agent.create or agent.send for the most common case.
- Local agents can use stdio or HTTP servers available on the caller's machine. If a local MCP server requires OAuth login, the SDK can reuse a saved login from the Cursor app but can't open a browser to sign you in.
- Cloud agents support both HTTP and stdio (stdio runs inside the cloud VM). HTTP
headersandauthare handled by Cursor's backend and redacted before the VM sees them. Stdioenvvalues are passed into the VM - treat them like any runtime secret. - Inline servers fully replace creation-time servers on a per-send override - not merged.
- If you resume an agent and still need MCP tools, pass servers again on the resume call. Inline servers are not persisted.
For full schema and authentication options, see the SDK docs for each language and Cursor MCP.
Production Best Practices
Apply these to any integration that runs unattended:
- Always dispose.
await using(TypeScript) orwith ... as agent:(Python) is the cleanest path. Non-negotiable. - Distinguish startup failures from run failures. Exit code 1 for thrown
CursorAgentError, exit code 2 forresult.status == "error", exit code 0 only forfinished. - Log
run.idandagent.agentId/agent.agent_idimmediately aftersend()before streaming. If the stream hangs, the IDs are what you need to investigate in the dashboard or viaAgent.getRun(...)/Agent.get_run(...). - Respect
error.isRetryable/err.is_retryable. It's the backend telling you the specific failure is safe to retry. Python errors also exposeretry_after(HTTP-style seconds value or date) - honor it before falling back to exponential backoff. Blind retries can cause duplicate cloud runs. - Don't load ambient settings unless you mean to. The default in TypeScript is
local.settingSources: []; the default in Python is nolocal.setting_sources. Both mean "inline config only". Opting into"all"loads project/user/team/MDM settings from the caller's environment, which is rarely what you want from a service. Setting sources have no effect on cloud agents - cloud always honors team/project/plugins. - For cloud agents in CI, set
skipReviewerRequest: true(TypeScript) /skip_reviewer_request=True(Python) unless a human should be paged - it suppresses the reviewer-request step and keeps PR notifications quiet. - Always pass
apiKey/api_keyexplicitly in shared-infrastructure code instead of relying on the env var. Makes the credential dependency obvious and prevents cross-tenant mistakes. - Prefer the one-shot
Agent.prompt(...)for true one-shots - it disposes for you and is harder to leak.
Observing a Run You Didn't Launch
You can inspect any agent or run by ID later. Cloud IDs that start with bc- auto-route to the cloud API; anything else is local.
TypeScript:
const info = await Agent.get("bc-abc123", { apiKey });
const run = await Agent.getRun(runId, { runtime: "cloud", agentId: "bc-abc123", apiKey });
// Local: you need the cwd where the agent was created.
const localList = await Agent.list({ runtime: "local", cwd: process.cwd() });
Python:
from cursor_sdk import CursorClient
with CursorClient.launch_bridge(workspace=".") as client:
info = client.agents.get("bc-abc123")
run = client.agents.get_run(run_id)
# Local: filter by cwd.
local_list = client.agents.list(runtime="local", cwd=".")
A cloud bc--prefixed agent ID is not a run ID. If you only have a run ID (from a log or a webhook), pass it to getRun / get_run with the runtime hint; don't confuse the two.
Offering a Canvas
If the user's integration monitors, lists, or visualizes agents - dashboards of active runs, conversation replays, tool-call timelines - offer a Cursor Canvas to render it. If they accept, defer entirely to the canvas skill.
What This Skill Doesn't Cover
- The Cloud Agents REST API (
/v1/agents/*). Use it from languages without a first-party SDK, or when you want a minimal HTTP surface. .cursor/hooks.jsonhooks. Both SDKs respect them; neither manages them. See Hooks.- Self-hosted cloud (private workers, self-hosted pools, my-machines). See Self-hosted pool and related docs.
- SDKs in languages other than TypeScript and Python. The REST API is the portable option there.