Skip to content
Open
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
18 changes: 10 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
Expand All @@ -17,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6

Expand All @@ -36,7 +38,7 @@ jobs:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
permissions:
contents: read
id-token: write
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write

steps:
- uses: actions/checkout@v6
Expand All @@ -39,8 +40,6 @@ jobs:
PATHS_RELEASED='[\".\", \"packages/mcp-server\"]'
fi
yarn tsn scripts/publish-packages.ts "{ \"paths_released\": \"$PATHS_RELEASED\" }"
env:
NPM_TOKEN: ${{ secrets.BEEPER_DESKTOP_NPM_TOKEN || secrets.NPM_TOKEN }}

- name: Upload MCP Server DXT GitHub release asset
run: |
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/release-doctor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ jobs:
- name: Check release environment
run: |
bash ./bin/check-release-environment
env:
NPM_TOKEN: ${{ secrets.BEEPER_DESKTOP_NPM_TOKEN || secrets.NPM_TOKEN }}

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.prism.log
.stdy.log
node_modules
yarn-error.log
codegen.log
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "4.7.0"
".": "4.7.1"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 23
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml
openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a
config_hash: ca148af6be59ec54295b2c5f852a38d1
config_hash: 7d85c0b454fc78a59db6474c5c4d73c6
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## 4.7.1 (2026-03-25)

