Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ce8107e
codegen metadata
stainless-app[bot] Mar 18, 2026
75ac934
refactor(tests): switch from prism to steady
stainless-app[bot] Mar 20, 2026
e614b67
chore(tests): bump steady to v0.19.4
stainless-app[bot] Mar 21, 2026
eadfc98
chore(tests): bump steady to v0.19.5
stainless-app[bot] Mar 21, 2026
62d04a2
feat(api): api update
stainless-app[bot] Mar 21, 2026
408ed83
feat(api): api update
stainless-app[bot] Mar 23, 2026
9270ff5
chore(internal): update gitignore
stainless-app[bot] Mar 24, 2026
b7abcfb
chore(internal): fix MCP server TS errors that occur with required cl…
stainless-app[bot] Mar 24, 2026
4ed8bba
chore(tests): bump steady to v0.19.6
stainless-app[bot] Mar 24, 2026
0dc945e
codegen metadata
stainless-app[bot] Mar 24, 2026
14012e7
chore(ci): skip lint on metadata-only changes
stainless-app[bot] Mar 25, 2026
6aaa435
chore(tests): bump steady to v0.19.7
stainless-app[bot] Mar 25, 2026
acf8988
feat(api): api update
stainless-app[bot] Mar 26, 2026
97f213b
chore(internal): update multipart form array serialization
stainless-app[bot] Mar 27, 2026
c6d4043
chore(internal): support custom-instructions-path flag in MCP servers
stainless-app[bot] Mar 27, 2026
4a9b2cf
chore(internal): support local docs search in MCP servers
stainless-app[bot] Mar 28, 2026
40c8402
chore(ci): escape input path in publish-npm workflow
stainless-app[bot] Mar 28, 2026
1394392
feat(api): api update
stainless-app[bot] Mar 29, 2026
8ab7798
codegen metadata
stainless-app[bot] Mar 30, 2026
9e3018f
chore(mcp-server): add support for session id, forward client info
stainless-app[bot] Mar 31, 2026
f218072
chore(internal): improve local docs search for MCP servers
stainless-app[bot] Mar 31, 2026
0ebc5b3
codegen metadata
stainless-app[bot] Mar 31, 2026
bb96b8b
chore(tests): bump steady to v0.20.1
stainless-app[bot] Apr 1, 2026
eee2438
chore(internal): improve local docs search for MCP servers
stainless-app[bot] Apr 1, 2026
a305f3d
fix(internal): gitignore generated `oidc` dir
stainless-app[bot] Apr 1, 2026
2a71a6e
chore(tests): bump steady to v0.20.2
stainless-app[bot] Apr 1, 2026
41a4e6a
chore(internal): support type annotations when running MCP in local e…
stainless-app[bot] Apr 2, 2026
2b1db35
chore: configure new SDK language
stainless-app[bot] Apr 2, 2026
752879e
chore: update SDK settings
stainless-app[bot] Apr 2, 2026
4ee9dca
chore: update SDK settings
stainless-app[bot] Apr 2, 2026
1b7ca39
feat(api): manual updates
stainless-app[bot] Apr 2, 2026
5f1ad6d
chore(mcp-server): log client info
stainless-app[bot] Apr 3, 2026
138925b
feat(api): api update
stainless-app[bot] Apr 3, 2026
aa4d800
chore(internal): fix MCP server import ordering
stainless-app[bot] Apr 7, 2026
9451cde
feat(api): api update
stainless-app[bot] Apr 8, 2026
8254ca5
codegen metadata
stainless-app[bot] Apr 8, 2026
d0e4012
codegen metadata
stainless-app[bot] Apr 9, 2026
532797f
release: 0.35.1
stainless-app[bot] Apr 9, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-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 @@ -43,7 +43,7 @@ jobs:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-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
6 changes: 4 additions & 2 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ jobs:

