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
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,28 @@ jobs:
run: ./scripts/build

- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/hyperspell-typescript'
if: |-
github.repository == 'stainless-sdks/hyperspell-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());

- name: Upload tarball
if: github.repository == 'stainless-sdks/hyperspell-typescript'
if: |-
github.repository == 'stainless-sdks/hyperspell-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh

- name: Upload MCP Server tarball
if: github.repository == 'stainless-sdks/hyperspell-typescript'
if: |-
github.repository == 'stainless-sdks/hyperspell-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s?subpackage=mcp-server
AUTH: ${{ steps.github-oidc.outputs.github_token }}
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 @@
{
".": "0.32.1"
".": "0.33.0"
}
4 changes: 2 additions & 2 deletions .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/hyperspell%2Fhyperspell-ca07e6605f61ae00e12be55df648b38e467a31d505fdeec7879c8a9ea9e1b390.yml
openapi_spec_hash: 25915d4fcda54adbd8a7f106d8af2d65
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-d6e895ab5ce17b403a1981c9f3e3e1a357d2016683627cbbc725c10f6aa2e13a.yml
openapi_spec_hash: 36fc6b210e87fbd995fd578adcbe6626
config_hash: fd3005a8f140e5baadd3d25b3c9cd79f
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.33.0 (2026-03-08)

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

### Features

