Set up and govern every agent from code.
The Rayrun API and SDK manage everything the dashboard does. Tool calls still go through your MCP endpoint.
- External users and short-lived MCP sessions.
- Connections, credentials, and indexing.
- Source builds and releases.
- Tool policies, hooks, and clients.
- Workspace Skills and signed webhooks.
- Publications of hosted services, their end users, and custom domains.
- Activity and review queues.
Connections, profiles, hooks, webhooks, publications, and deployments keep a history you can compare and restore. Review decisions stay in Rayrun, so an automation key cannot approve its own action.
To get a credential:
- In a terminal, run
rayrun login once. - For CI, create a key in Dashboard → Settings → API keys. The key is shown once.
- Call
rayrun.identity.get() first to see the user, workspace, and scopes.
npm install @rayrun/sdk
import { Rayrun } from '@rayrun/sdk';
const rayrun = new Rayrun({
apiKey: process.env.RAYRUN_API_KEY,
});
const identity = await rayrun.identity.get();
console.log(identity.workspace, identity.scopes);
const { connection } = await rayrun.connections.create({
kind: 'mcp',
name: 'Linear',
url: 'https://mcp.linear.app/mcp',
});
await rayrun.connections.setPolicy(connection.uid, 'ask-before-changes');Give every product user their own MCP identity.
Upsert your stable user ID and mint a short-lived session. Hand its endpoint and token to that user’s agent runtime. The user is not a workspace member.
- Runtime context comes from the session and reaches every upstream call.
- Connect Links collect the user’s own provider OAuth grant.
- A signed webhook tells your application when authorization completes.
- The JavaScript and Python clients expose the same lifecycle.
const { uid } = await rayrun.externalPrincipals.upsert({
externalId: 'customer_123',
metadata: { plan: 'pro' },
});
const session = await rayrun.externalPrincipals.createSession(uid, {
runtimeContext: { accountId: 'customer_123', plan: 'pro' },
});
// Configure this user’s agent with only these short-lived values.
console.log(session.endpoint, session.sessionToken);
// When the user must authorize GitHub, Slack, Linear, or another connection:
const link = await rayrun.externalPrincipals.createConnectLink(uid, {
connectionId,
});
console.log(link.url);Build and release an MCP server from CI.
Send the same files the CLI uploads. Validation is read-only. Deploying stores an encrypted source revision and queues an isolated build.
- Keep one stable project UUID in CI, so a retry after a lost response converges.
- Poll the build until it and its release are terminal.
- A release goes active only after the candidate answers
tools/list. - Build logs and rollback use separate methods and scopes.
import { readFile } from 'node:fs/promises';
import { setTimeout as delay } from 'node:timers/promises';
import { Rayrun } from '@rayrun/sdk';
const apiKey = process.env.RAYRUN_API_KEY;
const manifest = JSON.parse(await readFile('./rayrun.json', 'utf8'));
const sourceProjectId = manifest.projectId;
const workspaceSecretUid = process.env.RAYRUN_INVOICE_SECRET_UID;
if (!apiKey || !sourceProjectId || !workspaceSecretUid) {
throw new Error('Deployment credentials and source project ID are required.');
}
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(sourceProjectId)) {
throw new Error('rayrun.json projectId must be a UUID.');
}
const rayrun = new Rayrun({ apiKey });
const files = JSON.parse(await readFile('./source-files.json', 'utf8'));
const validation = await rayrun.deployments.validate(files);
const queued = await rayrun.deployments.deploySourceProject(sourceProjectId, {
displayName: validation.name,
files,
secretBindings: {
INVOICE_API_TOKEN: workspaceSecretUid,
},
});
let deployment = await rayrun.deployments.get(queued.buildUid);
const timeoutMs = Number(process.env.RAYRUN_DEPLOY_TIMEOUT_MS ?? 60 * 60_000);
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error('Invalid deployment timeout.');
const deadline = Date.now() + timeoutMs;
while (
['queued', 'running'].includes(deployment.build.status) ||
(deployment.build.status === 'succeeded' &&
(!deployment.release || ['pending', 'verifying'].includes(deployment.release.status)))
) {
if (Date.now() >= deadline) throw new Error('Deployment timed out.');
await delay(2_000);
deployment = await rayrun.deployments.get(queued.buildUid);
}
if (deployment.build.status !== 'succeeded' || deployment.release?.status !== 'active') {
throw new Error(
deployment.release?.errorMessage ?? deployment.build.errorMessage ?? 'Deployment failed.',
);
}
const buildLog = await rayrun.deployments.getLog(queued.buildUid);
const releases = await rayrun.deployments.listReleases(queued.connectionId);Ship the same hook from CI.
- Pull the generated declarations.
- Test without a live upstream call.
- Save a versioned draft.
- Deploy an active or shadow revision.
- Compare or restore drafts. Roll back to an earlier compatible deployment.
Draft history is capped at 64 MiB per workspace. Hook reads and writes have separate scopes. Captured logs go only to keys with write access.
const { hook } = await rayrun.hooks.get(connectionId, toolUid);
const tested = await rayrun.hooks.test(connectionId, toolUid, {
source: hook.draftSource,
config: hook.draftConfig,
arguments: { query: 'release' },
mockResult: { items: [] },
});
const expectedBehavior =
tested.before.status === 'completed' &&
tested.before.outcome.action === 'continue' &&
tested.after?.status === 'completed' &&
tested.after.outcome.action === 'return';
if (!expectedBehavior) {
throw new Error('The hosted hook did not produce the expected test outcomes.');
}
const saved = await rayrun.hooks.saveDraft(connectionId, toolUid, {
source: hook.draftSource,
config: hook.draftConfig,
expectedVersion: hook.version,
reason: 'Normalize upstream release results',
});
const history = await rayrun.hooks.listDraftVersions(connectionId, toolUid, { limit: 25 });
await rayrun.hooks.getDraftVersion(connectionId, toolUid, history.items[0].uid);
await rayrun.hooks.deploy(connectionId, toolUid, {
expectedVersion: saved.hook.version,
mode: 'shadow',
});One contract, readable without the SDK.
- Every endpoint is validated from the schema that generates the OpenAPI document.
- Errors carry a stable code and a request ID.
- Lists use opaque cursors. API keys carry explicit scopes.
Download the OpenAPI 3.1 document.
curl https://ray.run/v1/connections \
--header "Authorization: Bearer $RAYRUN_API_KEY"
React to changes without polling.
Subscribe to connection, indexing, definition, policy, call, review, and publication events. Rayrun sends signed JSON events or ordered NDJSON batches. Event IDs are stable across retries.
- Tool-call payloads are included when capture is on for that service and the webhook opts in.
- Projection hashes and byte counts are always in the metadata.
- The dashboard shows delivery health and replay.
- Edit, compare, and restore a webhook without changing its signing secret.
import { Rayrun, verifyWebhookSignature } from '@rayrun/sdk';
const rayrun = new Rayrun({ apiKey: process.env.RAYRUN_API_KEY });
const { secret, uid: webhookUid } = await rayrun.webhooks.create({
batchMaxEvents: 100,
deliveryMode: 'batch',
eventTypes: ['activity.call'],
forwardToolPayloads: true,
url: 'https://example.com/rayrun-events',
});
const webhook = (await rayrun.webhooks.list()).items.find(({ uid }) => uid === webhookUid);
if (!webhook) throw new Error('Webhook was not found after creation.');
const updated = await rayrun.webhooks.update(webhookUid, {
enabled: false,
expectedVersion: webhook.configurationVersion,
reason: 'Pause during receiver maintenance',
});
const history = await rayrun.webhooks.listConfigurationVersions(webhookUid, { limit: 25 });
await rayrun.webhooks.restoreConfigurationVersion(webhookUid, history.items.at(-1).uid, {
expectedVersion: updated.configurationVersion,
reason: 'Restore the reviewed destination',
});
const valid = verifyWebhookSignature({
body: rawBody,
secret,
signature: request.headers['rayrun-signature'],
});