diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbf24437..1cba3029 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node uses: actions/setup-node@v4 @@ -41,7 +41,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node uses: actions/setup-node@v4 @@ -57,7 +57,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/hyperspell-typescript' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -83,7 +83,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node uses: actions/setup-node@v4 diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 44d68bca..7c6f5dd8 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -20,7 +20,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node uses: actions/setup-node@v3 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index d4928c11..f9ea8493 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,10 +12,11 @@ jobs: if: github.repository == 'hyperspell/node-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | bash ./bin/check-release-environment env: NPM_TOKEN: ${{ secrets.HYPERSPELL_NPM_TOKEN || secrets.NPM_TOKEN }} + diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 716d0046..8e3d9554 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.30.0" + ".": "0.31.0" } diff --git a/.stats.yml b/.stats.yml index b7a86017..737339ac 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-a73c73c4848db6cc0b836219f2ace7bc9f6b4611d36f9daa2158f8bd5a7d0864.yml -openapi_spec_hash: 4ef2aeca3ffe2c6e6fbca0770a69c6fb +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-34d6c3c2efa9b9b88d95ff49ff64f45440720b14a046edf3fac953d6eb0d13a1.yml +openapi_spec_hash: 80ce00b9f30af244e0a331c3430ed5e4 config_hash: bd77d0b7029518c697756456d6854f07 diff --git a/CHANGELOG.md b/CHANGELOG.md index f2334470..46aea751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 0.31.0 (2026-02-05) + +Full Changelog: [v0.30.0...v0.31.0](https://github.com/hyperspell/node-sdk/compare/v0.30.0...v0.31.0) + +### Features + +* **api:** api update ([a0df1be](https://github.com/hyperspell/node-sdk/commit/a0df1be8d411efda7d9c06ba663e64a72e308de0)) +* **mcp:** add initial server instructions ([581fb5e](https://github.com/hyperspell/node-sdk/commit/581fb5e5aaf49b32facacc9adc28ca3797dbd117)) + + +### Bug Fixes + +* **client:** avoid memory leak with abort signals ([5b29273](https://github.com/hyperspell/node-sdk/commit/5b2927352218310b6e3a3ec4c950dc4e8d15cbf9)) +* **docs:** fix mcp installation instructions for remote servers ([d4c938b](https://github.com/hyperspell/node-sdk/commit/d4c938b9490184c9de54a36e0635b3b7b24e8aeb)) +* **mcp:** allow falling back for required env variables ([6054c99](https://github.com/hyperspell/node-sdk/commit/6054c99672bb1399b5db94931d20979394ab9f60)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([090b5c1](https://github.com/hyperspell/node-sdk/commit/090b5c1a28992d318a2904cabe73556f61e76edf)) +* **client:** do not parse responses with empty content-length ([b7dde77](https://github.com/hyperspell/node-sdk/commit/b7dde774dee58aaf475d07c08d8792753c09fc85)) +* **client:** restructure abort controller binding ([ecf2c20](https://github.com/hyperspell/node-sdk/commit/ecf2c20a9d7aa7cfb1d5734a14ba75a4f5ebe0e8)) +* **internal:** codegen related update ([1c6bacc](https://github.com/hyperspell/node-sdk/commit/1c6bacc1daa1c93198ea61ec75325cf7dc1e7950)) +* **internal:** refactor flag parsing for MCP servers and add debug flag ([051f042](https://github.com/hyperspell/node-sdk/commit/051f0428d7d30eadf1deb83214757e98e7cb86c0)) +* **internal:** support oauth authorization code flow for MCP servers ([027ce2c](https://github.com/hyperspell/node-sdk/commit/027ce2cd7d3164f49134235a10446ed1e838d17f)) +* **internal:** update `actions/checkout` version ([954ce30](https://github.com/hyperspell/node-sdk/commit/954ce3099c58efa8c5071b6830dad969401a050a)) +* **internal:** update lock file ([e192aa3](https://github.com/hyperspell/node-sdk/commit/e192aa3c9ac16fb38cad2e56f2e46498eb11f8ee)) +* **mcp:** up tsconfig lib version to es2022 ([db052dc](https://github.com/hyperspell/node-sdk/commit/db052dcd1a475b54cb7c302df23bcfaa73c969d0)) + ## 0.30.0 (2026-01-16) Full Changelog: [v0.29.0...v0.30.0](https://github.com/hyperspell/node-sdk/compare/v0.29.0...v0.30.0) diff --git a/MIGRATION.md b/MIGRATION.md index 3b0d7631..f4e786f4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -234,8 +234,8 @@ The `for await` syntax **is not affected**. This still works as-is: ```ts // Automatically fetches more pages as needed. -for await (const memory of client.memories.list()) { - console.log(memory); +for await (const memoryListResponse of client.memories.list()) { + console.log(memoryListResponse); } ``` @@ -257,10 +257,10 @@ Page classes for individual methods are now type aliases: ```ts // Before -export class MemoriesCursorPage extends CursorPage {} +export class MemoryListResponsesCursorPage extends CursorPage {} // After -export type MemoriesCursorPage = CursorPage; +export type MemoryListResponsesCursorPage = CursorPage; ``` If you were importing these classes at runtime, you'll need to switch to importing the base class or only import them at the type-level. diff --git a/README.md b/README.md index 6e6fb8e3..14ce4b1b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Hyperspell MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJzc2UiLCJ1cmwiOiJodHRwczovL2h5cGVyc3BlbGwuc3RsbWNwLmNvbS9zc2UifQ) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%2Fsse%22%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D) > Note: You may need to set environment variables in your MCP client. @@ -167,13 +167,13 @@ List methods in the Hyperspell API are paginated. You can use the `for await … of` syntax to iterate through items across all pages: ```ts -async function fetchAllMemories(params) { - const allMemories = []; +async function fetchAllMemoryListResponses(params) { + const allMemoryListResponses = []; // Automatically fetches more pages as needed. - for await (const memory of client.memories.list({ collection: 'REPLACE_ME' })) { - allMemories.push(memory); + for await (const memoryListResponse of client.memories.list({ collection: 'REPLACE_ME' })) { + allMemoryListResponses.push(memoryListResponse); } - return allMemories; + return allMemoryListResponses; } ``` @@ -181,8 +181,8 @@ Alternatively, you can request a single page at a time: ```ts let page = await client.memories.list({ collection: 'REPLACE_ME' }); -for (const memory of page.items) { - console.log(memory); +for (const memoryListResponse of page.items) { + console.log(memoryListResponse); } // Convenience methods are provided for manually paginating: diff --git a/api.md b/api.md index d9277c64..278f87e3 100644 --- a/api.md +++ b/api.md @@ -62,20 +62,21 @@ Methods: Types: -- Memory - MemoryStatus +- MemoryListResponse - MemoryDeleteResponse - MemoryAddBulkResponse +- MemoryGetResponse - MemoryStatusResponse Methods: - client.memories.update(resourceID, { ...params }) -> MemoryStatus -- client.memories.list({ ...params }) -> MemoriesCursorPage +- client.memories.list({ ...params }) -> MemoryListResponsesCursorPage - client.memories.delete(resourceID, { ...params }) -> MemoryDeleteResponse - client.memories.add({ ...params }) -> MemoryStatus - client.memories.addBulk({ ...params }) -> MemoryAddBulkResponse -- client.memories.get(resourceID, { ...params }) -> Memory +- client.memories.get(resourceID, { ...params }) -> MemoryGetResponse - client.memories.search({ ...params }) -> QueryResult - client.memories.status() -> MemoryStatusResponse - client.memories.upload({ ...params }) -> MemoryStatus diff --git a/package.json b/package.json index 75244aaa..655d2f79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperspell", - "version": "0.30.0", + "version": "0.31.0", "description": "The official TypeScript library for the Hyperspell API", "author": "Hyperspell ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 3e454fbc..656c6b69 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -41,14 +41,14 @@ For clients with a configuration JSON, it might look something like this: If you use Cursor, you can install the MCP server by using the button below. You will need to set your environment variables in Cursor's `mcp.json`, which can be found in Cursor Settings > Tools & MCP > New MCP Server. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJzc2UiLCJ1cmwiOiJodHRwczovL2h5cGVyc3BlbGwuc3RsbWNwLmNvbS9zc2UiLCJlbnYiOnsiSFlQRVJTUEVMTF9BUElfS0VZIjoiU2V0IHlvdXIgSFlQRVJTUEVMTF9BUElfS0VZIGhlcmUuIiwiSFlQRVJTUEVMTF9VU0VSX0lEIjoiU2V0IHlvdXIgSFlQRVJTUEVMTF9VU0VSX0lEIGhlcmUuIn19) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0) ### VS Code If you use MCP, you can install the MCP server by clicking the link below. You will need to set your environment variables in VS Code's `mcp.json`, which can be found via Command Palette > MCP: Open User Configuration. -[Open VS Code](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%2Fsse%22%2C%22env%22%3A%7B%22HYPERSPELL_API_KEY%22%3A%22Set%20your%20HYPERSPELL_API_KEY%20here.%22%2C%22HYPERSPELL_USER_ID%22%3A%22Set%20your%20HYPERSPELL_USER_ID%20here.%22%7D%7D) +[Open VS Code](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D) ### Claude Code @@ -56,7 +56,7 @@ If you use Claude Code, you can install the MCP server by running the command be environment variables in Claude Code's `.claude.json`, which can be found in your home directory. ``` -claude mcp add hyperspell_mcp_api --env HYPERSPELL_API_KEY="Your HYPERSPELL_API_KEY here." HYPERSPELL_USER_ID="Your HYPERSPELL_USER_ID here." --transport sse https://hyperspell.stlmcp.com/sse +claude mcp add hyperspell_mcp_api --header "x-hyperspell-api-key: My API Key" --header "X-As-User: My User ID" --transport http https://hyperspell.stlmcp.com ``` ## Code Mode diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 89e47e24..0e369567 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "hyperspell-mcp", - "version": "0.30.0", + "version": "0.31.0", "description": "The official MCP Server for the Hyperspell API", "author": "Hyperspell ", "types": "dist/index.d.ts", @@ -34,10 +34,13 @@ "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.25.2", "@valtown/deno-http-worker": "^0.0.21", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "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", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -50,9 +53,11 @@ }, "devDependencies": { "@anthropic-ai/mcpb": "^2.1.2", + "@types/cookie-parser": "^1.4.10", "@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", diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 5966b4ff..1ed298cc 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -2,8 +2,9 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { readEnv, readEnvOrError } from './server'; +import { readEnv, requireValue } from './server'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { Hyperspell } from 'hyperspell'; const prompt = `Runs JavaScript code to interact with the Hyperspell API. @@ -54,7 +55,7 @@ export function codeTool(): McpTool { required: ['code'], }, }; - const handler = async (_: unknown, args: any): Promise => { + const handler = async (client: Hyperspell, args: any): Promise => { const code = args.code as string; const intent = args.intent as string | undefined; @@ -70,8 +71,11 @@ export function codeTool(): McpTool { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), 'Content-Type': 'application/json', client_envs: JSON.stringify({ - HYPERSPELL_API_KEY: readEnvOrError('HYPERSPELL_API_KEY'), - HYPERSPELL_BASE_URL: readEnv('HYPERSPELL_BASE_URL'), + 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({ diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index 2624322f..a924dab2 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -3,7 +3,7 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from 'hyperspell'; -export const parseAuthHeaders = (req: IncomingMessage): Partial => { +export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { if (req.headers.authorization) { const scheme = req.headers.authorization.split(' ')[0]!; const value = req.headers.authorization.slice(scheme.length + 1); @@ -15,6 +15,8 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial = 'Unsupported authorization scheme. Expected the "Authorization" header to be a supported scheme (Bearer).', ); } + } else if (required) { + throw new Error('Missing required Authorization header; see WWW-Authenticate header for details.'); } const apiKey = diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index dcfeba6a..1f851cb6 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,13 +2,14 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; - import express from 'express'; +import morgan from 'morgan'; +import morganBody from 'morgan-body'; import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; -const newServer = ({ +const newServer = async ({ clientOptions, req, res, @@ -16,12 +17,12 @@ const newServer = ({ clientOptions: ClientOptions; req: express.Request; res: express.Response; -}): McpServer | null => { - const server = newMcpServer(); +}): Promise => { + const server = await newMcpServer(); try { - const authOptions = parseAuthHeaders(req); - initMcpServer({ + const authOptions = parseAuthHeaders(req, false); + await initMcpServer({ server: server, clientOptions: { ...clientOptions, @@ -45,7 +46,7 @@ const newServer = ({ const post = (options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) => async (req: express.Request, res: express.Response) => { - const server = newServer({ ...options, req, res }); + const server = await newServer({ ...options, req, res }); // If we return null, we already set the authorization error. if (server === null) return; const transport = new StreamableHTTPServerTransport(); @@ -75,15 +76,28 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, - mcpOptions = {}, + mcpOptions, + debug, }: { clientOptions?: ClientOptions; - mcpOptions?: McpOptions; + 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.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); app.delete('/', del); @@ -91,9 +105,13 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { - const app = streamableHTTPApp({ mcpOptions: options }); - const server = app.listen(port); +export const launchStreamableHTTPServer = async (params: { + mcpOptions: McpOptions; + debug: boolean; + port: number | string | undefined; +}) => { + const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); + const server = app.listen(params.port); const address = server.address(); if (typeof address === 'string') { @@ -101,6 +119,6 @@ export const launchStreamableHTTPServer = async (options: McpOptions, port: numb } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${port}`); + console.error(`MCP Server running on streamable HTTP on port ${params.port}`); } }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 0f6dd426..d75968e3 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -21,7 +21,11 @@ async function main() { await launchStdioServer(); break; case 'http': - await launchStreamableHTTPServer(options, options.port ?? options.socket); + await launchStreamableHTTPServer({ + mcpOptions: options, + debug: options.debug, + port: options.port ?? options.socket, + }); break; } } diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index c66ad8ce..74380833 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers'; import z from 'zod'; export type CLIOptions = McpOptions & { + debug: boolean; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; @@ -15,17 +16,24 @@ export type McpOptions = { export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) - .option('tools', { + .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('no-tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Use dynamic tools or all tools', + description: 'Tools to explicitly disable', }) - .option('no-tools', { + .option('port', { + type: 'number', + default: 3000, + description: 'Port to serve on if using http transport', + }) + .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Do not use any dynamic or all tools', + description: 'Tools to explicitly enable', }) .option('transport', { type: 'string', @@ -33,14 +41,8 @@ export function parseCLIOptions(): CLIOptions { default: 'stdio', description: 'What transport to use; stdio for local servers or http for remote servers', }) - .option('port', { - type: 'number', - description: 'Port to serve on if using http transport', - }) - .option('socket', { - type: 'string', - description: 'Unix socket to serve on if using http transport', - }) + .env('MCP_SERVER') + .version(true) .help(); const argv = opts.parseSync(); @@ -56,6 +58,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), + debug: !!argv.debug, transport, port: argv.port, socket: argv.socket, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 2965bd0b..e97e47c6 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -17,23 +17,59 @@ import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; export { ClientOptions } from 'hyperspell'; -export const newMcpServer = () => +async function getInstructions() { + // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + 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 () => new McpServer( { name: 'hyperspell_api', - version: '0.30.0', + version: '0.31.0', + }, + { + instructions: await getInstructions(), + capabilities: { tools: {}, logging: {} }, }, - { capabilities: { tools: {}, logging: {} } }, ); -// Create server instance -export const server = newMcpServer(); - /** * Initializes the provided MCP Server with the given tools and handlers. * If not provided, the default client, tools and handlers will be used. */ -export function initMcpServer(params: { +export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; @@ -146,3 +182,10 @@ export const readEnvOrError = (env: string): string => { } return envValue; }; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index f07696f3..47aeb0c9 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -2,9 +2,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async () => { - const server = newMcpServer(); + const server = await newMcpServer(); - initMcpServer({ server }); + await initMcpServer({ server }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index 08a5d77d..e95247d7 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -2,8 +2,8 @@ "include": ["src", "tests", "examples"], "exclude": [], "compilerOptions": { - "target": "es2020", - "lib": ["es2020"], + "target": "es2022", + "lib": ["es2022"], "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock index a592e9c7..fe102775 100644 --- a/packages/mcp-server/yarn.lock +++ b/packages/mcp-server/yarn.lock @@ -3302,9 +3302,6 @@ readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -"replicate@file:../../dist": - version "2.0.0-alpha.74" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" diff --git a/src/client.ts b/src/client.ts index 416a7912..fb8174a5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,15 +30,16 @@ import { } from './resources/evaluate'; import { Memories, - MemoriesCursorPage, - Memory, MemoryAddBulkParams, MemoryAddBulkResponse, MemoryAddParams, MemoryDeleteParams, MemoryDeleteResponse, MemoryGetParams, + MemoryGetResponse, MemoryListParams, + MemoryListResponse, + MemoryListResponsesCursorPage, MemorySearchParams, MemoryStatus, MemoryStatusResponse, @@ -549,9 +550,10 @@ export class Hyperspell { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - if (signal) signal.addEventListener('abort', () => controller.abort()); + const abort = this._makeAbort(controller); + if (signal) signal.addEventListener('abort', abort, { once: true }); - const timeout = setTimeout(() => controller.abort(), ms); + const timeout = setTimeout(abort, ms); const isReadableBody = ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) || @@ -574,6 +576,7 @@ export class Hyperspell { return await this.fetch.call(undefined, url, fetchOptions); } finally { clearTimeout(timeout); + if (signal) signal.removeEventListener('abort', abort); } } @@ -718,6 +721,12 @@ export class Hyperspell { return headers.values; } + private _makeAbort(controller: AbortController) { + // note: we can't just inline this method inside `fetchWithTimeout()` because then the closure + // would capture all request options, and cause a memory leak. + return () => controller.abort(); + } + private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): { bodyHeaders: HeadersLike; body: BodyInit | undefined; @@ -810,12 +819,13 @@ export declare namespace Hyperspell { export { Memories as Memories, - type Memory as Memory, type MemoryStatus as MemoryStatus, + type MemoryListResponse as MemoryListResponse, type MemoryDeleteResponse as MemoryDeleteResponse, type MemoryAddBulkResponse as MemoryAddBulkResponse, + type MemoryGetResponse as MemoryGetResponse, type MemoryStatusResponse as MemoryStatusResponse, - type MemoriesCursorPage as MemoriesCursorPage, + type MemoryListResponsesCursorPage as MemoryListResponsesCursorPage, type MemoryUpdateParams as MemoryUpdateParams, type MemoryListParams as MemoryListParams, type MemoryDeleteParams as MemoryDeleteParams, diff --git a/src/internal/parse.ts b/src/internal/parse.ts index 7c91bd39..af00cee3 100644 --- a/src/internal/parse.ts +++ b/src/internal/parse.ts @@ -29,6 +29,12 @@ export async function defaultParseResponse(client: Hyperspell, props: APIResp const mediaType = contentType?.split(';')[0]?.trim(); const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); if (isJSON) { + const contentLength = response.headers.get('content-length'); + if (contentLength === '0') { + // if there is no content we can't do anything + return undefined as T; + } + const json = await response.json(); return json as T; } diff --git a/src/resources/index.ts b/src/resources/index.ts index 23089d86..8ae020bb 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -24,10 +24,11 @@ export { } from './integrations/integrations'; export { Memories, - type Memory, type MemoryStatus, + type MemoryListResponse, type MemoryDeleteResponse, type MemoryAddBulkResponse, + type MemoryGetResponse, type MemoryStatusResponse, type MemoryUpdateParams, type MemoryListParams, @@ -37,7 +38,7 @@ export { type MemoryGetParams, type MemorySearchParams, type MemoryUploadParams, - type MemoriesCursorPage, + type MemoryListResponsesCursorPage, } from './memories'; export { Vaults, diff --git a/src/resources/memories.ts b/src/resources/memories.ts index 8e471527..8676c0c8 100644 --- a/src/resources/memories.ts +++ b/src/resources/memories.ts @@ -37,7 +37,7 @@ export class Memories extends APIResource { * @example * ```ts * // Automatically fetches more pages as needed. - * for await (const memory of client.memories.list()) { + * for await (const memoryListResponse of client.memories.list()) { * // ... * } * ``` @@ -45,8 +45,8 @@ export class Memories extends APIResource { list( query: MemoryListParams | null | undefined = {}, options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList('/memories/list', CursorPage, { query, ...options }); + ): PagePromise { + return this._client.getAPIList('/memories/list', CursorPage, { query, ...options }); } /** @@ -129,7 +129,7 @@ export class Memories extends APIResource { * }); * ``` */ - get(resourceID: string, params: MemoryGetParams, options?: RequestOptions): APIPromise { + get(resourceID: string, params: MemoryGetParams, options?: RequestOptions): APIPromise { const { source } = params; return this._client.get(path`/memories/get/${source}/${resourceID}`, options); } @@ -182,9 +182,27 @@ export class Memories extends APIResource { } } -export type MemoriesCursorPage = CursorPage; +export type MemoryListResponsesCursorPage = CursorPage; -export interface Memory { +export interface MemoryStatus { + resource_id: string; + + source: + | 'collections' + | 'reddit' + | 'notion' + | 'slack' + | 'google_calendar' + | 'google_mail' + | 'box' + | 'google_drive' + | 'vault' + | 'web_crawler'; + + status: 'pending' | 'processing' | 'completed' | 'failed'; +} + +export interface MemoryListResponse { resource_id: string; source: @@ -199,7 +217,7 @@ export interface Memory { | 'vault' | 'web_crawler'; - metadata?: Memory.Metadata; + metadata?: MemoryListResponse.Metadata; /** * The relevance of the resource to the query @@ -209,7 +227,7 @@ export interface Memory { title?: string | null; } -export namespace Memory { +export namespace MemoryListResponse { export interface Metadata { created_at?: string | null; @@ -237,24 +255,6 @@ export namespace Memory { } } -export interface MemoryStatus { - resource_id: string; - - source: - | 'collections' - | 'reddit' - | 'notion' - | 'slack' - | 'google_calendar' - | 'google_mail' - | 'box' - | 'google_drive' - | 'vault' - | 'web_crawler'; - - status: 'pending' | 'processing' | 'completed' | 'failed'; -} - export interface MemoryDeleteResponse { chunks_deleted: number; @@ -294,6 +294,59 @@ export interface MemoryAddBulkResponse { success?: boolean; } +export interface MemoryGetResponse { + resource_id: string; + + source: + | 'collections' + | 'reddit' + | 'notion' + | 'slack' + | 'google_calendar' + | 'google_mail' + | 'box' + | 'google_drive' + | 'vault' + | 'web_crawler'; + + metadata?: MemoryGetResponse.Metadata; + + /** + * The relevance of the resource to the query + */ + score?: number | null; + + title?: string | null; +} + +export namespace MemoryGetResponse { + export interface Metadata { + created_at?: string | null; + + events?: Array; + + indexed_at?: string | null; + + last_modified?: string | null; + + status?: 'pending' | 'processing' | 'completed' | 'failed'; + + url?: string | null; + + [k: string]: unknown; + } + + export namespace Metadata { + export interface Event { + message: string; + + type: 'error' | 'warning' | 'info' | 'success'; + + time?: string; + } + } +} + export interface MemoryStatusResponse { providers: { [key: string]: { [key: string]: number } }; @@ -953,12 +1006,13 @@ export interface MemoryUploadParams { export declare namespace Memories { export { - type Memory as Memory, type MemoryStatus as MemoryStatus, + type MemoryListResponse as MemoryListResponse, type MemoryDeleteResponse as MemoryDeleteResponse, type MemoryAddBulkResponse as MemoryAddBulkResponse, + type MemoryGetResponse as MemoryGetResponse, type MemoryStatusResponse as MemoryStatusResponse, - type MemoriesCursorPage as MemoriesCursorPage, + type MemoryListResponsesCursorPage as MemoryListResponsesCursorPage, type MemoryUpdateParams as MemoryUpdateParams, type MemoryListParams as MemoryListParams, type MemoryDeleteParams as MemoryDeleteParams, diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 1fc4772c..3a4a309f 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1,9 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import * as MemoriesAPI from './memories'; - export interface QueryResult { - documents: Array; + documents: Array; /** * The answer to the query, if the request was set to answer. @@ -27,3 +25,58 @@ export interface QueryResult { */ score?: number | null; } + +export namespace QueryResult { + export interface Document { + resource_id: string; + + source: + | 'collections' + | 'reddit' + | 'notion' + | 'slack' + | 'google_calendar' + | 'google_mail' + | 'box' + | 'google_drive' + | 'vault' + | 'web_crawler'; + + metadata?: Document.Metadata; + + /** + * The relevance of the resource to the query + */ + score?: number | null; + + title?: string | null; + } + + export namespace Document { + export interface Metadata { + created_at?: string | null; + + events?: Array; + + indexed_at?: string | null; + + last_modified?: string | null; + + status?: 'pending' | 'processing' | 'completed' | 'failed'; + + url?: string | null; + + [k: string]: unknown; + } + + export namespace Metadata { + export interface Event { + message: string; + + type: 'error' | 'warning' | 'info' | 'success'; + + time?: string; + } + } + } +} diff --git a/src/version.ts b/src/version.ts index 91a9bb6d..b6314c28 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.30.0'; // x-release-please-version +export const VERSION = '0.31.0'; // x-release-please-version