From c85924dccb934adba75421f757b5c2c226984e00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:25:32 +0000 Subject: [PATCH 01/11] feat(api): api update --- .stats.yml | 4 ++-- src/resources/memories.ts | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index d92f0726..611d4875 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-be19b15cbcf156f621134060e45ab8129def46ceb32d075f44bc2229b7927eb2.yml -openapi_spec_hash: d91cba474f423492510b46439da6a3d7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-c66ab595ac173e641115976a95f5dfda67f7ff4816b3e75625db6ad52ca14876.yml +openapi_spec_hash: f7c3ab607029cd55c66ad0fb01e0c3c2 config_hash: 983708fc30c86269c2149a960d0bfec1 diff --git a/src/resources/memories.ts b/src/resources/memories.ts index a39c85b4..c8da63d7 100644 --- a/src/resources/memories.ts +++ b/src/resources/memories.ts @@ -329,8 +329,8 @@ export interface MemoryUpdateParams { | 'web_crawler'; /** - * Body param: The collection to move the document to. Set to null to remove the - * collection. + * @deprecated Body param: The collection to move the document to — deprecated, set + * the collection using metadata instead. */ collection?: string | unknown | null; @@ -408,7 +408,8 @@ export interface MemoryAddParams { text: string; /** - * The collection to add the document to for easier retrieval. + * @deprecated The collection to add the document to — deprecated, set the + * collection using metadata instead. */ collection?: string | null; @@ -453,7 +454,8 @@ export namespace MemoryAddBulkParams { text: string; /** - * The collection to add the document to for easier retrieval. + * @deprecated The collection to add the document to — deprecated, set the + * collection using metadata instead. */ collection?: string | null; @@ -831,7 +833,8 @@ export interface MemoryUploadParams { file: Uploadable; /** - * The collection to add the document to. + * @deprecated The collection to add the document to — deprecated, set the + * collection using metadata instead. */ collection?: string | null; From ccacaaccdae08b289609ed69dd272f952007e3e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:24:05 +0000 Subject: [PATCH 02/11] chore(internal): cache fetch instruction calls in MCP server --- packages/mcp-server/src/instructions.ts | 74 +++++++++++++++++++++++++ packages/mcp-server/src/server.ts | 37 +------------ 2 files changed, 75 insertions(+), 36 deletions(-) create mode 100644 packages/mcp-server/src/instructions.ts diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 00000000..42ddc65a --- /dev/null +++ b/packages/mcp-server/src/instructions.ts @@ -0,0 +1,74 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { readEnv } from './util'; + +const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes + +interface InstructionsCacheEntry { + fetchedInstructions: string; + fetchedAt: number; +} + +const instructionsCache = new Map(); + +// 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 { + 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: Date.now() }); + return fetchedInstructions; +} + +async function fetchLatestInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/hyperspell', + { + method: 'GET', + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + console.warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = ` + This is the hyperspell MCP server. You will use Code Mode to help the user perform + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! + `; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + instructions = ` + If needed, you can get the current time by executing Date.now(). + + ${instructions} + `; + + return instructions; +} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index b592d58e..22e24391 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,47 +11,12 @@ import { ClientOptions } from 'hyperspell'; import Hyperspell from 'hyperspell'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; +import { getInstructions } from './instructions'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; import { readEnv } from './util'; -async function getInstructions(stainlessApiKey: string | undefined): Promise { - // Setting the stainless API key is optional, but may be required - // to authenticate requests to the Stainless API. - const response = await fetch( - readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/hyperspell', - { - method: 'GET', - headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, - }, - ); - - let instructions: string | undefined; - if (!response.ok) { - console.warn( - 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', - ); - - instructions = ` - This is the hyperspell MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! - `; - } - - instructions ??= ((await response.json()) as { instructions: string }).instructions; - instructions = ` - The current time in Unix timestamps is ${Date.now()}. - - ${instructions} - `; - - return instructions; -} - export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { From 44dc1e76ae1637fa4e889409f6755cde9ec41d8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:25:41 +0000 Subject: [PATCH 03/11] feat(api): api update --- .stats.yml | 4 ++-- src/resources/memories.ts | 18 ------------------ tests/api-resources/memories.test.ts | 27 ++++++--------------------- 3 files changed, 8 insertions(+), 41 deletions(-) diff --git a/.stats.yml b/.stats.yml index 611d4875..782ef1fc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-c66ab595ac173e641115976a95f5dfda67f7ff4816b3e75625db6ad52ca14876.yml -openapi_spec_hash: f7c3ab607029cd55c66ad0fb01e0c3c2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-36cb6e2474f3fe09749b7a2f24409d48c8db332d624fa7eeb1ee6b6135774133.yml +openapi_spec_hash: 339a1b55d6b1a55213d16bf336045d0d config_hash: 983708fc30c86269c2149a960d0bfec1 diff --git a/src/resources/memories.ts b/src/resources/memories.ts index c8da63d7..0950ab75 100644 --- a/src/resources/memories.ts +++ b/src/resources/memories.ts @@ -620,8 +620,6 @@ export namespace MemorySearchParams { * Search options for Box */ export interface Box { - collection?: string | null; - /** * Weight of results from this source. A weight greater than 1.0 means more results * from this source will be returned, a weight less than 1.0 means fewer results @@ -642,8 +640,6 @@ export namespace MemorySearchParams { */ calendar_id?: string | null; - collection?: string | null; - /** * Weight of results from this source. A weight greater than 1.0 means more results * from this source will be returned, a weight less than 1.0 means fewer results @@ -657,8 +653,6 @@ export namespace MemorySearchParams { * Search options for Google Drive */ export interface GoogleDrive { - collection?: string | null; - /** * Weight of results from this source. A weight greater than 1.0 means more results * from this source will be returned, a weight less than 1.0 means fewer results @@ -672,8 +666,6 @@ export namespace MemorySearchParams { * Search options for Gmail */ export interface GoogleMail { - collection?: string | null; - /** * List of label IDs to filter messages (e.g., ['INBOX', 'SENT', 'DRAFT']). * Multiple labels are combined with OR logic - messages matching ANY specified @@ -695,8 +687,6 @@ export namespace MemorySearchParams { * Search options for Notion */ export interface Notion { - collection?: string | null; - /** * List of Notion page IDs to search. If not provided, all pages in the workspace * will be searched. @@ -716,8 +706,6 @@ export namespace MemorySearchParams { * Search options for Reddit */ export interface Reddit { - collection?: string | null; - /** * The time period to search. Defaults to 'month'. */ @@ -752,8 +740,6 @@ export namespace MemorySearchParams { */ channels?: Array; - collection?: string | null; - /** * If set, pass 'exclude_archived' to Slack. If None, omit the param. */ @@ -788,8 +774,6 @@ export namespace MemorySearchParams { * Search options for vault */ export interface Vault { - collection?: string | null; - /** * Weight of results from this source. A weight greater than 1.0 means more results * from this source will be returned, a weight less than 1.0 means fewer results @@ -803,8 +787,6 @@ export namespace MemorySearchParams { * Search options for Web Crawler */ export interface WebCrawler { - collection?: string | null; - /** * Maximum depth to crawl from the starting URL */ diff --git a/tests/api-resources/memories.test.ts b/tests/api-resources/memories.test.ts index 8af38cd4..59824d2f 100644 --- a/tests/api-resources/memories.test.ts +++ b/tests/api-resources/memories.test.ts @@ -160,27 +160,14 @@ describe('resource memories', () => { after: '2019-12-27T18:11:19.117Z', answer_model: 'llama-3.1', before: '2019-12-27T18:11:19.117Z', - box: { collection: 'collection', weight: 0 }, + box: { weight: 0 }, filter: { foo: 'bar' }, - google_calendar: { - calendar_id: 'calendar_id', - collection: 'collection', - weight: 0, - }, - google_drive: { collection: 'collection', weight: 0 }, - google_mail: { - collection: 'collection', - label_ids: ['string'], - weight: 0, - }, + google_calendar: { calendar_id: 'calendar_id', weight: 0 }, + google_drive: { weight: 0 }, + google_mail: { label_ids: ['string'], weight: 0 }, max_results: 0, - notion: { - collection: 'collection', - notion_page_ids: ['string'], - weight: 0, - }, + notion: { notion_page_ids: ['string'], weight: 0 }, reddit: { - collection: 'collection', period: 'hour', sort: 'relevance', subreddit: 'subreddit', @@ -188,16 +175,14 @@ describe('resource memories', () => { }, slack: { channels: ['string'], - collection: 'collection', exclude_archived: true, include_dms: true, include_group_dms: true, include_private: true, weight: 0, }, - vault: { collection: 'collection', weight: 0 }, + vault: { weight: 0 }, web_crawler: { - collection: 'collection', max_depth: 0, url: 'url', weight: 0, From ff10988f658c9ba9fe926f3ae5b9ecc2b6f1908e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:03:51 +0000 Subject: [PATCH 04/11] fix(mcp): initialize SDK lazily to avoid failing the connection on init errors --- packages/mcp-server/src/http.ts | 31 +++++---------- packages/mcp-server/src/index.ts | 2 +- packages/mcp-server/src/server.ts | 66 ++++++++++++++++++++++++------- 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index e5d2f463..9a539974 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -24,28 +24,17 @@ const newServer = async ({ const stainlessApiKey = getStainlessApiKey(req, mcpOptions); const server = await newMcpServer(stainlessApiKey); - try { - const authOptions = parseClientAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); - await initMcpServer({ - server: server, - mcpOptions: mcpOptions, - clientOptions: { - ...clientOptions, - ...authOptions, - }, - stainlessApiKey: stainlessApiKey, - }); - } catch (error) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Unauthorized: ${error instanceof Error ? error.message : error}`, - }, - }); - return null; - } + await initMcpServer({ + server: server, + mcpOptions: mcpOptions, + clientOptions: { + ...clientOptions, + ...authOptions, + }, + stainlessApiKey: stainlessApiKey, + }); return server; }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 003a7655..654d25cf 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -24,7 +24,7 @@ async function main() { await launchStreamableHTTPServer({ mcpOptions: options, debug: options.debug, - port: options.port ?? options.socket, + port: options.socket ?? options.port, }); break; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 22e24391..1a67571c 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -56,15 +56,33 @@ export async function initMcpServer(params: { error: logAtLevel('error'), }; - let client = new Hyperspell({ - ...{ userID: readEnv('HYPERSPELL_USER_ID') }, - logger, - ...params.clientOptions, - defaultHeaders: { - ...params.clientOptions?.defaultHeaders, - 'X-Stainless-MCP': 'true', - }, - }); + let _client: Hyperspell | undefined; + let _clientError: Error | undefined; + let _logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off' | undefined; + + const getClient = (): Hyperspell => { + if (_clientError) throw _clientError; + if (!_client) { + try { + _client = new Hyperspell({ + ...{ userID: readEnv('HYPERSPELL_USER_ID') }, + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); + if (_logLevel) { + _client = _client.withOptions({ logLevel: _logLevel }); + } + } catch (e) { + _clientError = e instanceof Error ? e : new Error(String(e)); + throw _clientError; + } + } + return _client; + }; const providedTools = selectTools(params.mcpOptions); const toolMap = Object.fromEntries(providedTools.map((mcpTool) => [mcpTool.tool.name, mcpTool])); @@ -82,6 +100,21 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } + let client: Hyperspell; + try { + client = getClient(); + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to initialize client: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + return executeHandler({ handler: mcpTool.handler, reqContext: { @@ -94,24 +127,29 @@ export async function initMcpServer(params: { server.setRequestHandler(SetLevelRequestSchema, async (request) => { const { level } = request.params; + let logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off'; switch (level) { case 'debug': - client = client.withOptions({ logLevel: 'debug' }); + logLevel = 'debug'; break; case 'info': - client = client.withOptions({ logLevel: 'info' }); + logLevel = 'info'; break; case 'notice': case 'warning': - client = client.withOptions({ logLevel: 'warn' }); + logLevel = 'warn'; break; case 'error': - client = client.withOptions({ logLevel: 'error' }); + logLevel = 'error'; break; default: - client = client.withOptions({ logLevel: 'off' }); + logLevel = 'off'; break; } + _logLevel = logLevel; + if (_client) { + _client = _client.withOptions({ logLevel }); + } return {}; }); } From 0417d08178a0efe5b858d15c3a2c6763964e8bbe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:08:02 +0000 Subject: [PATCH 05/11] chore: update mock server docs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4108c69b..fc2f9494 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ $ pnpm link -—global hyperspell Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. ```sh -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh From 54f2bffc32d97851ded586a4a0840e5b74bddd54 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:18:12 +0000 Subject: [PATCH 06/11] chore(mcp): correctly update version in sync with sdk --- release-please-config.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release-please-config.json b/release-please-config.json index b1909804..9b042792 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -68,6 +68,11 @@ "type": "json", "path": "packages/mcp-server/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/mcp-server/manifest.json", + "jsonpath": "$.version" } ] } From 8904a4f7743ce268f189871f0ca4bb49672ad0fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:27:46 +0000 Subject: [PATCH 07/11] fix(docs/contributing): correct pnpm link command --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc2f9494..0d767f59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ $ yarn link hyperspell # With pnpm $ pnpm link --global $ cd ../my-package -$ pnpm link -—global hyperspell +$ pnpm link --global hyperspell ``` ## Running tests From f6fdb27d81b75495ef4e2399699c9153dc056490 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:37:06 +0000 Subject: [PATCH 08/11] chore(internal): upgrade @modelcontextprotocol/sdk and hono --- packages/mcp-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 0e369567..3e6bdbed 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -32,7 +32,7 @@ "dependencies": { "hyperspell": "file:../../dist/", "@cloudflare/cabidela": "^0.2.4", - "@modelcontextprotocol/sdk": "^1.25.2", + "@modelcontextprotocol/sdk": "^1.26.0", "@valtown/deno-http-worker": "^0.0.21", "cookie-parser": "^1.4.6", "cors": "^2.8.5", From 6ae4a00a4d3f0eb59041c2e1b49046bc1a55ed40 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:18:57 +0000 Subject: [PATCH 09/11] chore(internal): make MCP code execution location configurable via a flag --- packages/mcp-server/Dockerfile | 14 +- packages/mcp-server/src/code-tool-paths.cts | 3 + packages/mcp-server/src/code-tool-types.ts | 1 + packages/mcp-server/src/code-tool-worker.ts | 293 ++++++++++++++++++ packages/mcp-server/src/code-tool.ts | 313 +++++++++++++++++--- packages/mcp-server/src/options.ts | 12 + packages/mcp-server/src/server.ts | 1 + packages/mcp-server/tests/options.test.ts | 20 +- 8 files changed, 590 insertions(+), 67 deletions(-) create mode 100644 packages/mcp-server/src/code-tool-paths.cts create mode 100644 packages/mcp-server/src/code-tool-worker.ts diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 800712db..2e6573e1 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -37,9 +37,21 @@ COPY . . RUN yarn install --frozen-lockfile && \ yarn build -# Production stage +FROM denoland/deno:bin-2.6.10 AS deno_installer +FROM gcr.io/distroless/cc@sha256:66d87e170bc2c5e2b8cf853501141c3c55b4e502b8677595c57534df54a68cc5 AS cc + FROM node:24-alpine +# Install deno +COPY --from=deno_installer /deno /usr/local/bin/deno + +# Add in shared libraries needed by Deno +COPY --from=cc --chown=root:root --chmod=755 /lib/*-linux-gnu/* /usr/local/lib/ +COPY --from=cc --chown=root:root --chmod=755 /lib/ld-linux-* /lib/ + +RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/ +ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib + # Add non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts new file mode 100644 index 00000000..15ce7f55 --- /dev/null +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const workerPath = require.resolve('./code-tool-worker.mjs'); diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts index 1598dcd9..4c069221 100644 --- a/packages/mcp-server/src/code-tool-types.ts +++ b/packages/mcp-server/src/code-tool-types.ts @@ -8,6 +8,7 @@ export type WorkerInput = { client_opts: ClientOptions; intent?: string | undefined; }; + export type WorkerOutput = { is_error: boolean; result: unknown | null; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts new file mode 100644 index 00000000..067906ee --- /dev/null +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -0,0 +1,293 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import path from 'node:path'; +import util from 'node:util'; +import Fuse from 'fuse.js'; +import ts from 'typescript'; +import { WorkerOutput } from './code-tool-types'; +import { Hyperspell, ClientOptions } from 'hyperspell'; + +function getRunFunctionSource(code: string): { + type: 'declaration' | 'expression'; + client: string | undefined; + code: string; +} | null { + const sourceFile = ts.createSourceFile('code.ts', code, ts.ScriptTarget.Latest, true); + const printer = ts.createPrinter(); + + for (const statement of sourceFile.statements) { + // Check for top-level function declarations + if (ts.isFunctionDeclaration(statement)) { + if (statement.name?.text === 'run') { + return { + type: 'declaration', + client: statement.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, statement.body!, sourceFile), + }; + } + } + + // Check for variable declarations: const run = () => {} or const run = function() {} + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'run' && + // Check if it's initialized with a function + declaration.initializer && + (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer)) + ) { + return { + type: 'expression', + client: declaration.initializer.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, declaration.initializer, sourceFile), + }; + } + } + } + } + + return null; +} + +function getTSDiagnostics(code: string): string[] { + const functionSource = getRunFunctionSource(code)!; + const codeWithImport = [ + 'import { Hyperspell } from "hyperspell";', + functionSource.type === 'declaration' ? + `async function run(${functionSource.client}: Hyperspell)` + : `const run: (${functionSource.client}: Hyperspell) => Promise =`, + functionSource.code, + ].join('\n'); + const sourcePath = path.resolve('code.ts'); + const ast = ts.createSourceFile(sourcePath, codeWithImport, ts.ScriptTarget.Latest, true); + const options = ts.getDefaultCompilerOptions(); + options.target = ts.ScriptTarget.Latest; + options.module = ts.ModuleKind.NodeNext; + options.moduleResolution = ts.ModuleResolutionKind.NodeNext; + const host = ts.createCompilerHost(options, true); + const newHost: typeof host = { + ...host, + getSourceFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return ast; + } + return host.getSourceFile(...args); + }, + readFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return codeWithImport; + } + return host.readFile(...args); + }, + fileExists: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return true; + } + return host.fileExists(...args); + }, + }; + const program = ts.createProgram({ + options, + rootNames: [sourcePath], + host: newHost, + }); + const diagnostics = ts.getPreEmitDiagnostics(program, ast); + return diagnostics.map((d) => { + const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + if (!d.file || !d.start) return `- ${message}`; + const { line: lineNumber } = ts.getLineAndCharacterOfPosition(d.file, d.start); + const line = codeWithImport.split('\n').at(lineNumber)?.trim(); + return line ? `- ${message}\n ${line}` : `- ${message}`; + }); +} + +const fuse = new Fuse( + [ + 'client.connections.list', + 'client.connections.revoke', + 'client.integrations.connect', + 'client.integrations.list', + 'client.integrations.googleCalendar.list', + 'client.integrations.webCrawler.index', + 'client.integrations.slack.list', + 'client.memories.add', + 'client.memories.addBulk', + 'client.memories.delete', + 'client.memories.get', + 'client.memories.list', + 'client.memories.search', + 'client.memories.status', + 'client.memories.update', + 'client.memories.upload', + 'client.evaluate.getQuery', + 'client.evaluate.scoreHighlight', + 'client.evaluate.scoreQuery', + 'client.vaults.list', + 'client.auth.deleteUser', + 'client.auth.me', + 'client.auth.userToken', + ], + { threshold: 1, shouldSort: true }, +); + +function getMethodSuggestions(fullyQualifiedMethodName: string): string[] { + return fuse + .search(fullyQualifiedMethodName) + .map(({ item }) => item) + .slice(0, 5); +} + +const proxyToObj = new WeakMap(); +const objToProxy = new WeakMap(); + +type ClientProxyConfig = { + path: string[]; + isBelievedBad?: boolean; +}; + +function makeSdkProxy(obj: T, { path, isBelievedBad = false }: ClientProxyConfig): T { + let proxy: T = objToProxy.get(obj); + + if (!proxy) { + proxy = new Proxy(obj, { + get(target, prop, receiver) { + const propPath = [...path, String(prop)]; + const value = Reflect.get(target, prop, receiver); + + if (isBelievedBad || (!(prop in target) && value === undefined)) { + // If we're accessing a path that doesn't exist, it will probably eventually error. + // Let's proxy it and mark it bad so that we can control the error message. + // We proxy an empty class so that an invocation or construction attempt is possible. + return makeSdkProxy(class {}, { path: propPath, isBelievedBad: true }); + } + + if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + return makeSdkProxy(value, { path: propPath, isBelievedBad }); + } + + return value; + }, + + apply(target, thisArg, args) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a function. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.apply(target, proxyToObj.get(thisArg) ?? thisArg, args); + }, + + construct(target, args, newTarget) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a constructor. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.construct(target, args, newTarget); + }, + }); + + objToProxy.set(obj, proxy); + proxyToObj.set(proxy, obj); + } + + return proxy; +} + +function parseError(code: string, error: unknown): string | undefined { + if (!(error instanceof Error)) return; + const message = error.name ? `${error.name}: ${error.message}` : error.message; + try { + // Deno uses V8; the first ":LINE:COLUMN" is the top of stack. + const lineNumber = error.stack?.match(/:([0-9]+):[0-9]+/)?.[1]; + // -1 for the zero-based indexing + const line = + lineNumber && + code + .split('\n') + .at(parseInt(lineNumber, 10) - 1) + ?.trim(); + return line ? `${message}\n at line ${lineNumber}\n ${line}` : message; + } catch { + return message; + } +} + +const fetch = async (req: Request): Promise => { + const { opts, code } = (await req.json()) as { opts: ClientOptions; code: string }; + + const runFunctionSource = code ? getRunFunctionSource(code) : null; + if (!runFunctionSource) { + const message = + code ? + 'The code is missing a top-level `run` function.' + : 'The code argument is missing. Provide one containing a top-level `run` function.'; + return Response.json( + { + is_error: true, + result: `${message} Write code within this template:\n\n\`\`\`\nasync function run(client) {\n // Fill this out\n}\n\`\`\``, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const diagnostics = getTSDiagnostics(code); + if (diagnostics.length > 0) { + return Response.json( + { + is_error: true, + result: `The code contains TypeScript diagnostics:\n${diagnostics.join('\n')}`, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const client = new Hyperspell({ + ...opts, + }); + + const log_lines: string[] = []; + const err_lines: string[] = []; + const console = { + log: (...args: unknown[]) => { + log_lines.push(util.format(...args)); + }, + error: (...args: unknown[]) => { + err_lines.push(util.format(...args)); + }, + }; + try { + let run_ = async (client: any) => {}; + eval(`${code}\nrun_ = run;`); + const result = await run_(makeSdkProxy(client, { path: ['client'] })); + return Response.json({ + is_error: false, + result, + log_lines, + err_lines, + } satisfies WorkerOutput); + } catch (e) { + return Response.json( + { + is_error: true, + result: parseError(code, e), + log_lines, + err_lines, + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } +}; + +export default { fetch }; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 4036b38a..514da218 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,6 +1,12 @@ // 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, McpTool, Metadata, @@ -12,6 +18,8 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; +import { McpCodeExecutionMode } from './options'; +import { ClientOptions } from 'hyperspell'; const prompt = `Runs JavaScript code to interact with the Hyperspell API. @@ -40,9 +48,19 @@ Variables will not persist between calls, so make sure to return or log any data * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then * a generic endpoint that can be used to invoke any endpoint with the provided arguments. * - * @param endpoints - The endpoints to include in the list. + * @param blockedMethods - The methods to block for code execution. Blocking is done by simple string + * matching, so it is not secure against obfuscation. For stronger security, block in the downstream API + * with limited API keys. + * @param codeExecutionMode - Whether to execute code in a local Deno environment or in a remote + * sandbox environment hosted by Stainless. */ -export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ + blockedMethods, + codeExecutionMode, +}: { + blockedMethods: SdkMethod[] | undefined; + codeExecutionMode: McpCodeExecutionMode; +}): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -62,6 +80,7 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und required: ['code'], }, }; + const handler = async ({ reqContext, args, @@ -70,9 +89,6 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und args: any; }): Promise => { const code = args.code as string; - const intent = args.intent as string | undefined; - const client = reqContext.client; - // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If @@ -89,51 +105,254 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und } } - const codeModeEndpoint = - readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; - - // 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', - 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', - ), - HYPERSPELL_BASE_URL: readEnv('HYPERSPELL_BASE_URL') ?? client.baseURL ?? undefined, - }), - }, - body: JSON.stringify({ - project_name: 'hyperspell', - code, - intent, - client_opts: { userID: readEnv('HYPERSPELL_USER_ID') }, - } satisfies WorkerInput), - }); + if (codeExecutionMode === 'local') { + return await localDenoHandler({ reqContext, args }); + } else { + return await remoteStainlessHandler({ reqContext, args }); + } + }; + + return { metadata, tool, handler }; +} + +const remoteStainlessHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: any; +}): Promise => { + const code = args.code as string; + const intent = args.intent as string | undefined; + const client = reqContext.client; - if (!res.ok) { - throw new Error( - `${res.status}: ${ - res.statusText - } error when trying to contact Code Tool server. Details: ${await res.text()}`, + const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + + // 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', + 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', + ), + HYPERSPELL_BASE_URL: readEnv('HYPERSPELL_BASE_URL') ?? client.baseURL ?? undefined, + }), + }, + body: JSON.stringify({ + project_name: 'hyperspell', + code, + intent, + client_opts: { userID: readEnv('HYPERSPELL_USER_ID') }, + } satisfies WorkerInput), + }); + + if (!res.ok) { + throw new Error( + `${res.status}: ${ + res.statusText + } error when trying to contact Code Tool server. Details: ${await res.text()}`, + ); + } + + const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; + const hasLogs = log_lines.length > 0 || err_lines.length > 0; + const output = { + result, + ...(log_lines.length > 0 && { log_lines }), + ...(err_lines.length > 0 && { err_lines }), + }; + if (is_error) { + return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); + } + return asTextContentResult(output); +}; + +const localDenoHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: unknown; +}): Promise => { + const client = reqContext.client; + const baseURLHostname = new URL(client.baseURL).hostname; + const { code } = args as { code: string }; + + let denoPath: string; + + const packageRoot = path.resolve(path.dirname(workerPath), '..'); + const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules'); + + // Check if deno is in PATH + const { execSync } = await import('node:child_process'); + try { + execSync('command -v deno', { stdio: 'ignore' }); + denoPath = 'deno'; + } catch { + try { + // Use deno binary in node_modules if it's found + const denoNodeModulesPath = path.resolve(packageNodeModulesPath, 'deno', 'bin.cjs'); + await fs.promises.access(denoNodeModulesPath, fs.constants.X_OK); + denoPath = denoNodeModulesPath; + } catch { + return asErrorResult( + 'Deno is required for code execution but was not found. ' + + 'Install it from https://deno.land or run: npm install deno', ); } + } + + const allowReadPaths = [ + 'code-tool-worker.mjs', + `${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, + packageRoot, + ]; - const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; - const hasLogs = log_lines.length > 0 || err_lines.length > 0; - const output = { - result, - ...(log_lines.length > 0 && { log_lines }), - ...(err_lines.length > 0 && { err_lines }), - }; - if (is_error) { - return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); + // Follow symlinks in node_modules to allow read access to workspace-linked packages + try { + const sdkPkgName = 'hyperspell'; + const sdkDir = path.resolve(packageNodeModulesPath, sdkPkgName); + const realSdkDir = fs.realpathSync(sdkDir); + if (realSdkDir !== sdkDir) { + allowReadPaths.push(realSdkDir); } - return asTextContentResult(output); - }; + } catch { + // Ignore if symlink resolution fails + } - return { metadata, tool, handler }; -} + const allowRead = allowReadPaths.join(','); + + const worker = await newDenoHTTPWorker(url.pathToFileURL(workerPath), { + denoExecutable: denoPath, + runFlags: [ + `--node-modules-dir=manual`, + `--allow-read=${allowRead}`, + `--allow-net=${baseURLHostname}`, + // Allow environment variables because instantiating the client will try to read from them, + // even though they are not set. + '--allow-env', + ], + printOutput: true, + spawnOptions: { + cwd: path.dirname(workerPath), + }, + }); + + try { + const resp = await new Promise((resolve, reject) => { + worker.addEventListener('exit', (exitCode) => { + reject(new Error(`Worker exited with code ${exitCode}`)); + }); + + const opts: ClientOptions = { + baseURL: client.baseURL, + apiKey: client.apiKey, + userID: client.userID, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }; + + const req = worker.request( + 'http://localhost', + { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }, + (resp) => { + const body: Uint8Array[] = []; + resp.on('error', (err) => { + reject(err); + }); + resp.on('data', (chunk) => { + body.push(chunk); + }); + resp.on('end', () => { + resolve( + new Response(Buffer.concat(body).toString(), { + status: resp.statusCode ?? 200, + headers: resp.headers as any, + }), + ); + }); + }, + ); + + const body = JSON.stringify({ + opts, + code, + }); + + req.write(body, (err) => { + if (err != null) { + reject(err); + } + }); + + req.end(); + }); + + if (resp.status === 200) { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const returnOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [returnOutput, logOutput, errOutput].filter((block) => block !== null), + }; + } else { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const messageOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [messageOutput, logOutput, errOutput].filter((block) => block !== null), + isError: true, + }; + } + } finally { + worker.terminate(); + } +}; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 32a88713..9e9d15cd 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -19,8 +19,11 @@ export type McpOptions = { codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; + codeExecutionMode: McpCodeExecutionMode; }; +export type McpCodeExecutionMode = 'stainless-sandbox' | 'local'; + export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) .option('code-allow-http-gets', { @@ -40,6 +43,13 @@ export function parseCLIOptions(): CLIOptions { description: 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', }) + .option('code-execution-mode', { + type: 'string', + choices: ['stainless-sandbox', 'local'], + default: 'stainless-sandbox', + description: + "Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.", + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) .option('no-tools', { type: 'string', @@ -93,6 +103,7 @@ export function parseCLIOptions(): CLIOptions { codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, + codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode, transport, port: argv.port, socket: argv.socket, @@ -124,6 +135,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M : defaultOptions.includeDocsTools; return { + codeExecutionMode: defaultOptions.codeExecutionMode, ...(docsTools !== undefined && { includeDocsTools: docsTools }), }; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 1a67571c..68704053 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -161,6 +161,7 @@ export function selectTools(options?: McpOptions): McpTool[] { const includedTools = [ codeTool({ blockedMethods: blockedMethodsForCodeTool(options), + codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox', }), ]; if (options?.includeDocsTools ?? true) { diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 7a2d5114..17306295 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -1,4 +1,4 @@ -import { parseCLIOptions, parseQueryOptions } from '../src/options'; +import { parseCLIOptions } from '../src/options'; // Mock process.argv const mockArgv = (args: string[]) => { @@ -30,21 +30,3 @@ describe('parseCLIOptions', () => { cleanup(); }); }); - -describe('parseQueryOptions', () => { - const defaultOptions = {}; - - it('default parsing should be empty', () => { - const query = ''; - const result = parseQueryOptions(defaultOptions, query); - - expect(result).toEqual({}); - }); - - it('should handle invalid query string gracefully', () => { - const query = 'invalid=value&tools=invalid-operation'; - - // Should throw due to Zod validation for invalid tools - expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); - }); -}); From 5a6e5c74e6ddcc376450e94b0f5bdafe073ba801 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:25:38 +0000 Subject: [PATCH 10/11] feat(api): api update --- .stats.yml | 4 ++-- src/resources/auth.ts | 2 ++ src/resources/connections.ts | 3 ++- src/resources/integrations/integrations.ts | 3 ++- src/resources/integrations/web-crawler.ts | 3 ++- src/resources/memories.ts | 23 +++++++++++++++------- src/resources/shared.ts | 3 ++- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 782ef1fc..a56a7796 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-36cb6e2474f3fe09749b7a2f24409d48c8db332d624fa7eeb1ee6b6135774133.yml -openapi_spec_hash: 339a1b55d6b1a55213d16bf336045d0d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-28ef76911100f5f3b0934f35b93da890f59db35d8ad889bfb11fe67d5879d07e.yml +openapi_spec_hash: 5c83e03e89be87832239a3eb04eccff3 config_hash: 983708fc30c86269c2149a960d0bfec1 diff --git a/src/resources/auth.ts b/src/resources/auth.ts index d91efc02..e656d32e 100644 --- a/src/resources/auth.ts +++ b/src/resources/auth.ts @@ -65,6 +65,7 @@ export interface AuthMeResponse { | 'google_drive' | 'vault' | 'web_crawler' + | 'trace' >; /** @@ -81,6 +82,7 @@ export interface AuthMeResponse { | 'google_drive' | 'vault' | 'web_crawler' + | 'trace' >; /** diff --git a/src/resources/connections.ts b/src/resources/connections.ts index 658ad531..381f4de2 100644 --- a/src/resources/connections.ts +++ b/src/resources/connections.ts @@ -56,7 +56,8 @@ export namespace ConnectionListResponse { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; } } diff --git a/src/resources/integrations/integrations.ts b/src/resources/integrations/integrations.ts index 582a18d5..9adcecd8 100644 --- a/src/resources/integrations/integrations.ts +++ b/src/resources/integrations/integrations.ts @@ -91,7 +91,8 @@ export namespace IntegrationListResponse { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; } } diff --git a/src/resources/integrations/web-crawler.ts b/src/resources/integrations/web-crawler.ts index b4d91963..94a1974a 100644 --- a/src/resources/integrations/web-crawler.ts +++ b/src/resources/integrations/web-crawler.ts @@ -33,7 +33,8 @@ export interface WebCrawlerIndexResponse { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; status: 'pending' | 'processing' | 'completed' | 'failed'; } diff --git a/src/resources/memories.ts b/src/resources/memories.ts index 0950ab75..6b4dea19 100644 --- a/src/resources/memories.ts +++ b/src/resources/memories.ts @@ -200,7 +200,8 @@ export interface Memory { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; /** * The type of document (e.g. Document, Website, Email) @@ -237,7 +238,8 @@ export interface MemoryStatus { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; status: 'pending' | 'processing' | 'completed' | 'failed'; } @@ -255,7 +257,8 @@ export interface MemoryListResponse { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; metadata?: Shared.Metadata; @@ -284,7 +287,8 @@ export interface MemoryDeleteResponse { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; success: boolean; } @@ -326,7 +330,8 @@ export interface MemoryUpdateParams { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; /** * @deprecated Body param: The collection to move the document to — deprecated, set @@ -379,6 +384,7 @@ export interface MemoryListParams extends CursorPageParams { | 'google_drive' | 'vault' | 'web_crawler' + | 'trace' | null; /** @@ -398,7 +404,8 @@ export interface MemoryDeleteParams { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; } export interface MemoryAddParams { @@ -497,7 +504,8 @@ export interface MemoryGetParams { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; } export interface MemorySearchParams { @@ -535,6 +543,7 @@ export interface MemorySearchParams { | 'google_drive' | 'vault' | 'web_crawler' + | 'trace' >; } diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 0b317e8d..6fd9dae2 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -66,7 +66,8 @@ export namespace QueryResult { | 'dropbox' | 'google_drive' | 'vault' - | 'web_crawler'; + | 'web_crawler' + | 'trace'; metadata?: Shared.Metadata; From 42293473fc72a14641ed7398bf2ddbdeae3cac12 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:25:57 +0000 Subject: [PATCH 11/11] release: 0.32.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 25 +++++++++++++++++++++++++ package.json | 2 +- packages/mcp-server/manifest.json | 10 +++++++--- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8e3d9554..47582228 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.31.0" + ".": "0.32.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4c62c0..fe2502cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.32.0 (2026-02-26) + +Full Changelog: [v0.31.0...v0.32.0](https://github.com/hyperspell/node-sdk/compare/v0.31.0...v0.32.0) + +### Features + +* **api:** api update ([5a6e5c7](https://github.com/hyperspell/node-sdk/commit/5a6e5c74e6ddcc376450e94b0f5bdafe073ba801)) +* **api:** api update ([44dc1e7](https://github.com/hyperspell/node-sdk/commit/44dc1e76ae1637fa4e889409f6755cde9ec41d8f)) +* **api:** api update ([c85924d](https://github.com/hyperspell/node-sdk/commit/c85924dccb934adba75421f757b5c2c226984e00)) + + +### Bug Fixes + +* **docs/contributing:** correct pnpm link command ([8904a4f](https://github.com/hyperspell/node-sdk/commit/8904a4f7743ce268f189871f0ca4bb49672ad0fd)) +* **mcp:** initialize SDK lazily to avoid failing the connection on init errors ([ff10988](https://github.com/hyperspell/node-sdk/commit/ff10988f658c9ba9fe926f3ae5b9ecc2b6f1908e)) + + +### Chores + +* **internal:** cache fetch instruction calls in MCP server ([ccacaac](https://github.com/hyperspell/node-sdk/commit/ccacaaccdae08b289609ed69dd272f952007e3e2)) +* **internal:** make MCP code execution location configurable via a flag ([6ae4a00](https://github.com/hyperspell/node-sdk/commit/6ae4a00a4d3f0eb59041c2e1b49046bc1a55ed40)) +* **internal:** upgrade @modelcontextprotocol/sdk and hono ([f6fdb27](https://github.com/hyperspell/node-sdk/commit/f6fdb27d81b75495ef4e2399699c9153dc056490)) +* **mcp:** correctly update version in sync with sdk ([54f2bff](https://github.com/hyperspell/node-sdk/commit/54f2bffc32d97851ded586a4a0840e5b74bddd54)) +* update mock server docs ([0417d08](https://github.com/hyperspell/node-sdk/commit/0417d08178a0efe5b858d15c3a2c6763964e8bbe)) + ## 0.31.0 (2026-02-18) Full Changelog: [v0.30.0...v0.31.0](https://github.com/hyperspell/node-sdk/compare/v0.30.0...v0.31.0) diff --git a/package.json b/package.json index 655d2f79..5fc34580 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperspell", - "version": "0.31.0", + "version": "0.32.0", "description": "The official TypeScript library for the Hyperspell API", "author": "Hyperspell ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 6b52fd89..a10efcdd 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,7 +1,7 @@ { "dxt_version": "0.2", "name": "hyperspell-mcp", - "version": "0.27.0", + "version": "0.32.0", "description": "The official MCP Server for the Hyperspell API", "author": { "name": "Hyperspell", @@ -18,7 +18,9 @@ "entry_point": "index.js", "mcp_config": { "command": "node", - "args": ["${__dirname}/index.js"], + "args": [ + "${__dirname}/index.js" + ], "env": { "HYPERSPELL_API_KEY": "${user_config.HYPERSPELL_API_KEY}", "HYPERSPELL_USER_ID": "${user_config.HYPERSPELL_USER_ID}" @@ -46,5 +48,7 @@ "node": ">=18.0.0" } }, - "keywords": ["api"] + "keywords": [ + "api" + ] } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 3e6bdbed..6004bf72 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "hyperspell-mcp", - "version": "0.31.0", + "version": "0.32.0", "description": "The official MCP Server for the Hyperspell API", "author": "Hyperspell ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 68704053..7e96073e 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -21,7 +21,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'hyperspell_api', - version: '0.31.0', + version: '0.32.0', }, { instructions: await getInstructions(stainlessApiKey), diff --git a/src/version.ts b/src/version.ts index b6314c28..b413d151 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.31.0'; // x-release-please-version +export const VERSION = '0.32.0'; // x-release-please-version