- name: Publish to NPM
run: |
if [ -n "${{ github.event.inputs.path }}" ]; then
PATHS_RELEASED='[\"${{ github.event.inputs.path }}\"]'
if [ -n "$INPUT_PATH" ]; then
PATHS_RELEASED="[\"$INPUT_PATH\"]"
else
PATHS_RELEASED='[\".\", \"packages/mcp-server\"]'
fi
pnpm tsn scripts/publish-packages.ts "{ \"paths_released\": \"$PATHS_RELEASED\" }"
env:
INPUT_PATH: ${{ github.event.inputs.path }}

- name: Upload MCP Server DXT GitHub release asset
run: |
Expand Down
2 changes: 2 additions & 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 All @@ -10,3 +11,4 @@ dist-deno
.eslintcache
dist-bundle
*.mcpb
oidc
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.35.0"
".": "0.35.1"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-b1f2b7cb843e6f4e6123e838ce29cbbaea0a48b1a72057632de1d0d21727c5d8.yml
openapi_spec_hash: 21a354f587a2fe19797860c7b6da81a9
config_hash: 0ed970a9634b33d0af471738b478740d
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-460d7c66cd5e8cf979cd761066c51d8f813a119a20e2149fcfcf847eb650d545.yml
openapi_spec_hash: 8ee512464a88de45c86faf4f46f4905c
config_hash: bd8505e17db740d82e578d0edaa9bfe0
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
# Changelog

## 0.35.1 (2026-04-09)

