diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ddfa3e3..8032c17 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.1" + ".": "0.12.0" } diff --git a/.stats.yml b/.stats.yml index 0987f00..e26fc30 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/isaacus%2Fisaacus-ee884a4336559147aacf9a927a540f21e9760f00d2d5588af00fa8a25e2707d9.yml -openapi_spec_hash: 2ba78bd360942c63a7d08dba791f00d2 -config_hash: a85580968a69d8d6fadf96e5e2d6870e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/isaacus%2Fisaacus-6705b8e0baa0e4aad69a1c04e9876b352e40e0e5caf21e87e7b2c355e70c4e66.yml +openapi_spec_hash: 87d3cc80f5ddc5275e8a47d35f1a484e +config_hash: eb6af7379e9073b3ece2803bfcf65e68 diff --git a/CHANGELOG.md b/CHANGELOG.md index 03155e3..9c6c27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 0.12.0 (2025-12-02) + +Full Changelog: [v0.11.1...v0.12.0](https://github.com/isaacus-dev/isaacus-typescript/compare/v0.11.1...v0.12.0) + +### Features + +* **mcp:** add detail field to docs search tool ([a4a3478](https://github.com/isaacus-dev/isaacus-typescript/commit/a4a3478307e75a7147f2bc3137677327abbeae35)) +* **mcp:** enable optional code execution tool on http mcp servers ([579b63d](https://github.com/isaacus-dev/isaacus-typescript/commit/579b63d0d5a3cde717ac41654d6e708673d0191c)) +* **mcp:** return logs on code tool errors ([f8cc9b0](https://github.com/isaacus-dev/isaacus-typescript/commit/f8cc9b0afc5365abe559f07c85883895ee2f4900)) + + +### Bug Fixes + +* **mcpb:** pin @anthropic-ai/mcpb version ([fc022b2](https://github.com/isaacus-dev/isaacus-typescript/commit/fc022b2a940c8c3161082faa8c4e5457bdd53957)) +* **mcp:** return tool execution error on api error ([5ae2a33](https://github.com/isaacus-dev/isaacus-typescript/commit/5ae2a33f2dfe5e3f1437e48045c15be34ee9c4e5)) +* **mcp:** return tool execution error on jq failure ([719a2bc](https://github.com/isaacus-dev/isaacus-typescript/commit/719a2bcb3ae336919ed9f63c04c06bf1eb5ad1fd)) + + +### Chores + +* **client:** fix logger property type ([73a6ad5](https://github.com/isaacus-dev/isaacus-typescript/commit/73a6ad5b2c1a9cd42c061401c47abc51db8510d4)) +* **internal:** codegen related update ([d58f59b](https://github.com/isaacus-dev/isaacus-typescript/commit/d58f59bc6ac418f586698a45d535a68998454cbe)) +* **internal:** codegen related update ([47ef370](https://github.com/isaacus-dev/isaacus-typescript/commit/47ef37027fcc8731651c47ec72db684ae0662e54)) +* **internal:** grammar fix (it's -> its) ([cbeea36](https://github.com/isaacus-dev/isaacus-typescript/commit/cbeea365a8107e894cdeff0d0f7ed6ecfb1e2f2f)) +* mcp code tool explicit error message when missing a run function ([45f0ec4](https://github.com/isaacus-dev/isaacus-typescript/commit/45f0ec4f7cd466097f623a9dde1a0b8e084f9fdc)) +* **mcp:** add friendlier MCP code tool errors on incorrect method invocations ([08e1eac](https://github.com/isaacus-dev/isaacus-typescript/commit/08e1eac13c3581f7c09150de36e8948e8ee94b6e)) +* **mcp:** add line numbers to code tool errors ([51fc353](https://github.com/isaacus-dev/isaacus-typescript/commit/51fc353019df5583769e087b93153d398019a639)) +* **mcp:** clarify http auth error ([39fab03](https://github.com/isaacus-dev/isaacus-typescript/commit/39fab03e07a9d600b627d2146fbe231377a78577)) +* **mcp:** upgrade jq-web ([5f375e8](https://github.com/isaacus-dev/isaacus-typescript/commit/5f375e8eb4353218198c5edd82404e00066a1fff)) +* use structured error when code execution tool errors ([bb4ba8a](https://github.com/isaacus-dev/isaacus-typescript/commit/bb4ba8a1485229cfc60ba38361520b2a9f65b7de)) + + +### Documentation + +* **mcp:** add a README button for one-click add to Cursor ([afbeeb5](https://github.com/isaacus-dev/isaacus-typescript/commit/afbeeb5b7b662f3144ef2571b7f33b1225daf5df)) +* **mcp:** add a README link to add server to VS Code or Claude Code ([6042a69](https://github.com/isaacus-dev/isaacus-typescript/commit/6042a69bb085d6a0d21b982d448e540fccc6036f)) +* **sdk:** specify example params ([00ec4d8](https://github.com/isaacus-dev/isaacus-typescript/commit/00ec4d8e97a35bc2de73666db6eede82bd618236)) + ## 0.11.1 (2025-10-14) Full Changelog: [v0.11.0...v0.11.1](https://github.com/isaacus-dev/isaacus-typescript/compare/v0.11.0...v0.11.1) diff --git a/README.md b/README.md index 5b38b5f..2899786 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ const client = new Isaacus({ const embeddingResponse = await client.embeddings.create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], + task: 'retrieval/query', }); console.log(embeddingResponse.embeddings); @@ -49,6 +50,7 @@ const client = new Isaacus({ const params: Isaacus.EmbeddingCreateParams = { model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], + task: 'retrieval/query', }; const embeddingResponse: Isaacus.EmbeddingResponse = await client.embeddings.create(params); ``` @@ -67,6 +69,7 @@ const embeddingResponse = await client.embeddings .create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], + task: 'retrieval/query', }) .catch(async (err) => { if (err instanceof Isaacus.APIError) { @@ -108,7 +111,7 @@ const client = new Isaacus({ }); // Or, configure per-request: -await client.embeddings.create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'] }, { +await client.embeddings.create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], task: 'retrieval/query' }, { maxRetries: 5, }); ``` @@ -125,7 +128,7 @@ const client = new Isaacus({ }); // Override per-request: -await client.embeddings.create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'] }, { +await client.embeddings.create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], task: 'retrieval/query' }, { timeout: 5 * 1000, }); ``` @@ -152,6 +155,7 @@ const response = await client.embeddings .create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], + task: 'retrieval/query', }) .asResponse(); console.log(response.headers.get('X-My-Header')); @@ -161,6 +165,7 @@ const { data: embeddingResponse, response: raw } = await client.embeddings .create({ model: 'kanon-2-embedder', texts: ['Are restraints of trade enforceable under English law?', 'What is a non-compete clause?'], + task: 'retrieval/query', }) .withResponse(); console.log(raw.headers.get('X-My-Header')); diff --git a/package.json b/package.json index a9d0106..b5a7768 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "isaacus", - "version": "0.11.1", + "version": "0.12.0", "description": "The official TypeScript library for the Isaacus API", "author": "Isaacus ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 9c5476e..6d54f7f 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -34,12 +34,36 @@ For clients with a configuration JSON, it might look something like this: } ``` +### Cursor + +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=isaacus-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImlzYWFjdXMtbWNwIl0sImVudiI6eyJJU0FBQ1VTX0FQSV9LRVkiOiJTZXQgeW91ciBJU0FBQ1VTX0FQSV9LRVkgaGVyZS4ifX0) + +### 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%22isaacus-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22isaacus-mcp%22%5D%2C%22env%22%3A%7B%22ISAACUS_API_KEY%22%3A%22Set%20your%20ISAACUS_API_KEY%20here.%22%7D%7D) + +### Claude Code + +If you use Claude Code, you can install the MCP server by running the command below in your terminal. You will need to set your +environment variables in Claude Code's `.claude.json`, which can be found in your home directory. + +``` +claude mcp add --transport stdio isaacus_api --env ISAACUS_API_KEY="Your ISAACUS_API_KEY here." -- npx -y isaacus-mcp +``` + ## Exposing endpoints to your MCP Client -There are two ways to expose endpoints as tools in the MCP server: +There are three ways to expose endpoints as tools in the MCP server: 1. Exposing one tool per endpoint, and filtering as necessary 2. Exposing a set of tools to dynamically discover and invoke endpoints from the API +3. Exposing a docs search tool and a code execution tool, allowing the client to write code to be executed against the TypeScript client ### Filtering endpoints and tools @@ -74,6 +98,18 @@ All of these command-line options can be repeated, combined together, and have c Use `--list` to see the list of available tools, or see below. +### Code execution + +If you specify `--tools=code` to the MCP server, it will expose just two tools: + +- `search_docs` - Searches the API documentation and returns a list of markdown results +- `execute` - Runs code against the TypeScript client + +This allows the LLM to implement more complex logic by chaining together many API calls without loading +intermediary results into its context window. + +The code execution itself happens in a Deno sandbox that has network access only to the base URL for the API. + ### Specifying the MCP Client Different clients have varying abilities to handle arbitrary tools and schemas. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index bfd570a..97196ed 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "isaacus-mcp", - "version": "0.11.1", + "version": "0.12.0", "description": "The official MCP Server for the Isaacus API", "author": "Isaacus ", "types": "dist/index.d.ts", @@ -36,8 +36,10 @@ "@valtown/deno-http-worker": "^0.0.21", "cors": "^2.8.5", "express": "^5.1.0", - "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz", + "fuse.js": "^7.1.0", + "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", "qs": "^6.14.0", + "typescript": "5.8.3", "yargs": "^17.7.2", "zod": "^3.25.20", "zod-to-json-schema": "^3.24.5", @@ -47,7 +49,7 @@ "mcp-server": "dist/index.js" }, "devDependencies": { - "@anthropic-ai/mcpb": "^1.1.0", + "@anthropic-ai/mcpb": "1.1.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", @@ -64,8 +66,7 @@ "ts-morph": "^19.0.0", "ts-node": "^10.5.0", "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.9/tsc-multi.tgz", - "tsconfig-paths": "^4.0.0", - "typescript": "5.8.3" + "tsconfig-paths": "^4.0.0" }, "imports": { "isaacus-mcp": ".", diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts index c7d8173..65ef304 100644 --- a/packages/mcp-server/src/code-tool-types.ts +++ b/packages/mcp-server/src/code-tool-types.ts @@ -11,4 +11,8 @@ export type WorkerSuccess = { logLines: string[]; errLines: string[]; }; -export type WorkerError = { message: string | undefined }; +export type WorkerError = { + message: string | undefined; + logLines: string[]; + errLines: string[]; +}; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts index 032da42..a19ef41 100644 --- a/packages/mcp-server/src/code-tool-worker.ts +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -1,11 +1,171 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import util from 'node:util'; + +import Fuse from 'fuse.js'; +import ts from 'typescript'; + import { WorkerInput, WorkerSuccess, WorkerError } from './code-tool-types'; import { Isaacus } from 'isaacus'; +function getRunFunctionNode( + code: string, +): ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | null { + const sourceFile = ts.createSourceFile('code.ts', code, ts.ScriptTarget.Latest, true); + + for (const statement of sourceFile.statements) { + // Check for top-level function declarations + if (ts.isFunctionDeclaration(statement)) { + if (statement.name?.text === 'run') { + return statement; + } + } + + // 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 + if ( + declaration.initializer && + (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer)) + ) { + return declaration.initializer; + } + } + } + } + } + + return null; +} + +const fuse = new Fuse( + [ + 'client.embeddings.create', + 'client.classifications.universal.create', + 'client.rerankings.create', + 'client.extractions.qa.create', + ], + { 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 WorkerInput; + if (code == null) { + return Response.json( + { + message: + 'The code param is missing. Provide one containing a top-level `run` function. Write code within this template:\n\n```\nasync function run(client) {\n // Fill this out\n}\n```', + logLines: [], + errLines: [], + } satisfies WorkerError, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const runFunctionNode = getRunFunctionNode(code); + if (!runFunctionNode) { + return Response.json( + { + message: + 'The code is missing a top-level `run` function. Write code within this template:\n\n```\nasync function run(client) {\n // Fill this out\n}\n```', + logLines: [], + errLines: [], + } satisfies WorkerError, + { status: 400, statusText: 'Code execution error' }, + ); + } + const client = new Isaacus({ ...opts, }); @@ -22,21 +182,19 @@ const fetch = async (req: Request): Promise => { }; try { let run_ = async (client: any) => {}; - eval(` - ${code} - run_ = run; - `); - const result = await run_(client); + eval(`${code}\nrun_ = run;`); + const result = await run_(makeSdkProxy(client, { path: ['client'] })); return Response.json({ result, logLines, errLines, } satisfies WorkerSuccess); } catch (e) { - const message = e instanceof Error ? e.message : undefined; return Response.json( { - message, + message: parseError(code, e), + logLines, + errLines, } satisfies WorkerError, { status: 400, statusText: 'Code execution error' }, ); diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 61049d9..958a9ed 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -3,7 +3,7 @@ import { dirname } from 'node:path'; import { pathToFileURL } from 'node:url'; import Isaacus, { ClientOptions } from 'isaacus'; -import { Endpoint, ContentBlock, Metadata } from './tools/types'; +import { ContentBlock, Endpoint, Metadata, ToolCallResult } from './tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -12,7 +12,7 @@ import { WorkerInput, WorkerError, WorkerSuccess } from './code-tool-types'; /** * A tool that runs code against a copy of the SDK. * - * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once, + * Instead of exposing every endpoint as its own tool, which uses up too many tokens for LLMs to use at once, * 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. * @@ -23,7 +23,7 @@ export async function codeTool(): Promise { const tool: Tool = { name: 'execute', description: - 'Runs Typescript code to interact with the API.\nYou are a skilled programmer writing code to interface with the service.\nDefine an async function named "run" that takes a single parameter of an initialized client, and it will be run.\nDo not initialize a client, but instead use the client that you are given as a parameter.\nYou will be returned anything that your function returns, plus the results of any console.log statements.\nIf any code triggers an error, the tool will return an error response, so you do not need to add error handling unless you want to output something more helpful than the raw error.\nIt is not necessary to add comments to code, unless by adding those comments you believe that you can generate better code.\nThis code will run in a container, and you will not be able to use fetch or otherwise interact with the network calls other than through the client you are given.\nAny variables you define won\'t live between successive uses of this call, so make sure to return or log any data you might need later.', + 'Runs JavaScript code to interact with the API.\n\nYou are a skilled programmer writing code to interface with the service.\nDefine an async function named "run" that takes a single parameter of an initialized client named "client", and it will be run.\nWrite code within this template:\n\n```\nasync function run(client) {\n // Fill this out\n}\n```\n\nYou will be returned anything that your function returns, plus the results of any console.log statements.\nIf any code triggers an error, the tool will return an error response, so you do not need to add error handling unless you want to output something more helpful than the raw error.\nIt is not necessary to add comments to code, unless by adding those comments you believe that you can generate better code.\nThis code will run in a container, and you will not be able to use fetch or otherwise interact with the network calls other than through the client you are given.\nAny variables you define won\'t live between successive uses of this call, so make sure to return or log any data you might need later.', inputSchema: { type: 'object', properties: { code: { type: 'string' } } }, }; @@ -31,7 +31,7 @@ export async function codeTool(): Promise { const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker'); const { workerPath } = await import('./code-tool-paths.cjs'); - const handler = async (client: Isaacus, args: unknown) => { + const handler = async (client: Isaacus, args: unknown): Promise => { const baseURLHostname = new URL(client.baseURL).hostname; const { code } = args as { code: string }; @@ -97,7 +97,7 @@ export async function codeTool(): Promise { } satisfies WorkerInput); req.write(body, (err) => { - if (err !== null && err !== undefined) { + if (err != null) { reject(err); } }); @@ -108,12 +108,12 @@ export async function codeTool(): Promise { if (resp.status === 200) { const { result, logLines, errLines } = (await resp.json()) as WorkerSuccess; const returnOutput: ContentBlock | null = - result === null ? null - : result === undefined ? null - : { + result == null ? null : ( + { type: 'text', - text: typeof result === 'string' ? (result as string) : JSON.stringify(result), - }; + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); const logOutput: ContentBlock | null = logLines.length === 0 ? null @@ -132,11 +132,33 @@ export async function codeTool(): Promise { content: [returnOutput, logOutput, errOutput].filter((block) => block !== null), }; } else { - const { message } = (await resp.json()) as WorkerError; - throw new Error(message); + const { message, logLines, errLines } = (await resp.json()) as WorkerError; + const messageOutput: ContentBlock | null = + message == null ? null : ( + { + type: 'text', + text: message, + } + ); + const logOutput: ContentBlock | null = + logLines.length === 0 ? + null + : { + type: 'text', + text: logLines.join('\n'), + }; + const errOutput: ContentBlock | null = + errLines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + errLines.join('\n'), + }; + return { + content: [messageOutput, logOutput, errOutput].filter((block) => block !== null), + isError: true, + }; } - } catch (e) { - throw e; } finally { worker.terminate(); } diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index e28e583..b090539 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -13,8 +13,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'search_docs', - description: - 'Search for documentation for how to use the client to interact with the API.\nThe tool will return an array of Markdown-formatted documentation pages.', + description: 'Search for documentation for how to use the client to interact with the API.', inputSchema: { type: 'object', properties: { @@ -25,7 +24,12 @@ export const tool: Tool = { language: { type: 'string', description: 'The language for the SDK to search for.', - enum: ['http', 'python', 'go', 'typescript', 'terraform', 'ruby', 'java', 'kotlin'], + enum: ['http', 'python', 'go', 'typescript', 'javascript', 'terraform', 'ruby', 'java', 'kotlin'], + }, + detail: { + type: 'string', + description: 'The amount of detail to return.', + enum: ['default', 'verbose'], }, }, required: ['query', 'language'], diff --git a/packages/mcp-server/src/dynamic-tools.ts b/packages/mcp-server/src/dynamic-tools.ts index 63f31f6..82e70f7 100644 --- a/packages/mcp-server/src/dynamic-tools.ts +++ b/packages/mcp-server/src/dynamic-tools.ts @@ -14,7 +14,7 @@ function zodToInputSchema(schema: z.ZodSchema) { /** * A list of tools that expose all the endpoints in the API dynamically. * - * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once, + * Instead of exposing every endpoint as its own tool, which uses up too many tokens for LLMs to use at once, * 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. * diff --git a/packages/mcp-server/src/filtering.ts b/packages/mcp-server/src/filtering.ts index 1aa9a40..eaae0fc 100644 --- a/packages/mcp-server/src/filtering.ts +++ b/packages/mcp-server/src/filtering.ts @@ -12,3 +12,7 @@ export async function maybeFilter(jqFilter: unknown | undefined, response: any): async function jq(json: any, jqFilter: string) { return (await initJq).json(json, jqFilter); } + +export function isJqError(error: any): error is Error { + return error instanceof Error && 'stderr' in error; +} diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index c536d7b..2ac2859 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -11,7 +11,9 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial = case 'Bearer': return { apiKey: req.headers.authorization.slice('Bearer '.length) }; default: - throw new Error(`Unsupported authorization scheme`); + throw new Error( + 'Unsupported authorization scheme. Expected the "Authorization" header to be a supported scheme (Bearer).', + ); } } diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index ec34ab4..8451700 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -46,12 +46,12 @@ const newServer = ({ }, mcpOptions, }); - } catch { + } catch (error) { res.status(401).json({ jsonrpc: '2.0', error: { code: -32000, - message: 'Unauthorized', + message: `Unauthorized: ${error instanceof Error ? error.message : error}`, }, }); return null; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 4fe3b98..b6ff597 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -284,8 +284,10 @@ const coerceArray = (zodType: T) => ); const QueryOptions = z.object({ - tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Use dynamic tools or all tools'), - no_tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Do not use dynamic tools or all tools'), + tools: coerceArray(z.enum(['dynamic', 'all', 'code', 'docs'])).describe('Specify which MCP tools to use'), + no_tools: coerceArray(z.enum(['dynamic', 'all', 'code', 'docs'])).describe( + 'Specify which MCP tools to not use.', + ), tool: coerceArray(z.string()).describe('Include tools matching the specified names'), resource: coerceArray(z.string()).describe('Include tools matching the specified resources'), operation: coerceArray(z.enum(['read', 'write'])).describe( @@ -385,11 +387,16 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M : queryOptions.tools?.includes('docs') ? true : defaultOptions.includeDocsTools; + let codeTools: boolean | undefined = + queryOptions.no_tools && queryOptions.no_tools?.includes('code') ? false + : queryOptions.tools?.includes('code') && defaultOptions.includeCodeTools ? true + : defaultOptions.includeCodeTools; + return { client: queryOptions.client ?? defaultOptions.client, includeDynamicTools: dynamicTools, includeAllTools: allTools, - includeCodeTools: undefined, + includeCodeTools: codeTools, includeDocsTools: docsTools, filters, capabilities: clientCapabilities, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index bef0e7d..b073490 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -34,7 +34,7 @@ export const newMcpServer = () => new McpServer( { name: 'isaacus_api', - version: '0.11.1', + version: '0.12.0', }, { capabilities: { tools: {}, logging: {} } }, ); diff --git a/packages/mcp-server/src/tools/classifications/universal/create-classifications-universal.ts b/packages/mcp-server/src/tools/classifications/universal/create-classifications-universal.ts index fe35998..17575ef 100644 --- a/packages/mcp-server/src/tools/classifications/universal/create-classifications-universal.ts +++ b/packages/mcp-server/src/tools/classifications/universal/create-classifications-universal.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from 'isaacus-mcp/filtering'; -import { Metadata, asTextContentResult } from 'isaacus-mcp/tools/types'; +import { isJqError, maybeFilter } from 'isaacus-mcp/filtering'; +import { Metadata, asErrorResult, asTextContentResult } from 'isaacus-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import Isaacus from 'isaacus'; @@ -89,9 +89,16 @@ export const tool: Tool = { export const handler = async (client: Isaacus, args: Record | undefined) => { const { jq_filter, ...body } = args as any; - return asTextContentResult( - await maybeFilter(jq_filter, await client.classifications.universal.create(body)), - ); + try { + return asTextContentResult( + await maybeFilter(jq_filter, await client.classifications.universal.create(body)), + ); + } catch (error) { + if (error instanceof Isaacus.APIError || isJqError(error)) { + return asErrorResult(error.message); + } + throw error; + } }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/embeddings/create-embeddings.ts b/packages/mcp-server/src/tools/embeddings/create-embeddings.ts index 0c2bcb3..f29f478 100644 --- a/packages/mcp-server/src/tools/embeddings/create-embeddings.ts +++ b/packages/mcp-server/src/tools/embeddings/create-embeddings.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from 'isaacus-mcp/filtering'; -import { Metadata, asTextContentResult } from 'isaacus-mcp/tools/types'; +import { isJqError, maybeFilter } from 'isaacus-mcp/filtering'; +import { Metadata, asErrorResult, asTextContentResult } from 'isaacus-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import Isaacus from 'isaacus'; @@ -75,7 +75,14 @@ export const tool: Tool = { export const handler = async (client: Isaacus, args: Record | undefined) => { const { jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.embeddings.create(body))); + try { + return asTextContentResult(await maybeFilter(jq_filter, await client.embeddings.create(body))); + } catch (error) { + if (error instanceof Isaacus.APIError || isJqError(error)) { + return asErrorResult(error.message); + } + throw error; + } }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/extractions/qa/create-extractions-qa.ts b/packages/mcp-server/src/tools/extractions/qa/create-extractions-qa.ts index 3bb9f63..ba67746 100644 --- a/packages/mcp-server/src/tools/extractions/qa/create-extractions-qa.ts +++ b/packages/mcp-server/src/tools/extractions/qa/create-extractions-qa.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from 'isaacus-mcp/filtering'; -import { Metadata, asTextContentResult } from 'isaacus-mcp/tools/types'; +import { isJqError, maybeFilter } from 'isaacus-mcp/filtering'; +import { Metadata, asErrorResult, asTextContentResult } from 'isaacus-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import Isaacus from 'isaacus'; @@ -88,7 +88,14 @@ export const tool: Tool = { export const handler = async (client: Isaacus, args: Record | undefined) => { const { jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.extractions.qa.create(body))); + try { + return asTextContentResult(await maybeFilter(jq_filter, await client.extractions.qa.create(body))); + } catch (error) { + if (error instanceof Isaacus.APIError || isJqError(error)) { + return asErrorResult(error.message); + } + throw error; + } }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/rerankings/create-rerankings.ts b/packages/mcp-server/src/tools/rerankings/create-rerankings.ts index 3a0ee04..703695e 100644 --- a/packages/mcp-server/src/tools/rerankings/create-rerankings.ts +++ b/packages/mcp-server/src/tools/rerankings/create-rerankings.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from 'isaacus-mcp/filtering'; -import { Metadata, asTextContentResult } from 'isaacus-mcp/tools/types'; +import { isJqError, maybeFilter } from 'isaacus-mcp/filtering'; +import { Metadata, asErrorResult, asTextContentResult } from 'isaacus-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import Isaacus from 'isaacus'; @@ -93,7 +93,14 @@ export const tool: Tool = { export const handler = async (client: Isaacus, args: Record | undefined) => { const { jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.rerankings.create(body))); + try { + return asTextContentResult(await maybeFilter(jq_filter, await client.rerankings.create(body))); + } catch (error) { + if (error instanceof Isaacus.APIError || isJqError(error)) { + return asErrorResult(error.message); + } + throw error; + } }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/types.ts b/packages/mcp-server/src/tools/types.ts index 5b7c5ba..4c5eda4 100644 --- a/packages/mcp-server/src/tools/types.ts +++ b/packages/mcp-server/src/tools/types.ts @@ -87,6 +87,18 @@ export async function asBinaryContentResult(response: Response): Promise { const defaultOptions = { client: undefined, includeDynamicTools: undefined, + includeCodeTools: undefined, includeAllTools: undefined, filters: [], capabilities: { @@ -383,6 +384,27 @@ describe('parseQueryOptions', () => { { type: 'tool', op: 'exclude', value: 'exclude-tool' }, ]); }); + + it('code tools are enabled on http servers with default option set', () => { + const query = 'tools=code'; + const result = parseQueryOptions({ ...defaultOptions, includeCodeTools: true }, query); + + expect(result.includeCodeTools).toBe(true); + }); + + it('code tools are prevented on http servers when no default option set', () => { + const query = 'tools=code'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.includeCodeTools).toBe(undefined); + }); + + it('code tools are prevented on http servers when default option is explicitly false', () => { + const query = 'tools=code'; + const result = parseQueryOptions({ ...defaultOptions, includeCodeTools: false }, query); + + expect(result.includeCodeTools).toBe(false); + }); }); describe('parseEmbeddedJSON', () => { diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock index 966d057..2bb21c6 100644 --- a/packages/mcp-server/yarn.lock +++ b/packages/mcp-server/yarn.lock @@ -2494,9 +2494,9 @@ jest@^29.4.0: import-local "^3.0.2" jest-cli "^29.7.0" -"jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz": - version "0.8.6" - resolved "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz#14d0e126987736e82e964d675c3838b5944faa6f" +"jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz": + version "0.8.8" + resolved "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz#7849ef64bdfc28f70cbfc9888f886860e96da10d" js-tokens@^4.0.0: version "4.0.0" diff --git a/src/client.ts b/src/client.ts index 4ac2a40..d00abf4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -117,7 +117,7 @@ export class Isaacus { baseURL: string; maxRetries: number; timeout: number; - logger: Logger | undefined; + logger: Logger; logLevel: LogLevel | undefined; fetchOptions: MergedRequestInit | undefined; diff --git a/src/version.ts b/src/version.ts index 945825f..ce6b899 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.11.1'; // x-release-please-version +export const VERSION = '0.12.0'; // x-release-please-version