When something fails while you are building an adapter, the first question is
always "is it my code or the bridge?". This page is the oracle: a complete
spawn → Ping → GetVersion → Me → CreateAgent → Send sequence in
JSON mode using nothing but a shell and curl. If a step fails here too, the
problem is on the bridge side (or in your key / environment); if it works
here but not through your adapter, diff your adapter's raw request against
the one below.
Every command was verified against a real bridge. Run them from any
directory; they assume a POSIX shell with curl, python3 (JSON
extraction only), and xxd (streaming request framing only).
export CURSOR_API_KEY=key_... # from https://cursor.com/dashboard
./cursor-sdk-bridge/bin/cursor-sdk-bridge --workspace "$PWD" --port 39217 \
2> /tmp/bridge-stderr.log &
sleep 2
B=http://127.0.0.1:39217
TOKEN=$(cat "$(grep -o '"authTokenFile":"[^"]*"' /tmp/bridge-stderr.log | cut -d'"' -f4)")(A fixed --port keeps the one-liners copy-pasteable; production adapters
should use the default ephemeral port and parse the ready line properly —
see protocol.md.)
curl -s -X POST "$B/sdk.v1.SdkBridgeControlService/Ping" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{}'
# -> {"message":"pong"}
curl -s -X POST "$B/sdk.v1.SdkBridgeControlService/GetVersion" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{}'
# -> {"bridgeVersion":"1.0.0","protocolVersion":"sdk.v1","capabilities":[...]}A {"code":"unauthenticated","message":"Unauthorized"} here means your
bearer token is wrong — re-read authTokenFile and trim whitespace.
Catalog calls require a per-call api_key; the bridge never falls back to
its environment for these:
curl -s -X POST "$B/sdk.v1.SdkCursorService/Me" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d "{\"options\":{\"apiKey\":\"$CURSOR_API_KEY\"}}"
# -> {"user":{"apiKeyName":...,"userEmail":...}}Omitting the key yields
{"code":"unauthenticated","message":"API key is required for cloud catalog calls."}.
Set the key on options.apiKey (see protocol.md on why the
env var alone is not sufficient):
curl -s -X POST "$B/sdk.v1.SdkAgentService/CreateAgent" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d "{\"options\":{\"model\":{\"id\":\"composer-2\"},\"apiKey\":\"$CURSOR_API_KEY\",\"local\":{\"cwd\":[\"$PWD\"]}}}"
# -> {"agentId":"agent-...","model":{"id":"composer-2"}}
AGENT=agent-... # paste the returned id(Use a model id from SdkCursorService.ListModels — same shape as Me
above.)
Send is a server-streaming RPC, so the request uses the Connect streaming
content type and each message is framed as 1 flags byte + a 4-byte big-endian
length + the payload (Connect streaming spec).
Building that frame by hand:
BODY="{\"agentId\":\"$AGENT\",\"message\":{\"text\":\"Say hello.\"}}"
{ printf '\x00'; printf '%s' "$BODY" | wc -c | xargs printf '%08x' | xxd -r -p; printf '%s' "$BODY"; } > /tmp/send.bin
curl -sN -X POST "$B/sdk.v1.SdkAgentService/Send" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/connect+json" \
--data-binary @/tmp/send.bin | tr -c '[:print:]\n' '.'The response interleaves 5-byte binary frame headers (rendered as . by the
tr) with JSON envelopes: sdkMessage events, then result, then done,
then the end-of-stream frame (flags 0x02) whose JSON carries
{"error":{...}} if and only if the RPC failed. A failed run still ends as a successful
stream — the failure text arrives in the status event payload (see
streaming.md).
curl -s -X POST "$B/sdk.v1.SdkBridgeControlService/Shutdown" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{}'- Every error body is a Connect JSON error:
{"code":..., "message":..., "details":[...]}with thesdk.v1.SdkErrorDetailsdetail base64-encoded indetails[].value(seeerrors.md). - A bare
{"code":"internal","message":"internal error"}with no details means the bridge masked an unexpected internal failure; the underlying message is not recoverable from the outside. Re-running the failing step from this smoke test at least tells you whether your adapter's request is what triggers it.