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
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.31.0"
".": "0.32.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-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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ $ yarn link hyperspell
# With pnpm
$ pnpm link --global
$ cd ../my-package
$ pnpm link -global hyperspell
$ pnpm link --global hyperspell
```

## Running tests

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

```sh
$ npx prism mock path/to/your/openapi.yml
$ ./scripts/mock
```

```sh
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.31.0",
"version": "0.32.0",
"description": "The official TypeScript library for the Hyperspell API",
"author": "Hyperspell <hello@hyperspell.com>",
"types": "dist/index.d.ts",
Expand Down
10 changes: 7 additions & 3 deletions 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.27.0",
"version": "0.32.0",
"description": "The official MCP Server for the Hyperspell API",
"author": {
"name": "Hyperspell",
Expand All @@ -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}"
Expand Down Expand Up @@ -46,5 +48,7 @@
"node": ">=18.0.0"
}
},
"keywords": ["api"]
"keywords": [
"api"
]
}
4 changes: 2 additions & 2 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.31.0",
"version": "0.32.0",
"description": "The official MCP Server for the Hyperspell API",
"author": "Hyperspell <hello@hyperspell.com>",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -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",
Expand Down
31 changes: 10 additions & 21 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
74 changes: 74 additions & 0 deletions packages/mcp-server/src/instructions.ts
Original file line number Diff line number Diff line change
@@ -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<string, InstructionsCacheEntry>();

// Periodically evict stale entries so the cache doesn't grow unboundedly.
const _cacheCleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of instructionsCache) {
if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) {
instructionsCache.delete(key);
}
}
}, INSTRUCTIONS_CACHE_TTL_MS);

// Don't keep the process alive just for cleanup.
_cacheCleanupInterval.unref();

export async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
const 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<string> {
// 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;
}
105 changes: 54 additions & 51 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
// 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),
Expand Down Expand Up @@ -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]));
Expand All @@ -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: {
Expand All @@ -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 {};
});
}
Expand Down
Loading