* **api:** api update ([c5c7f31](https://github.com/hyperspell/node-sdk/commit/c5c7f31a366080de58b305429d0bcb84dec91493))


### Bug Fixes

* **client:** preserve URL params already embedded in path ([af6d83b](https://github.com/hyperspell/node-sdk/commit/af6d83b7176a0e8672c1ee54a6db5a4bd48d4bd9))


### Chores

* **ci:** skip uploading artifacts on stainless-internal branches ([6a453f9](https://github.com/hyperspell/node-sdk/commit/6a453f9854dd43f6ee2d20674cd31e72743327f4))
* **internal:** codegen related update ([c49250c](https://github.com/hyperspell/node-sdk/commit/c49250ccd40e962f760fcec44f6ecd85856a1578))
* **internal:** codegen related update ([e7684c4](https://github.com/hyperspell/node-sdk/commit/e7684c45c0ad25bf0015ccd24d98ea182f8d6055))
* **internal:** use x-stainless-mcp-client-envs header for MCP remote code tool calls ([f8b4ea0](https://github.com/hyperspell/node-sdk/commit/f8b4ea07e91449a9262938777c8c32a3b30c9aba))
* **mcp-server:** improve instructions ([604dcdd](https://github.com/hyperspell/node-sdk/commit/604dcddc6a332f9990e06eacf0edcb64bb2fd171))
* **mcp-server:** return access instructions for 404 without API key ([2750b02](https://github.com/hyperspell/node-sdk/commit/2750b02b4f1e091728ed4c9d202cd3387ed51294))
* **test:** do not count install time for mock server timeout ([1d19a8e](https://github.com/hyperspell/node-sdk/commit/1d19a8e8ca7be9782a217c28f12c0329931a725d))
* update placeholder string ([b91b593](https://github.com/hyperspell/node-sdk/commit/b91b5935f49d8d5be04738bd2cda7dce878ea736))

## 0.32.1 (2026-03-02)

Full Changelog: [v0.32.0...v0.32.1](https://github.com/hyperspell/node-sdk/compare/v0.32.0...v0.32.1)
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.32.1",
"version": "0.33.0",
"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.32.1",
"version": "0.33.0",
"description": "The official MCP Server for the Hyperspell API",
"author": {
"name": "Hyperspell",
Expand Down
8 changes: 4 additions & 4 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperspell-mcp",
"version": "0.32.1",
"version": "0.33.0",
"description": "The official MCP Server for the Hyperspell API",
"author": "Hyperspell <hello@hyperspell.com>",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -39,8 +39,9 @@
"express": "^5.1.0",
"fuse.js": "^7.1.0",
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz",
"morgan": "^1.10.0",
"morgan-body": "^2.6.9",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"qs": "^6.14.1",
"typescript": "5.8.3",
"yargs": "^17.7.2",
Expand All @@ -57,7 +58,6 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jest": "^29.4.0",
"@types/morgan": "^1.9.10",
"@types/qs": "^6.14.0",
"@types/yargs": "^17.0.8",
"@typescript-eslint/eslint-plugin": "8.31.1",
Expand Down
30 changes: 27 additions & 3 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { readEnv, requireValue } from './util';
import { WorkerInput, WorkerOutput } from './code-tool-types';
import { getLogger } from './logger';
import { SdkMethod } from './methods';
import { McpCodeExecutionMode } from './options';
import { ClientOptions } from 'hyperspell';
Expand Down Expand Up @@ -83,6 +84,8 @@ export function codeTool({
},
};

const logger = getLogger();

const handler = async ({
reqContext,
args,
Expand All @@ -107,11 +110,27 @@ export function codeTool({
}
}

let result: ToolCallResult;
const startTime = Date.now();

if (codeExecutionMode === 'local') {
return await localDenoHandler({ reqContext, args });
logger.debug('Executing code in local Deno environment');
result = await localDenoHandler({ reqContext, args });
} else {
return await remoteStainlessHandler({ reqContext, args });
logger.debug('Executing code in remote Stainless environment');
result = await remoteStainlessHandler({ reqContext, args });
}

logger.info(
{
codeExecutionMode,
durationMs: Date.now() - startTime,
isError: result.isError,
contentRows: result.content?.length ?? 0,
},
'Got code tool execution result',
);
return result;
};

return { metadata, tool, handler };
Expand All @@ -136,7 +155,7 @@ const remoteStainlessHandler = async ({
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
'Content-Type': 'application/json',
client_envs: JSON.stringify({
'x-stainless-mcp-client-envs': JSON.stringify({
HYPERSPELL_API_KEY: requireValue(
readEnv('HYPERSPELL_API_KEY') ?? client.apiKey,
'set HYPERSPELL_API_KEY environment variable or provide apiKey client option',
Expand All @@ -153,6 +172,11 @@ const remoteStainlessHandler = async ({
});

if (!res.ok) {
if (res.status === 404 && !reqContext.stainlessApiKey) {
throw new Error(
'Could not access code tool for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
);
}
throw new Error(
`${res.status}: ${
res.statusText
Expand Down
40 changes: 36 additions & 4 deletions packages/mcp-server/src/docs-search-tool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { Metadata, McpRequestContext, asTextContentResult } from './types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { Metadata, McpRequestContext, asTextContentResult } from './types';
import { getLogger } from './logger';

export const metadata: Metadata = {
resource: 'all',
Expand All @@ -12,7 +13,8 @@ export const metadata: Metadata = {

export const tool: Tool = {
name: 'search_docs',
description: 'Search for documentation for how to use the client to interact with the API.',
description:
'Search SDK documentation to find methods, parameters, and usage examples for interacting with the API. Use this before writing code when you need to discover the right approach.',
inputSchema: {
type: 'object',
properties: {
Expand Down Expand Up @@ -50,19 +52,49 @@ export const handler = async ({
}) => {
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 }),
},
});

const logger = getLogger();

Choose a reason for hiding this comment

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

Correctness: Calling getLogger() introduces a reliability regression. Since getLogger() throws an error if the logger is not configured, this handler will now crash in environments where logging is not initialized, whereas it previously functioned correctly. Guard the logger call or provide a fallback to ensure the tool remains functional regardless of the logging state.

🤖 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/docs-search-tool.ts around the new logging additions (line ~63), guard getLogger() so the handler doesn’t throw if logging isn’t configured. Wrap getLogger() in try/catch and make logging calls optional (logger?.info/warn) or ensure configuration occurs before this handler runs.

if (!result.ok) {
const errorText = await result.text();
logger.warn(
{
durationMs: Date.now() - startTime,
query: body.query,
status: result.status,
statusText: result.statusText,
errorText,
},
'Got error response from docs search tool',
);

if (result.status === 404 && !reqContext.stainlessApiKey) {
throw new Error(
'Could not find docs for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
);
}

throw new Error(
`${result.status}: ${result.statusText} when using doc search tool. Details: ${await result.text()}`,
`${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`,
);
}

return asTextContentResult(await result.json());
const resultBody = await result.json();
logger.info(
{
durationMs: Date.now() - startTime,
query: body.query,
Comment on lines +70 to +93

Choose a reason for hiding this comment

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

Correctness: 🚫 args can be undefined per the handler signature, so body.query will throw during logging. Use optional chaining or a default to avoid crashing the handler just for telemetry.

🤖 AI Agent Prompt for Cursor/Windsurf

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

File: packages/mcp-server/src/docs-search-tool.ts. Lines 69-92. The new logging accesses body.query, but args can be undefined, causing a runtime error. Update both logger.warn and logger.info payloads to use optional chaining (body?.query) or a safe default so logging does not throw when args is missing.

},
'Got docs search result',
);
return asTextContentResult(resultBody);
};

export default { metadata, tool, handler };
74 changes: 53 additions & 21 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ClientOptions } from 'hyperspell';
import express from 'express';
import morgan from 'morgan';
import morganBody from 'morgan-body';
import pino from 'pino';
import pinoHttp from 'pino-http';
import { getStainlessApiKey, parseClientAuthHeaders } from './auth';
import { getLogger } from './logger';
import { McpOptions } from './options';
import { initMcpServer, newMcpServer } from './server';

Comment on lines 4 to 13

Choose a reason for hiding this comment

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

Correctness: The introduction of getLogger() in streamableHTTPApp and launchStreamableHTTPServer will cause a runtime crash. The getLogger() implementation in logger.ts is designed to throw an error if _logger is not initialized via configureLogger(). Since launchStreamableHTTPServer does not invoke configureLogger() before calling streamableHTTPApp, the server will fail to start. You must ensure the logger is configured at the start of the server lifecycle or provide a default logger instance.

Expand Down Expand Up @@ -70,29 +71,60 @@ const del = async (req: express.Request, res: express.Response) => {
});
};

const redactHeaders = (headers: Record<string, any>) => {
const hiddenHeaders = /auth|cookie|key|token/i;
const filtered = { ...headers };
Object.keys(filtered).forEach((key) => {
if (hiddenHeaders.test(key)) {
filtered[key] = '[REDACTED]';
}
});
return filtered;
};

export const streamableHTTPApp = ({
clientOptions = {},
mcpOptions,
debug,
}: {
clientOptions?: ClientOptions;
mcpOptions: McpOptions;
debug: boolean;
}): express.Express => {
const app = express();
app.set('query parser', 'extended');
app.use(express.json());

if (debug) {
morganBody(app, {
logAllReqHeader: true,
logAllResHeader: true,
logRequestBody: true,
logResponseBody: true,
});
} else {
app.use(morgan('combined'));
}
app.use(
pinoHttp({
logger: getLogger(),
customLogLevel: (req, res) => {
if (res.statusCode >= 500) {
return 'error';
} else if (res.statusCode >= 400) {
return 'warn';
}
return 'info';
},
customSuccessMessage: function (req, res) {
return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`;
},
customErrorMessage: function (req, res, err) {
return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`;
},
serializers: {
req: pino.stdSerializers.wrapRequestSerializer((req) => {
return {
...req,
headers: redactHeaders(req.raw.headers),
};
}),
res: pino.stdSerializers.wrapResponseSerializer((res) => {
return {
...res,
headers: redactHeaders(res.headers),
};
Comment on lines +115 to +123

Choose a reason for hiding this comment

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

Correctness: ⚠️ req.raw is undefined on the serialized request object, so req.raw.headers will throw a TypeError at runtime. Additionally, res.headers is not a property of the serialized response object. Use req.headers (which is already available) and access the raw response object to call getHeaders() for response redaction. 🛡️

🤖 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/http.ts` around the pino serializers, replace direct access to `req.raw.headers` and `res.headers` with safe fallbacks (e.g., `req.raw?.headers ?? req.headers ?? {}` and `res.getHeaders?.() ?? res.headers ?? {}`) so `redactHeaders` never receives `undefined`.

}),
},
}),
);

app.get('/health', async (req: express.Request, res: express.Response) => {
res.status(200).send('OK');
Expand All @@ -106,22 +138,22 @@ export const streamableHTTPApp = ({

export const launchStreamableHTTPServer = async ({
mcpOptions,
debug,
port,
}: {
mcpOptions: McpOptions;
debug: boolean;
port: number | string | undefined;
}) => {
const app = streamableHTTPApp({ mcpOptions, debug });
const app = streamableHTTPApp({ mcpOptions });
const server = app.listen(port);
const address = server.address();

Choose a reason for hiding this comment

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

Correctness: Using getLogger() here can now throw if configureLogger() hasn’t been called, which is a behavior change from the previous console fallback. This makes launchStreamableHTTPServer crash for consumers that relied on the old behavior. Consider falling back to console when the logger isn’t configured. 🚨

🤖 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/http.ts` at the logger initialization inside `launchStreamableHTTPServer`, avoid throwing when `configureLogger()` hasn’t been called. Wrap `getLogger()` in a try/catch and fall back to `console` so existing callers don’t crash. Apply the suggested diff exactly.


const logger = getLogger();

if (typeof address === 'string') {
console.error(`MCP Server running on streamable HTTP at ${address}`);
logger.info(`MCP Server running on streamable HTTP at ${address}`);
} else if (address !== null) {
console.error(`MCP Server running on streamable HTTP on port ${address.port}`);
logger.info(`MCP Server running on streamable HTTP on port ${address.port}`);
} else {
console.error(`MCP Server running on streamable HTTP on port ${port}`);
logger.info(`MCP Server running on streamable HTTP on port ${port}`);
}
};
Loading