diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8e3d955..4758222 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.31.0" + ".": "0.32.0" } diff --git a/.stats.yml b/.stats.yml index d92f072..782ef1f 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-36cb6e2474f3fe09749b7a2f24409d48c8db332d624fa7eeb1ee6b6135774133.yml +openapi_spec_hash: 339a1b55d6b1a55213d16bf336045d0d config_hash: 983708fc30c86269c2149a960d0bfec1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4c62c..1384906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.32.0 (2026-02-24) + +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 ([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:** 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/CONTRIBUTING.md b/CONTRIBUTING.md index 4108c69..0d767f5 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 @@ -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 diff --git a/package.json b/package.json index 655d2f7..5fc3458 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 6b52fd8..a10efcd 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 0e36956..6004bf7 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", @@ -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", diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index e5d2f46..9a53997 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 003a765..654d25c 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/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 0000000..42ddc65 --- /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 b592d58..fa38d04 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,52 +11,17 @@ 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( { name: 'hyperspell_api', - version: '0.31.0', + version: '0.32.0', }, { instructions: await getInstructions(stainlessApiKey), @@ -91,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])); @@ -117,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: { @@ -129,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 {}; }); } diff --git a/release-please-config.json b/release-please-config.json index b190980..9b04279 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" } ] } diff --git a/src/resources/memories.ts b/src/resources/memories.ts index a39c85b..0950ab7 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; @@ -618,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 @@ -640,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 @@ -655,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 @@ -670,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 @@ -693,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. @@ -714,8 +706,6 @@ export namespace MemorySearchParams { * Search options for Reddit */ export interface Reddit { - collection?: string | null; - /** * The time period to search. Defaults to 'month'. */ @@ -750,8 +740,6 @@ export namespace MemorySearchParams { */ channels?: Array; - collection?: string | null; - /** * If set, pass 'exclude_archived' to Slack. If None, omit the param. */ @@ -786,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 @@ -801,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 */ @@ -831,7 +815,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; diff --git a/src/version.ts b/src/version.ts index b6314c2..b413d15 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 diff --git a/tests/api-resources/memories.test.ts b/tests/api-resources/memories.test.ts index 8af38cd..59824d2 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,