diff --git a/.stats.yml b/.stats.yml index 8716811..d28156b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml -openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397 -config_hash: a3a8e3c71c17eabb21ab8173521181a4 +configured_endpoints: 22 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-fc4ab722e6762cc69d533f57bea0d70b00e44a30c4ad8144e14ff70a1170ec7c.yml +openapi_spec_hash: 2533ea676c195d5f7d30a67c201fd32d +config_hash: b32e7b67898ff493ca749cdd513ab870 diff --git a/MIGRATION.md b/MIGRATION.md index 78e709a..3b0d763 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -54,6 +54,7 @@ client.parents.children.retrieve('c_456', { parent_id: 'p_123' }); This affects the following methods: +- `client.memories.update()` - `client.memories.delete()` - `client.memories.get()` diff --git a/api.md b/api.md index c6adb2b..6a3115e 100644 --- a/api.md +++ b/api.md @@ -69,6 +69,7 @@ Types: Methods: +- client.memories.update(resourceID, { ...params }) -> MemoryStatus - client.memories.list({ ...params }) -> MemoriesCursorPage - client.memories.delete(resourceID, { ...params }) -> MemoryDeleteResponse - client.memories.add({ ...params }) -> MemoryStatus diff --git a/bin/migration-config.json b/bin/migration-config.json index 14738c4..629c459 100644 --- a/bin/migration-config.json +++ b/bin/migration-config.json @@ -3,6 +3,43 @@ "githubRepo": "https://github.com/hyperspell/node-sdk", "clientClass": "Hyperspell", "methods": [ + { + "base": "memories", + "name": "update", + "params": [ + { + "type": "param", + "key": "resource_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "source", + "location": "path" + }, + { + "type": "param", + "key": "resource_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ] + }, { "base": "memories", "name": "delete", diff --git a/packages/mcp-server/.dockerignore b/packages/mcp-server/.dockerignore new file mode 100644 index 0000000..1850726 --- /dev/null +++ b/packages/mcp-server/.dockerignore @@ -0,0 +1,66 @@ +# Dependencies +node_modules/ +**/node_modules/ + +# Build outputs +dist/ +**/dist/ +build/ +**/build/ + +# Git +.git/ +.gitignore + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +LICENSE + +# Testing +test/ +tests/ +__tests__/ +*.test.js +*.spec.js +coverage/ +.nyc_output/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.* + +# Temporary files +*.tmp +*.temp +.cache/ + +# Examples and scripts +examples/ +bin/ + +# Other packages (we only need mcp-server) +packages/*/ +!packages/mcp-server/ diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile new file mode 100644 index 0000000..e9af9ae --- /dev/null +++ b/packages/mcp-server/Dockerfile @@ -0,0 +1,77 @@ +# Dockerfile for Hyperspell MCP Server + # + # This Dockerfile builds a Docker image for the MCP Server. + # + # To build the image locally: + # docker build -f packages/mcp-server/Dockerfile -t hyperspell-mcp:local . + # + # To run the image: + # docker run -i hyperspell-mcp:local [OPTIONS] + # + # Common options: + # --tool= Include specific tools + # --resource= Include tools for specific resources + # --operation=read|write Filter by operation type + # --client= Set client compatibility (e.g., claude, cursor) + # --transport= Set transport type (stdio or http) + # + # For a full list of options: + # docker run -i hyperspell-mcp:local --help + # + # Note: The MCP server uses stdio transport by default. Docker's -i flag + # enables interactive mode, allowing the container to communicate over stdin/stdout. + + # Build stage + FROM node:20-alpine AS builder + + # Install bash for build script + RUN apk add --no-cache bash openssl + + # Set working directory + WORKDIR /build + + # Copy entire repository + COPY . . + + # Install all dependencies and build everything + RUN yarn install --frozen-lockfile && \ + yarn build + + # Production stage + + FROM denoland/deno:alpine + RUN apk add --no-cache npm + + # Add non-root user + RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 + + # Set working directory + WORKDIR /app + +# Copy the built mcp-server dist directory +COPY --from=builder /build/packages/mcp-server/dist ./ + +# Copy node_modules from mcp-server (includes all production deps) +COPY --from=builder /build/packages/mcp-server/node_modules ./node_modules + + # Copy the built hyperspell into node_modules + COPY --from=builder /build/dist ./node_modules/hyperspell + + # Change ownership to nodejs user + RUN chown -R nodejs:nodejs /app + + # Switch to non-root user + USER nodejs + + # The MCP server uses stdio transport by default + # No exposed ports needed for stdio communication + + # This is needed for node to run on the deno:alpine image. + # See . + ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib + + # Set the entrypoint to the MCP server + ENTRYPOINT ["node", "index.js"] + + # Allow passing arguments to the MCP server + CMD [] diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index bbd404c..48db197 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -254,6 +254,7 @@ The following tools are available in this MCP server. ### Resource `memories`: +- `update_memory` (`write`): This tool lets you update memory in Hyperspell. - `add_memory` (`write`): This tool lets you add text, markdown, or JSON to the Hyperspell index so it can be searched later. It will return the `source` and `resource_id` that can be used to later retrieve the processed memory. - `get_memory` (`read`): This tool lets you retrieve a memory that has been previously indexed. - `search` (`write`): Search all memories indexed by Hyperspell. Set 'answer' to true to directly answer the query, or to 'false' to simply get all memories related to the query. diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 967415c..6b52fd8 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.22.1", + "version": "0.27.0", "description": "The official MCP Server for the Hyperspell API", "author": { "name": "Hyperspell", diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index cabacce..e7cf91c 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -7,6 +7,7 @@ export { Metadata, Endpoint, HandlerFunction }; import list_connections from './connections/list-connections'; import list_integrations from './integrations/list-integrations'; import connect_integration from './integrations/connect-integration'; +import update_memory from './memories/update-memory'; import add_memory from './memories/add-memory'; import get_memory from './memories/get-memory'; import search from './memories/search'; @@ -22,6 +23,7 @@ function addEndpoint(endpoint: Endpoint) { addEndpoint(list_connections); addEndpoint(list_integrations); addEndpoint(connect_integration); +addEndpoint(update_memory); addEndpoint(add_memory); addEndpoint(get_memory); addEndpoint(search); diff --git a/packages/mcp-server/src/tools/memories/update-memory.ts b/packages/mcp-server/src/tools/memories/update-memory.ts new file mode 100644 index 0000000..c692b75 --- /dev/null +++ b/packages/mcp-server/src/tools/memories/update-memory.ts @@ -0,0 +1,159 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { isJqError, maybeFilter } from 'hyperspell-mcp/filtering'; +import { Metadata, asErrorResult, asTextContentResult } from 'hyperspell-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import Hyperspell from 'hyperspell'; + +export const metadata: Metadata = { + resource: 'memories', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/memories/update/{source}/{resource_id}', + operationId: 'update_memories_update__source___resource_id__post', +}; + +export const tool: Tool = { + name: 'update_memory', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nThis tool lets you update memory in Hyperspell.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/memory_status',\n $defs: {\n memory_status: {\n type: 'object',\n title: 'DocumentStatusResponse',\n properties: {\n resource_id: {\n type: 'string',\n title: 'Resource Id'\n },\n source: {\n type: 'string',\n title: 'DocumentProviders',\n enum: [ 'collections',\n 'vault',\n 'web_crawler',\n 'notion',\n 'slack',\n 'google_calendar',\n 'reddit',\n 'box',\n 'google_drive',\n 'airtable',\n 'algolia',\n 'amplitude',\n 'asana',\n 'ashby',\n 'bamboohr',\n 'basecamp',\n 'bubbles',\n 'calendly',\n 'confluence',\n 'clickup',\n 'datadog',\n 'deel',\n 'discord',\n 'dropbox',\n 'exa',\n 'facebook',\n 'front',\n 'github',\n 'gitlab',\n 'google_docs',\n 'google_mail',\n 'google_sheet',\n 'hubspot',\n 'jira',\n 'linear',\n 'microsoft_teams',\n 'mixpanel',\n 'monday',\n 'outlook',\n 'perplexity',\n 'rippling',\n 'salesforce',\n 'segment',\n 'todoist',\n 'twitter',\n 'zoom'\n ]\n },\n status: {\n type: 'string',\n title: 'DocumentStatus',\n enum: [ 'pending',\n 'processing',\n 'completed',\n 'failed'\n ]\n }\n },\n required: [ 'resource_id',\n 'source',\n 'status'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + source: { + type: 'string', + title: 'DocumentProviders', + enum: [ + 'collections', + 'vault', + 'web_crawler', + 'notion', + 'slack', + 'google_calendar', + 'reddit', + 'box', + 'google_drive', + 'airtable', + 'algolia', + 'amplitude', + 'asana', + 'ashby', + 'bamboohr', + 'basecamp', + 'bubbles', + 'calendly', + 'confluence', + 'clickup', + 'datadog', + 'deel', + 'discord', + 'dropbox', + 'exa', + 'facebook', + 'front', + 'github', + 'gitlab', + 'google_docs', + 'google_mail', + 'google_sheet', + 'hubspot', + 'jira', + 'linear', + 'microsoft_teams', + 'mixpanel', + 'monday', + 'outlook', + 'perplexity', + 'rippling', + 'salesforce', + 'segment', + 'todoist', + 'twitter', + 'zoom', + ], + }, + resource_id: { + type: 'string', + title: 'Resource Id', + }, + collection: { + anyOf: [ + { + type: 'string', + }, + { + type: 'object', + additionalProperties: true, + }, + ], + title: 'Collection', + description: 'The collection to move the document to. Set to null to remove the collection.', + }, + metadata: { + anyOf: [ + { + type: 'object', + additionalProperties: true, + }, + { + type: 'object', + additionalProperties: true, + }, + ], + title: 'Metadata', + description: + 'Custom metadata for filtering. Keys must be alphanumeric with underscores, max 64 chars. Values must be string, number, or boolean. Will be merged with existing metadata.', + }, + text: { + anyOf: [ + { + type: 'string', + }, + { + type: 'object', + additionalProperties: true, + }, + ], + title: 'Text', + description: 'Full text of the document. If provided, the document will be re-indexed.', + }, + title: { + anyOf: [ + { + type: 'string', + }, + { + type: 'object', + additionalProperties: true, + }, + ], + title: 'Title', + description: 'Title of the document.', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['source', 'resource_id'], + }, + annotations: {}, +}; + +export const handler = async (client: Hyperspell, args: Record | undefined) => { + const { resource_id, jq_filter, ...body } = args as any; + try { + return asTextContentResult(await maybeFilter(jq_filter, await client.memories.update(resource_id, body))); + } catch (error) { + if (error instanceof Hyperspell.APIError || isJqError(error)) { + return asErrorResult(error.message); + } + throw error; + } +}; + +export default { metadata, tool, handler }; diff --git a/src/client.ts b/src/client.ts index 812c4bd..6e4110c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,6 +40,7 @@ import { MemorySearchParams, MemoryStatus, MemoryStatusResponse, + MemoryUpdateParams, MemoryUploadParams, } from './resources/memories'; import { VaultListParams, VaultListResponse, VaultListResponsesCursorPage, Vaults } from './resources/vaults'; @@ -812,6 +813,7 @@ export declare namespace Hyperspell { type MemoryDeleteResponse as MemoryDeleteResponse, type MemoryStatusResponse as MemoryStatusResponse, type MemoriesCursorPage as MemoriesCursorPage, + type MemoryUpdateParams as MemoryUpdateParams, type MemoryListParams as MemoryListParams, type MemoryDeleteParams as MemoryDeleteParams, type MemoryAddParams as MemoryAddParams, diff --git a/src/resources/index.ts b/src/resources/index.ts index b220e10..da3c3d7 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -28,6 +28,7 @@ export { type MemoryStatus, type MemoryDeleteResponse, type MemoryStatusResponse, + type MemoryUpdateParams, type MemoryListParams, type MemoryDeleteParams, type MemoryAddParams, diff --git a/src/resources/memories.ts b/src/resources/memories.ts index 8802aed..15b979c 100644 --- a/src/resources/memories.ts +++ b/src/resources/memories.ts @@ -10,6 +10,26 @@ import { multipartFormRequestOptions } from '../internal/uploads'; import { path } from '../internal/utils/path'; export class Memories extends APIResource { + /** + * Updates an existing document in the index. You can update the text, collection, + * title, and metadata. The document must already exist or a 404 will be returned. + * This works for documents from any source (vault, slack, gmail, etc.). + * + * To remove a collection, set it to null explicitly. + * + * @example + * ```ts + * const memoryStatus = await client.memories.update( + * 'resource_id', + * { source: 'collections' }, + * ); + * ``` + */ + update(resourceID: string, params: MemoryUpdateParams, options?: RequestOptions): APIPromise { + const { source, ...body } = params; + return this._client.post(path`/memories/update/${source}/${resourceID}`, { body, ...options }); + } + /** * This endpoint allows you to paginate through all documents in the index. You can * filter the documents by title, date, metadata, etc. @@ -350,12 +370,95 @@ export interface MemoryStatusResponse { total: { [key: string]: number }; } +export interface MemoryUpdateParams { + /** + * Path param: + */ + source: + | 'collections' + | 'vault' + | 'web_crawler' + | 'notion' + | 'slack' + | 'google_calendar' + | 'reddit' + | 'box' + | 'google_drive' + | 'airtable' + | 'algolia' + | 'amplitude' + | 'asana' + | 'ashby' + | 'bamboohr' + | 'basecamp' + | 'bubbles' + | 'calendly' + | 'confluence' + | 'clickup' + | 'datadog' + | 'deel' + | 'discord' + | 'dropbox' + | 'exa' + | 'facebook' + | 'front' + | 'github' + | 'gitlab' + | 'google_docs' + | 'google_mail' + | 'google_sheet' + | 'hubspot' + | 'jira' + | 'linear' + | 'microsoft_teams' + | 'mixpanel' + | 'monday' + | 'outlook' + | 'perplexity' + | 'rippling' + | 'salesforce' + | 'segment' + | 'todoist' + | 'twitter' + | 'zoom'; + + /** + * Body param: The collection to move the document to. Set to null to remove the + * collection. + */ + collection?: string | unknown | null; + + /** + * Body param: Custom metadata for filtering. Keys must be alphanumeric with + * underscores, max 64 chars. Values must be string, number, or boolean. Will be + * merged with existing metadata. + */ + metadata?: { [key: string]: string | number | boolean } | unknown | null; + + /** + * Body param: Full text of the document. If provided, the document will be + * re-indexed. + */ + text?: string | unknown | null; + + /** + * Body param: Title of the document. + */ + title?: string | unknown | null; +} + export interface MemoryListParams extends CursorPageParams { /** * Filter documents by collection. */ collection?: string | null; + /** + * Filter documents by metadata using MongoDB-style operators. Example: + * {"department": "engineering", "priority": {"$gt": 3}} + */ + filter?: string | null; + /** * Filter documents by source. */ @@ -1059,6 +1162,7 @@ export declare namespace Memories { type MemoryDeleteResponse as MemoryDeleteResponse, type MemoryStatusResponse as MemoryStatusResponse, type MemoriesCursorPage as MemoriesCursorPage, + type MemoryUpdateParams as MemoryUpdateParams, type MemoryListParams as MemoryListParams, type MemoryDeleteParams as MemoryDeleteParams, type MemoryAddParams as MemoryAddParams, diff --git a/tests/api-resources/memories.test.ts b/tests/api-resources/memories.test.ts index 2bfd8dd..a700ca3 100644 --- a/tests/api-resources/memories.test.ts +++ b/tests/api-resources/memories.test.ts @@ -9,6 +9,27 @@ const client = new Hyperspell({ }); describe('resource memories', () => { + test('update: only required params', async () => { + const responsePromise = client.memories.update('resource_id', { source: 'collections' }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('update: required and optional params', async () => { + const response = await client.memories.update('resource_id', { + source: 'collections', + collection: 'string', + metadata: { foo: 'string' }, + text: 'string', + title: 'string', + }); + }); + test('list', async () => { const responsePromise = client.memories.list(); const rawResponse = await responsePromise.asResponse(); @@ -24,7 +45,7 @@ describe('resource memories', () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( client.memories.list( - { collection: 'collection', cursor: 'cursor', size: 0, source: 'collections' }, + { collection: 'collection', cursor: 'cursor', filter: 'filter', size: 0, source: 'collections' }, { path: '/_stainless_unknown_path' }, ), ).rejects.toThrow(Hyperspell.NotFoundError);