Full Changelog: [v0.35.0...v0.35.1](https://github.com/hyperspell/node-sdk/compare/v0.35.0...v0.35.1)

### Features

* add 'mood' to memory type unions ([#89](https://github.com/hyperspell/node-sdk/issues/89)) ([76d16b8](https://github.com/hyperspell/node-sdk/commit/76d16b8175736cfd019f10f7363bb0d1b9d274a0))
* **api:** api update ([9451cde](https://github.com/hyperspell/node-sdk/commit/9451cde625b2736fee26be6e5181e6b1a0ffc5c2))
* **api:** api update ([138925b](https://github.com/hyperspell/node-sdk/commit/138925b62920e96037ff82db171bd96b8a049a98))
* **api:** api update ([1394392](https://github.com/hyperspell/node-sdk/commit/13943920182f9150b8222e5dc27d6fe69ecbc7eb))
* **api:** api update ([acf8988](https://github.com/hyperspell/node-sdk/commit/acf8988fd0e5ace07a871bbc2160db7d072effab))
* **api:** api update ([408ed83](https://github.com/hyperspell/node-sdk/commit/408ed8311554d2110d88031dad7a11f0943ce06c))
* **api:** api update ([62d04a2](https://github.com/hyperspell/node-sdk/commit/62d04a239910847ecbc4b644a13956f9b246a506))
* **api:** manual updates ([1b7ca39](https://github.com/hyperspell/node-sdk/commit/1b7ca39a9894d6649349ceb7d4f61de8d981c9f8))


### Bug Fixes

* **internal:** gitignore generated `oidc` dir ([a305f3d](https://github.com/hyperspell/node-sdk/commit/a305f3d8f340bbb1838b0e75ed0ca2e7da0d6cff))


### Chores

* **ci:** escape input path in publish-npm workflow ([40c8402](https://github.com/hyperspell/node-sdk/commit/40c84026a515ae0b62f676b71b8746ebfd254177))
* **ci:** skip lint on metadata-only changes ([14012e7](https://github.com/hyperspell/node-sdk/commit/14012e72febb7af66800c6e0f4c389cf5104a1d3))
* configure new SDK language ([2b1db35](https://github.com/hyperspell/node-sdk/commit/2b1db353a21ecb4c3200e44d7853587c90c7625c))
* **internal:** fix MCP server import ordering ([aa4d800](https://github.com/hyperspell/node-sdk/commit/aa4d8007866d8417cb8af1c9d17c11d59d8c1d3c))
* **internal:** fix MCP server TS errors that occur with required client options ([b7abcfb](https://github.com/hyperspell/node-sdk/commit/b7abcfb96eac3b96305957762f6cadb399236583))
* **internal:** improve local docs search for MCP servers ([eee2438](https://github.com/hyperspell/node-sdk/commit/eee2438cecb6267496d8b068d2b6b370dcce586c))
* **internal:** improve local docs search for MCP servers ([f218072](https://github.com/hyperspell/node-sdk/commit/f218072eee356e59cb1f7b0e3e667902acb30c79))
* **internal:** support custom-instructions-path flag in MCP servers ([c6d4043](https://github.com/hyperspell/node-sdk/commit/c6d404331011597d4ee14440eeb71faaed16ad95))
* **internal:** support local docs search in MCP servers ([4a9b2cf](https://github.com/hyperspell/node-sdk/commit/4a9b2cfd823fce71d3a08e435e545a97247abba5))
* **internal:** support type annotations when running MCP in local execution mode ([41a4e6a](https://github.com/hyperspell/node-sdk/commit/41a4e6a998f03224e0aa765c348f7d4d96a98d01))
* **internal:** update gitignore ([9270ff5](https://github.com/hyperspell/node-sdk/commit/9270ff5aac78355a25f64d1ccabb773c45120f36))
* **internal:** update multipart form array serialization ([97f213b](https://github.com/hyperspell/node-sdk/commit/97f213bf19e619a90ca72254e6e9d407aaecb41b))
* **mcp-server:** add support for session id, forward client info ([9e3018f](https://github.com/hyperspell/node-sdk/commit/9e3018f7649a0533788b5a9a75875dbfde5cd493))
* **mcp-server:** log client info ([5f1ad6d](https://github.com/hyperspell/node-sdk/commit/5f1ad6d048afabe3d270edf2fd8905053a7cb29f))
* **tests:** bump steady to v0.19.4 ([e614b67](https://github.com/hyperspell/node-sdk/commit/e614b6739cf3d7398e4d05d806ae0706c3264276))
* **tests:** bump steady to v0.19.5 ([eadfc98](https://github.com/hyperspell/node-sdk/commit/eadfc98aa8e6dfc6728a8028f01abcb4ffe91c8c))
* **tests:** bump steady to v0.19.6 ([4ed8bba](https://github.com/hyperspell/node-sdk/commit/4ed8bbac6d2a25580c3355ec7d5309ba18ffd3be))
* **tests:** bump steady to v0.19.7 ([6aaa435](https://github.com/hyperspell/node-sdk/commit/6aaa43556ea42c3c3440569b051c25adda5046c7))
* **tests:** bump steady to v0.20.1 ([bb96b8b](https://github.com/hyperspell/node-sdk/commit/bb96b8b039f8a6d906540298e31ebe3e0cd4549e))
* **tests:** bump steady to v0.20.2 ([2a71a6e](https://github.com/hyperspell/node-sdk/commit/2a71a6e88d24933ce432279a88062fa849454586))
* update SDK settings ([4ee9dca](https://github.com/hyperspell/node-sdk/commit/4ee9dca0d9d188abab3fabe72c5a9d2e53e49994))
* update SDK settings ([752879e](https://github.com/hyperspell/node-sdk/commit/752879e4c937f55b93ab0da3a002e18bb71240c5))


### Refactors

* **tests:** switch from prism to steady ([75ac934](https://github.com/hyperspell/node-sdk/commit/75ac9345ea6849863c3469f24dc2393046a6dcd0))

## 0.35.0 (2026-03-18)

Full Changelog: [v0.34.0...v0.35.0](https://github.com/hyperspell/node-sdk/compare/v0.34.0...v0.35.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 hyperspell

## 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperspell",
"version": "0.35.0",
"version": "0.35.1",
"description": "The official TypeScript library for the Hyperspell API",
"author": "Hyperspell <hello@hyperspell.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": "hyperspell-mcp",
"version": "0.35.0",
"version": "0.35.1",
"description": "The official MCP Server for the Hyperspell API",
"author": {
"name": "Hyperspell",
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperspell-mcp",
"version": "0.35.0",
"version": "0.35.1",
"description": "The official MCP Server for the Hyperspell API",
"author": "Hyperspell <hello@hyperspell.com>",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -40,6 +40,7 @@
"cors": "^2.8.5",
"express": "^5.1.0",
"fuse.js": "^7.1.0",
"minisearch": "^7.2.0",
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
Expand Down
12 changes: 10 additions & 2 deletions packages/mcp-server/src/code-tool-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import ts from 'typescript';
import { WorkerOutput } from './code-tool-types';
import { Hyperspell, ClientOptions } from 'hyperspell';

async function tseval(code: string) {
Comment on lines 8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Buffer is used as a global, but in a Deno environment (evidenced by the export default { fetch } pattern and Deno stack trace parsing) it is not globally available without an explicit import { Buffer } from 'node:buffer' — this will throw a ReferenceError at runtime when tseval is called.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `packages/mcp-server/src/code-tool-worker.ts`, the `tseval` function introduced at line 8-10 uses `Buffer` as a global. This file runs in a Deno environment (see `export default { fetch }` at the bottom and the Deno-specific stack trace comment in `parseError`). In Deno, `Buffer` is not a global and must be explicitly imported. Add `import { Buffer } from 'node:buffer';` at the top of the file alongside the existing `node:path` and `node:util` imports to prevent a ReferenceError at runtime.

Comment on lines 8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Buffer is not a global in Deno environments; the file uses node:path and node:util prefixed imports suggesting Deno execution, so Buffer.from(...) will throw ReferenceError: Buffer is not defined unless explicitly imported from node:buffer.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 8-10, the `tseval` function uses `Buffer.from()` but `Buffer` is not a global in Deno environments. Add `import { Buffer } from 'node:buffer';` at the top of the file alongside the other `node:` prefixed imports (`node:path`, `node:util`) to ensure `Buffer` is explicitly available.

Comment on lines 8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Buffer is a Node.js global; the rest of the file (e.g. parseError's stack comment "Deno uses V8") and the data:application/typescript import syntax strongly indicate this runs on Deno, where Buffer is not a guaranteed global and will throw ReferenceError: Buffer is not defined at runtime.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 8-10, the tseval function uses `Buffer.from(code).toString('base64')` to base64-encode the TypeScript source string. This file runs in a Deno environment (evidenced by the 'Deno uses V8' comment in parseError and the use of data:application/typescript imports which are Deno-specific). `Buffer` is a Node.js global and is not reliably available in Deno without explicit import, causing a ReferenceError at runtime. Replace `Buffer.from(code).toString('base64')` with a Deno/browser-compatible alternative such as `btoa(unescape(encodeURIComponent(code)))` to handle arbitrary Unicode input correctly.

Comment on lines 8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The code parameter originates directly from user-supplied request body (req.json()) and is executed without any sandboxing — an attacker who can reach this endpoint can run arbitrary code in the worker process with full access to the environment, credentials, and network.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 8-10, the `tseval` function dynamically imports user-supplied code via a data URL. The `code` argument originates from the raw request body with no sandboxing. Evaluate whether the endpoint that calls this worker is protected by authentication/authorization, and consider whether the execution environment (Deno/Cloudflare Worker) provides any meaningful isolation. At minimum, document the trust boundary explicitly and ensure no unauthenticated path can reach this handler.

Comment on lines 8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The code argument passed to tseval originates directly from the request body (req.json()) with no sandboxing beyond the proxy — a malicious caller can execute arbitrary code in the worker process by crafting the code field of the request payload.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts at lines 8-10, the tseval function dynamically imports and executes arbitrary TypeScript code that comes directly from an untrusted HTTP request body. Ensure this worker runs in a fully isolated sandbox (e.g., a separate Deno/Worker process with no access to secrets or the filesystem), enforce strict origin/auth checks before the fetch handler is invoked, or document the trust boundary explicitly if the endpoint is internal-only.

return import('data:application/typescript;charset=utf-8;base64,' + Buffer.from(code).toString('base64'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Buffer is a Node.js global and is not natively available in Deno unless Node.js compatibility mode is enabled; if this worker runs in a standard Deno environment, Buffer.from(code).toString('base64') will throw ReferenceError: Buffer is not defined. Use the Web-standard btoa(encodeURIComponent(code).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)))) or explicitly import Buffer from node:buffer.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `packages/mcp-server/src/code-tool-worker.ts`, line 11, the `tseval` function uses the global `Buffer` which is Node.js-specific and unavailable in Deno without Node.js compat mode. Replace the global `Buffer` usage with an explicit import from `node:buffer`: add `const { Buffer: NodeBuffer } = await import('node:buffer');` before the return statement and replace `Buffer.from(code).toString('base64')` with `NodeBuffer.from(code).toString('base64')`. Alternatively, if the environment is guaranteed to have Node.js compat, add a top-level import `import { Buffer } from 'node:buffer';` at the top of the file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Buffer is a Node.js global not available in Deno or edge runtimes; the parseError function explicitly mentions Deno and the module uses export default { fetch } (edge worker pattern), so this will throw ReferenceError: Buffer is not defined at runtime in those environments.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts at line 11, the `tseval` function uses `Buffer.from(code).toString('base64')` to base64-encode the TypeScript code. `Buffer` is a Node.js-only global and is not available in Deno or edge runtimes (the file uses `export default { fetch }` pattern and `parseError` mentions Deno explicitly). Replace `Buffer.from(code).toString('base64')` with a runtime-agnostic base64 encoding such as `btoa(unescape(encodeURIComponent(code)))` to handle Unicode safely across all environments.

}

function getRunFunctionSource(code: string): {
type: 'declaration' | 'expression';
client: string | undefined;
Expand Down Expand Up @@ -266,7 +270,9 @@ const fetch = async (req: Request): Promise<Response> => {

const log_lines: string[] = [];
const err_lines: string[] = [];
Comment on lines 271 to 272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Mutating globalThis.console is shared state — concurrent async requests will interleave, causing one request's patched console to be overwritten by another's mid-execution, so log_lines/err_lines will capture output from the wrong request.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-278 (the diff adding `globalThis.console = {...}`), there is a race condition: `globalThis.console` is a single shared global. When two async `fetch` requests run concurrently and both reach the `await run_(...)` call, each request will overwrite the other's patched console, so logs will be captured in the wrong request's `log_lines`/`err_lines` arrays. Fix this by not mutating `globalThis.console` at all — instead, pass a custom console object directly into `run_` (e.g. as a second argument), or use AsyncLocalStorage to scope the console interception per-request, ensuring concurrent requests do not interfere with each other.

Comment on lines 271 to 272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🔴 Mutating globalThis.console is not concurrency-safe in a server handling multiple simultaneous requests — request B will capture request A's patched console as its originalConsole, and log lines from different requests will bleed into each other's log_lines/err_lines arrays until the finally block restores the wrong reference.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-278, the diff replaces a local `const console` shadow with direct mutation of `globalThis.console`. This is unsafe under concurrent requests: each request saves and restores `globalThis.console`, but overlapping requests can interleave such that (1) one request's `log` override overwrites another's, and (2) the 'original' captured by request B is already request A's patched console. Fix by keeping the original approach of passing a custom console-like object directly to user code (e.g., inject it as a parameter or use a module-scoped override only inside `tseval`) instead of mutating the shared global.

Comment on lines 271 to 272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🐛 Mutating globalThis.console is not safe under concurrent requests. If two requests overlap, Request B captures Request A's overriding console as its originalConsole, and when B's finally block restores it, A's capturing function becomes the permanent global console — leaking captured log lines across requests indefinitely.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-272 introduce a race condition: `globalThis.console` is mutated for the duration of an async request, but concurrent requests can interleave their save/restore operations, causing one request's capturing console to leak as the global after another request's `finally` block runs. Fix this by avoiding mutation of the shared global — instead, pass a custom console object directly into the evaluated code's scope (e.g. inject it as a variable alongside `client`), or use a per-invocation wrapper that does not mutate `globalThis`.

Comment on lines 271 to 272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🐛 Mutating globalThis.console inside an async function creates a race condition: if two requests are in-flight concurrently, the second request overwrites the first's patched console, causing the first request's finally block to restore the wrong original and the second's log capture to be silently dropped or mixed.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `packages/mcp-server/src/code-tool-worker.ts`, lines 271-272 (and the matching `finally` block), `globalThis.console` is overwritten inside an async `fetch` handler. Because multiple requests can be in-flight concurrently (each `await` is a yield point), a second request will overwrite the first's patched console before the first's `finally` restores it, causing mixed log capture and incorrect restoration. Fix this by avoiding mutation of the shared global: instead of patching `globalThis.console`, pass a local console-like object directly into the evaluated code, or serialize requests so only one patch is active at a time.

const console = {
const originalConsole = globalThis.console;
globalThis.console = {
...originalConsole,
log: (...args: unknown[]) => {
log_lines.push(util.format(...args));
},
Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🔴 Mutating globalThis.console is not safe for concurrent requests: if two fetch calls overlap, Request B will capture Request A's patched console as its originalConsole, and the finally restore in B will leave the global pointing at A's interceptor instead of the real console, permanently leaking log capture across requests.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-278, the code patches `globalThis.console` to intercept log output from user-supplied code. Because `fetch` is async and can be called concurrently, multiple requests can interleave: Request B may snapshot Request A's already-patched console as its 'originalConsole', causing the finally-restore in B to leave `globalThis.console` pointing at A's interceptor. Fix by either: (1) using an AsyncLocalStorage context to scope the console override per-request without mutating shared global state, or (2) running the user code in an isolated worker where `console` can be safely replaced without affecting other concurrent requests.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Mutating globalThis.console is not safe if two requests execute concurrently: the second request overwrites the first's interception, causing log lines to route to the wrong request's arrays, and the finally restore in one request undoes the other's setup mid-execution.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In file `packages/mcp-server/src/code-tool-worker.ts`, lines 271-278 (and the corresponding finally block), the code mutates `globalThis.console` which is shared across concurrent request handlers. Two simultaneous fetch calls will stomp on each other's console override, routing log output to the wrong request's `log_lines`/`err_lines` arrays, and the `finally` restore from one request will undo the other's setup. Fix this by avoiding global mutation: instead of replacing `globalThis.console`, pass custom log/error functions as part of the execution context (e.g., inject them via a context object or closure into the tseval'd code), or ensure the worker is strictly single-threaded and document that assumption, or use an AsyncLocalStorage-based approach to track per-request console state without global mutation.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🐛 Mutating globalThis.console is shared state across all concurrent requests: if two fetch calls overlap, request B will overwrite request A's interceptor, and when A's finally block runs globalThis.console = originalConsole it will silently discard B's interceptor mid-flight, causing B's console.log/console.error calls to go uncaptured (or to the real console).

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts at lines 271-278, the diff replaces a local `const console` with a mutation of `globalThis.console`. Because `fetch` is an async handler that can be invoked concurrently, this creates a race condition: concurrent requests share the same `globalThis.console` reference, so one request can stomp another's interceptor, and the `finally` restore can remove a still-active interceptor from a concurrent request. Fix this by passing a custom console object directly to the user code instead of monkey-patching the global. For example, thread a `{ log, error }` console shim as a second argument to `run_`, or inject it via the proxy, so each request operates on its own isolated capture object with no global mutation.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Mutating globalThis.console in an async handler creates a race condition: if two requests overlap, one request's finally block will restore the console to the second request's overridden version (not the true original), causing log/error lines to leak between requests or be lost entirely.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-278, the diff introduces a race condition by saving and restoring `globalThis.console` in an async request handler. When two requests overlap, the second request's `finally` block may restore `globalThis.console` to the first request's overridden console rather than the true original. Fix this by avoiding mutation of `globalThis.console` entirely — instead, pass a custom console object (with overridden `log` and `error`) directly to the evaluated code, or use AsyncLocalStorage to isolate per-request console state.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Mutating globalThis.console is not safe under concurrent requests: if two requests are in-flight simultaneously (e.g., both reach await run_(...)), the second request overwrites the console set by the first, causing log lines to be captured into the wrong request's log_lines/err_lines arrays and potentially leaving globalThis.console in a corrupted state after finally restores only the version saved by the first request.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts around lines 271-278, the diff replaces a local `const console` shadow with a mutation of `globalThis.console`. This introduces a race condition: under concurrent async requests, two handlers can interleave at await points, causing each to stomp the other's console override and mix up captured log/error lines. Fix by avoiding global mutation entirely — for example, pass a custom console object directly into the evaluated code's scope rather than monkey-patching `globalThis.console`, or use AsyncLocalStorage to isolate per-request console state.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🐛 Mutating globalThis.console is not safe under concurrent requests — if two requests are in-flight simultaneously, Request B will overwrite globalThis.console with its own capture object, so Request A's console.log calls will be recorded into B's log_lines, and the finally restores will race each other. The previous local const console shadowing was request-scoped; this new approach makes console capture a shared mutable global that breaks under concurrency.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts, lines 271-278, the diff replaces a local `const console = {...}` with a mutation of `globalThis.console`. This creates a race condition when multiple requests are handled concurrently: whichever request last sets `globalThis.console` wins, causing cross-request log capture pollution and lost logs. Fix this by using an AsyncLocalStorage or similar per-request context to pass the capture console to the evaluated code, rather than mutating the shared global. Alternatively, if evaluated code must use globalThis.console, serialize request handling or find another isolation mechanism.

Comment on lines 271 to 278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Mutating globalThis.console is not safe under concurrent requests: if two requests interleave across the await tseval(...) / await run_(...) calls, one request's console override will clobber the other's, causing log lines to be captured into the wrong request's arrays or lost entirely after the finally restores.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/mcp-server/src/code-tool-worker.ts around lines 271-278, the code mutates globalThis.console to capture log output from dynamically evaluated user code. Because the fetch handler is async and yields at `await tseval(...)` and `await run_(...)`, concurrent requests can interleave and overwrite each other's globalThis.console override, causing log lines to be routed to the wrong request's log_lines/err_lines arrays or discarded. Consider passing a scoped console object directly into the evaluated code (e.g., injecting it as a global in the data URL or using a per-request wrapper) instead of mutating the shared globalThis.console.

Expand All @@ -276,7 +282,7 @@ const fetch = async (req: Request): Promise<Response> => {
};
try {
let run_ = async (client: any) => {};
eval(`${code}\nrun_ = run;`);
run_ = (await tseval(`${code}\nexport default run;`)).default;
const result = await run_(makeSdkProxy(client, { path: ['client'] }));
return Response.json({
is_error: false,
Expand All @@ -294,6 +300,8 @@ const fetch = async (req: Request): Promise<Response> => {
} satisfies WorkerOutput,
{ status: 400, statusText: 'Code execution error' },
);
} finally {
globalThis.console = originalConsole;
}
};

Expand Down
18 changes: 8 additions & 10 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,16 +285,14 @@ const localDenoHandler = async ({

// 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,
apiKey: client.apiKey,
userID: client.userID,
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
}).filter(([_, v]) => v != null),
) as ClientOptions;
const opts = {
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
...(client.apiKey != null ? { apiKey: client.apiKey } : undefined),
...(client.userID != null ? { userID: client.userID } : undefined),
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
} satisfies Partial<ClientOptions> as ClientOptions;

const req = worker.request(
'http://localhost',
Expand Down
54 changes: 46 additions & 8 deletions packages/mcp-server/src/docs-search-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { Metadata, McpRequestContext, asTextContentResult } from './types';
import { getLogger } from './logger';
import type { LocalDocsSearch } from './local-docs-search';

export const metadata: Metadata = {
resource: 'all',
Expand Down Expand Up @@ -43,20 +44,41 @@ export const tool: Tool = {
const docsSearchURL =
process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/hyperspell/docs/search';

export const handler = async ({
reqContext,
args,
}: {
reqContext: McpRequestContext;
args: Record<string, unknown> | undefined;
}) => {
let _localSearch: LocalDocsSearch | undefined;

export function setLocalSearch(search: LocalDocsSearch): void {
_localSearch = search;
}

async function searchLocal(args: Record<string, unknown>): Promise<unknown> {
if (!_localSearch) {
throw new Error('Local search not initialized');
}

const query = (args['query'] as string) ?? '';
const language = (args['language'] as string) ?? 'typescript';
const detail = (args['detail'] as string) ?? 'default';

return _localSearch.search({
query,
language,
detail,
maxResults: 5,
}).results;
}

async function searchRemote(args: Record<string, unknown>, reqContext: McpRequestContext): Promise<unknown> {
const body = args as any;
const query = new URLSearchParams(body).toString();

const startTime = Date.now();
const result = await fetch(`${docsSearchURL}?${query}`, {
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
...(reqContext.mcpSessionId && { 'x-stainless-mcp-session-id': reqContext.mcpSessionId }),
...(reqContext.mcpClientInfo && {
'x-stainless-mcp-client-info': JSON.stringify(reqContext.mcpClientInfo),
}),
},
});

Expand Down Expand Up @@ -94,7 +116,23 @@ export const handler = async ({
},
'Got docs search result',
);
return asTextContentResult(resultBody);
return resultBody;
}

export const handler = async ({
reqContext,
args,
}: {
reqContext: McpRequestContext;
args: Record<string, unknown> | undefined;
}) => {
const body = args ?? {};

if (_localSearch) {
return asTextContentResult(await searchLocal(body));
}

return asTextContentResult(await searchRemote(body, reqContext));
};

export default { metadata, tool, handler };
28 changes: 27 additions & 1 deletion packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const newServer = async ({
res: express.Response;
}): Promise<McpServer | null> => {
const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
const server = await newMcpServer(stainlessApiKey);
const customInstructionsPath = mcpOptions.customInstructionsPath;
const server = await newMcpServer({ stainlessApiKey, customInstructionsPath });

const authOptions = parseClientAuthHeaders(req, false);

Expand Down Expand Up @@ -68,6 +69,11 @@ const newServer = async ({
}
}

const mcpClientInfo =
typeof req.body?.params?.clientInfo?.name === 'string' ?
{ name: req.body.params.clientInfo.name, version: String(req.body.params.clientInfo.version ?? '') }
: undefined;

await initMcpServer({
server: server,
mcpOptions: effectiveMcpOptions,
Expand All @@ -77,8 +83,14 @@ const newServer = async ({
},
stainlessApiKey: stainlessApiKey,
upstreamClientEnvs,
mcpSessionId: (req as any).mcpSessionId,
mcpClientInfo,
});

if (mcpClientInfo) {
getLogger().info({ mcpSessionId: (req as any).mcpSessionId, mcpClientInfo }, 'MCP client connected');
}

return server;
};

Expand Down Expand Up @@ -134,9 +146,23 @@ export const streamableHTTPApp = ({
const app = express();
app.set('query parser', 'extended');
app.use(express.json());
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
const existing = req.headers['mcp-session-id'];
const sessionId = (Array.isArray(existing) ? existing[0] : existing) || crypto.randomUUID();
(req as any).mcpSessionId = sessionId;
const origWriteHead = res.writeHead.bind(res);
res.writeHead = function (statusCode: number, ...rest: any[]) {
res.setHeader('mcp-session-id', sessionId);
return origWriteHead(statusCode, ...rest);
} as typeof res.writeHead;
next();
});
app.use(
pinoHttp({
logger: getLogger(),
customProps: (req) => ({
mcpSessionId: (req as any).mcpSessionId,
}),
customLogLevel: (req, res) => {
if (res.statusCode >= 500) {
return 'error';
Expand Down
Loading
Loading