Full Changelog: [v4.7.0...v4.7.1](https://github.com/beeper/desktop-api-js/compare/v4.7.0...v4.7.1)

### Bug Fixes

* escape MCP `instructions` backticks as necessary ([5054afe](https://github.com/beeper/desktop-api-js/commit/5054afe374e27b9aec675a7d53d30dcaf0a7a351))


### Chores

* **ci:** skip lint on metadata-only changes ([c829f2e](https://github.com/beeper/desktop-api-js/commit/c829f2e95c7ff077a9bf30a0ca31256313d28e28))
* **internal:** fix MCP server TS errors that occur with required client options ([39f92ad](https://github.com/beeper/desktop-api-js/commit/39f92ad78be85d04b893d23d2c9cf5c655176fdc))
* **internal:** make generated MCP servers compatible with Cloudflare worker environments ([51ed091](https://github.com/beeper/desktop-api-js/commit/51ed09114417dd970167ef9e02663f729cc3ffa8))
* **internal:** support x-stainless-mcp-client-envs header in MCP servers ([f152521](https://github.com/beeper/desktop-api-js/commit/f152521d3d12eebf07826d8494e619f22a856eb5))
* **internal:** support x-stainless-mcp-client-permissions headers in MCP servers ([b714f03](https://github.com/beeper/desktop-api-js/commit/b714f0318bfde5ccdf2ef1f9e9679d2d6267a926))
* **internal:** tweak CI branches ([9b5fa46](https://github.com/beeper/desktop-api-js/commit/9b5fa462f35e2476f528a8b5b432e870651b4ae9))
* **internal:** update gitignore ([701eec1](https://github.com/beeper/desktop-api-js/commit/701eec1214e0ef23d92a4dbfb6f9bc660e93f79c))
* **tests:** bump steady to v0.19.4 ([21244db](https://github.com/beeper/desktop-api-js/commit/21244db3761dd06eef1cbd431ee988dafed9e0b9))
* **tests:** bump steady to v0.19.5 ([afd0423](https://github.com/beeper/desktop-api-js/commit/afd0423f3fe2664e660ada160f79b111947cefb9))
* **tests:** bump steady to v0.19.6 ([9226701](https://github.com/beeper/desktop-api-js/commit/9226701faed2318b21d08d2d89208639628e3307))
* **tests:** bump steady to v0.19.7 ([e681061](https://github.com/beeper/desktop-api-js/commit/e681061239598ef4cb16a05ff80406b54934bfad))
* update SDK settings ([d4a125e](https://github.com/beeper/desktop-api-js/commit/d4a125ec0947b1e2f95d56ee9293254c6225118f))


### Refactors

* **tests:** switch from prism to steady ([215316c](https://github.com/beeper/desktop-api-js/commit/215316c820ab2067a629b325e87f99bc4352f163))

## 4.7.0 (2026-03-13)

Full Changelog: [v4.6.0...v4.7.0](https://github.com/beeper/desktop-api-js/compare/v4.6.0...v4.7.0)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ $ pnpm link --global @beeper/desktop-api

## Running tests

Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.

```sh
$ ./scripts/mock
Expand Down
4 changes: 0 additions & 4 deletions bin/check-release-environment
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

errors=()

if [ -z "${NPM_TOKEN}" ]; then
errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi

lenErrors=${#errors[@]}

if [[ lenErrors -gt 0 ]]; then
Expand Down
13 changes: 11 additions & 2 deletions bin/publish-npm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

set -eux

npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN"
if [[ ${NPM_TOKEN:-} ]]; then
npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN"
elif [[ ! ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-} ]]; then
echo "ERROR: NPM_TOKEN must be set if not running in a Github Action with id-token permission"
exit 1
fi

yarn build
cd dist
Expand Down Expand Up @@ -57,5 +62,9 @@ else
TAG="latest"
fi

# Install OIDC compatible npm version
npm install --prefix ../oidc/ npm@11.6.2

# Publish with the appropriate tag
yarn publish --tag "$TAG"
export npm_config_registry='https://registry.npmjs.org'
../oidc/node_modules/.bin/npm publish --tag "$TAG"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beeper/desktop-api",
"version": "4.7.0",
"version": "4.7.1",
"description": "The official TypeScript library for the Beeper Desktop API",
"author": "Beeper Desktop <help@beeper.com>",
"types": "dist/index.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": "@beeper/desktop-mcp",
"version": "4.7.0",
"version": "4.7.1",
"description": "The official MCP Server for the Beeper Desktop API",
"author": {
"name": "Beeper Desktop",
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": "@beeper/desktop-mcp",
"version": "4.7.0",
"version": "4.7.1",
"description": "The official MCP Server for the Beeper Desktop API",
"author": "Beeper Desktop <help@beeper.com>",
"types": "dist/index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion packages/mcp-server/src/code-tool-paths.cts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export const workerPath = require.resolve('./code-tool-worker.mjs');
export function getWorkerPath(): string {
return require.resolve('./code-tool-worker.mjs');
}
43 changes: 27 additions & 16 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// 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 @@ -154,19 +149,23 @@ const remoteStainlessHandler = async ({

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

const localClientEnvs = {
BEEPER_ACCESS_TOKEN: requireValue(
readEnv('BEEPER_ACCESS_TOKEN') ?? client.accessToken,
'set BEEPER_ACCESS_TOKEN environment variable or provide accessToken client option',
),
BEEPER_DESKTOP_BASE_URL: readEnv('BEEPER_DESKTOP_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({
BEEPER_ACCESS_TOKEN: requireValue(
readEnv('BEEPER_ACCESS_TOKEN') ?? client.accessToken,
'set BEEPER_ACCESS_TOKEN environment variable or provide accessToken client option',
),
BEEPER_DESKTOP_BASE_URL: readEnv('BEEPER_DESKTOP_BASE_URL') ?? client.baseURL ?? undefined,
}),
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
},
body: JSON.stringify({
project_name: 'beeper-desktop-api',
Expand Down Expand Up @@ -209,6 +208,13 @@ 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 @@ -270,6 +276,9 @@ 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 @@ -279,13 +288,15 @@ const localDenoHandler = async ({
reject(new Error(`Worker exited with code ${exitCode}`));
});

const opts: ClientOptions = {
baseURL: client.baseURL,
accessToken: client.accessToken,
// Strip null/undefined values so that the worker SDK client can fall back to
// reading from environment variables (including any upstreamClientEnvs).
const opts = {
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
...(client.accessToken != null ? { accessToken: client.accessToken } : undefined),
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
};
} satisfies Partial<ClientOptions> as ClientOptions;

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

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: mcpOptions,
mcpOptions: effectiveMcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
stainlessApiKey: stainlessApiKey,
upstreamClientEnvs,
});

return server;
Expand Down Expand Up @@ -109,7 +151,7 @@ const oauthMetadata = (req: express.Request, res: express.Response) => {
};

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

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

// Periodically evict stale entries so the cache doesn't grow unboundedly.
const _cacheCleanupInterval = setInterval(() => {
const now = Date.now();
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 now = Date.now();
const cacheKey = stainlessApiKey ?? '';
const cached = instructionsCache.get(cacheKey);

if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
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);
}
}

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

Expand Down Expand Up @@ -62,6 +57,7 @@ async function fetchLatestInstructions(stainlessApiKey: string | undefined): Pro

instructions ??= ((await response.json()) as { instructions: string }).instructions;

instructions += `\nAccess to all chats and messages across networks using Beeper Desktop. Can be used to find, get, send, and manage messages and chats.`;
instructions +=
'\nAccess to all chats and messages across networks using Beeper Desktop. Can be used to find, get, send, and manage messages and chats.';
return instructions;
}
Loading
Loading