Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.11.1"
".": "1.11.2"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 118
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-132ed160716591bdcd9231c00da8c506d9451a5486b165fc27b2a01d93202082.yml
openapi_spec_hash: c2b44d9e9cda56e32141a7ea3794bbba
config_hash: 3bd89c812b96708c461fb98286ebf0b5
configured_endpoints: 117
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-0568973e19e8af9fa953b2ded109ab2b69e76e90e2b74f33617dbf7092e26274.yml
openapi_spec_hash: 10ba804ce69510d7985e05c77d0ffcf6
config_hash: de99cfce88e2d1f02246dc6c2f43bc6c
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.11.2 (2026-03-19)

Full Changelog: [v1.11.1...v1.11.2](https://github.com/runloopai/api-client-ts/compare/v1.11.1...v1.11.2)

### Chores

* remove create_tunnel endpoint ([5076da2](https://github.com/runloopai/api-client-ts/commit/5076da21a6729b4d288b530f78cc0e16415175d7))

## 1.11.1 (2026-03-18)

Full Changelog: [v1.11.0...v1.11.1](https://github.com/runloopai/api-client-ts/compare/v1.11.0...v1.11.1)
Expand Down
2 changes: 0 additions & 2 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ Types:
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxSendStdInResult</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxSnapshotListView</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxSnapshotView</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxTunnelView</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxView</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">TunnelView</a></code>
- <code><a href="./src/resources/devboxes/devboxes.ts">DevboxCreateSSHKeyResponse</a></code>
Expand All @@ -131,7 +130,6 @@ Methods:
- <code title="post /v1/devboxes/{id}">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">update</a>(id, { ...params }) -> DevboxView</code>
- <code title="get /v1/devboxes">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">list</a>({ ...params }) -> DevboxViewsDevboxesCursorIDPage</code>
- <code title="post /v1/devboxes/{id}/create_ssh_key">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">createSSHKey</a>(id) -> DevboxCreateSSHKeyResponse</code>
- <code title="post /v1/devboxes/{id}/create_tunnel">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">createTunnel</a>(id, { ...params }) -> DevboxTunnelView</code>
- <code title="post /v1/devboxes/disk_snapshots/{id}/delete">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">deleteDiskSnapshot</a>(id) -> unknown</code>
- <code title="post /v1/devboxes/{id}/download_file">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">downloadFile</a>(id, { ...params }) -> Response</code>
- <code title="post /v1/devboxes/{id}/enable_tunnel">client.devboxes.<a href="./src/resources/devboxes/devboxes.ts">enableTunnel</a>(id, { ...params }) -> TunnelView</code>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@runloop/api-client",
"version": "1.11.1",
"version": "1.11.2",
"description": "The official TypeScript library for the Runloop API",
"author": "Runloop <[email protected]>",
"types": "dist/sdk.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "@runloop/api-client-mcp",
"version": "1.11.1",
"version": "1.11.2",
"description": "The official MCP Server for the Runloop API",
"author": {
"name": "Runloop",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@runloop/api-client-mcp",
"version": "1.11.1",
"version": "1.11.2",
"description": "The official MCP Server for the Runloop API",
"author": "Runloop <[email protected]>",
"types": "dist/index.d.ts",
Expand Down
4 changes: 1 addition & 3 deletions packages/mcp-server/src/code-tool-paths.cts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export function getWorkerPath(): string {
return require.resolve('./code-tool-worker.mjs');
}
export const workerPath = require.resolve('./code-tool-worker.mjs');
51 changes: 19 additions & 32 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
import { newDenoHTTPWorker } from '@valtown/deno-http-worker';
import { workerPath } from './code-tool-paths.cjs';
import {
ContentBlock,
McpRequestContext,
Expand Down Expand Up @@ -144,23 +149,19 @@ const remoteStainlessHandler = async ({

const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';

const localClientEnvs = {
RUNLOOP_API_KEY: requireValue(
readEnv('RUNLOOP_API_KEY') ?? client.bearerToken,
'set RUNLOOP_API_KEY environment variable or provide bearerToken client option',
),
RUNLOOP_BASE_URL: readEnv('RUNLOOP_BASE_URL') ?? client.baseURL ?? undefined,
};
// Merge any upstream client envs from the request header, with upstream values taking precedence.
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };

// Setting a Stainless API key authenticates requests to the code tool endpoint.
const res = await fetch(codeModeEndpoint, {
method: 'POST',
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
'Content-Type': 'application/json',
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
'x-stainless-mcp-client-envs': JSON.stringify({
RUNLOOP_API_KEY: requireValue(
readEnv('RUNLOOP_API_KEY') ?? client.bearerToken,
'set RUNLOOP_API_KEY environment variable or provide bearerToken client option',
),
RUNLOOP_BASE_URL: readEnv('RUNLOOP_BASE_URL') ?? client.baseURL ?? undefined,
}),
},
body: JSON.stringify({
project_name: 'runloop',
Expand Down Expand Up @@ -203,13 +204,6 @@ const localDenoHandler = async ({
reqContext: McpRequestContext;
args: unknown;
}): Promise<ToolCallResult> => {
const fs = await import('node:fs');
const path = await import('node:path');
const url = await import('node:url');
const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
const { getWorkerPath } = await import('./code-tool-paths.cjs');
const workerPath = getWorkerPath();

const client = reqContext.client;
const baseURLHostname = new URL(client.baseURL).hostname;
const { code } = args as { code: string };
Expand Down Expand Up @@ -271,9 +265,6 @@ const localDenoHandler = async ({
printOutput: true,
spawnOptions: {
cwd: path.dirname(workerPath),
// Merge any upstream client envs into the Deno subprocess environment,
// with the upstream env vars taking precedence.
env: { ...process.env, ...reqContext.upstreamClientEnvs },
},
});

Expand All @@ -283,17 +274,13 @@ const localDenoHandler = async ({
reject(new Error(`Worker exited with code ${exitCode}`));
});

// Strip null/undefined values so that the worker SDK client can fall back to
// reading from environment variables (including any upstreamClientEnvs).
const opts: ClientOptions = Object.fromEntries(
Object.entries({
baseURL: client.baseURL,
bearerToken: client.bearerToken,
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
}).filter(([_, v]) => v != null),
) as ClientOptions;
const opts: ClientOptions = {
baseURL: client.baseURL,
bearerToken: client.bearerToken,
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
};

const req = worker.request(
'http://localhost',
Expand Down
46 changes: 2 additions & 44 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,56 +27,14 @@ const newServer = async ({

const authOptions = parseClientAuthHeaders(req, false);

let upstreamClientEnvs: Record<string, string> | undefined;
const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs'];
if (typeof clientEnvsHeader === 'string') {
try {
const parsed = JSON.parse(clientEnvsHeader);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
upstreamClientEnvs = parsed;
}
} catch {
// Ignore malformed header
}
}

// Parse x-stainless-mcp-client-permissions header to override permission options
//
// Note: Permissions are best-effort and intended to prevent clients from doing unexpected things;
// they're not a hard security boundary, so we allow arbitrary, client-driven overrides.
//
// See the Stainless MCP documentation for more details.
let effectiveMcpOptions = mcpOptions;
const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions'];
if (typeof clientPermissionsHeader === 'string') {
try {
const parsed = JSON.parse(clientPermissionsHeader);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
effectiveMcpOptions = {
...mcpOptions,
...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }),
...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }),
...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }),
};
getLogger().info(
{ clientPermissions: parsed },
'Overriding code execution permissions from x-stainless-mcp-client-permissions header',
);
}
} catch (error) {
getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header');
}
}

await initMcpServer({
server: server,
mcpOptions: effectiveMcpOptions,
mcpOptions: mcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
stainlessApiKey: stainlessApiKey,
upstreamClientEnvs,
});

return server;
Expand Down Expand Up @@ -114,7 +72,7 @@ const del = async (req: express.Request, res: express.Response) => {
};

const redactHeaders = (headers: Record<string, any>) => {
const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
const hiddenHeaders = /auth|cookie|key|token/i;
const filtered = { ...headers };
Object.keys(filtered).forEach((key) => {
if (hiddenHeaders.test(key)) {
Expand Down
25 changes: 15 additions & 10 deletions packages/mcp-server/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,29 @@ interface InstructionsCacheEntry {

const instructionsCache = new Map<string, InstructionsCacheEntry>();

export async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
// Periodically evict stale entries so the cache doesn't grow unboundedly.
const _cacheCleanupInterval = setInterval(() => {
const now = Date.now();
const cacheKey = stainlessApiKey ?? '';
const cached = instructionsCache.get(cacheKey);

if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
return cached.fetchedInstructions;
}

// Evict stale entries so the cache doesn't grow unboundedly.
for (const [key, entry] of instructionsCache) {
if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) {
instructionsCache.delete(key);
}
}
}, INSTRUCTIONS_CACHE_TTL_MS);

// Don't keep the process alive just for cleanup.
_cacheCleanupInterval.unref();

export async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
const cacheKey = stainlessApiKey ?? '';
const cached = instructionsCache.get(cacheKey);

if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
return cached.fetchedInstructions;
}

const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey);
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now });
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() });
return fetchedInstructions;
}

Expand Down
4 changes: 1 addition & 3 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) =>
new McpServer(
{
name: 'runloop_api_client_api',
version: '1.11.1',
version: '1.11.2',
},
{
instructions: await getInstructions(stainlessApiKey),
Expand All @@ -33,7 +33,6 @@ export async function initMcpServer(params: {
clientOptions?: ClientOptions;
mcpOptions?: McpOptions;
stainlessApiKey?: string | undefined;
upstreamClientEnvs?: Record<string, string> | undefined;
}) {
const server = params.server instanceof McpServer ? params.server.server : params.server;

Expand Down Expand Up @@ -95,7 +94,6 @@ export async function initMcpServer(params: {
reqContext: {
client,
stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey,
upstreamClientEnvs: params.upstreamClientEnvs,
},
args,
});
Expand Down
1 change: 0 additions & 1 deletion packages/mcp-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export type ToolCallResult = {
export type McpRequestContext = {
client: Runloop;
stainlessApiKey?: string | undefined;
upstreamClientEnvs?: Record<string, string> | undefined;
};

export type HandlerFunction = ({
Expand Down
4 changes: 0 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ import {
DevboxAsyncExecutionDetailView,
DevboxCreateParams,
DevboxCreateSSHKeyResponse,
DevboxCreateTunnelParams,
DevboxDeleteDiskSnapshotResponse,
DevboxDownloadFileParams,
DevboxEnableTunnelParams,
Expand All @@ -203,7 +202,6 @@ import {
DevboxSnapshotListView,
DevboxSnapshotView,
DevboxSnapshotViewsDiskSnapshotsCursorIDPage,
DevboxTunnelView,
DevboxUpdateParams,
DevboxUploadFileParams,
DevboxUploadFileResponse,
Expand Down Expand Up @@ -608,7 +606,6 @@ export declare namespace Runloop {
type DevboxSendStdInResult as DevboxSendStdInResult,
type DevboxSnapshotListView as DevboxSnapshotListView,
type DevboxSnapshotView as DevboxSnapshotView,
type DevboxTunnelView as DevboxTunnelView,
type DevboxView as DevboxView,
type TunnelView as TunnelView,
type DevboxCreateSSHKeyResponse as DevboxCreateSSHKeyResponse,
Expand All @@ -622,7 +619,6 @@ export declare namespace Runloop {
type DevboxCreateParams as DevboxCreateParams,
type DevboxUpdateParams as DevboxUpdateParams,
type DevboxListParams as DevboxListParams,
type DevboxCreateTunnelParams as DevboxCreateTunnelParams,
type DevboxDownloadFileParams as DevboxDownloadFileParams,
type DevboxEnableTunnelParams as DevboxEnableTunnelParams,
type DevboxExecuteParams as DevboxExecuteParams,
Expand Down
2 changes: 1 addition & 1 deletion src/resources/benchmark-jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ export namespace BenchmarkJobView {
/**
* The current state of the run.
*/
state: 'running' | 'canceled' | 'completed';
state: 'running' | 'canceled' | 'completed' | 'failed';

/**
* Agent configuration used for this run. Specifies whether the run was driven by
Expand Down
6 changes: 4 additions & 2 deletions src/resources/benchmark-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export class BenchmarkRuns extends APIResource {
}

/**
* Cancel a currently running Benchmark run.
* Cancel a Benchmark run. This will do the following: 1. Cancel all running
* scenarios and shutdown the underlying Devbox resources 2. Update the benchmark
* state to CANCELED 3. Calculate final score from completed scenarios
*/
cancel(id: string, options?: Core.RequestOptions): Core.APIPromise<BenchmarkRunView> {
return this._client.post(`/v1/benchmark_runs/${id}/cancel`, options);
Expand Down Expand Up @@ -118,7 +120,7 @@ export interface BenchmarkRunView {
/**
* The state of the BenchmarkRun.
*/
state: 'running' | 'canceled' | 'completed';
state: 'running' | 'canceled' | 'completed' | 'failed';

/**
* The ID of the Benchmark definition. Present if run was created from a benchmark
Expand Down
Loading
Loading