From f1321982b1b3ed64efc85d60e6704f43696e5e1b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:14:23 +0000 Subject: [PATCH 01/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 58a5c1f..ff342c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: d9c1f7b95d5659724df3e026c4fab291 +config_hash: a09a453fc58f48d89b5b8fce49bfb354 From d0216df26fc70e2dceacdd1384704d207e2497e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:32:30 +0000 Subject: [PATCH 02/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ff342c7..a8acd5d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: a09a453fc58f48d89b5b8fce49bfb354 +config_hash: 3b87fe01dc5604f4c7ae72e273509802 From 0b57412d43dbe610e2d54251b51dcb237913c614 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:07:23 +0000 Subject: [PATCH 03/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a8acd5d..aa8116c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 3b87fe01dc5604f4c7ae72e273509802 +config_hash: 05034d55bf9f8573b1160f7825bcd9b9 From f38ffbf0338d20a1ffa9cfc7ab62ddcaf01fa9a1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:07:51 +0000 Subject: [PATCH 04/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index aa8116c..af747c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 05034d55bf9f8573b1160f7825bcd9b9 +config_hash: d34508a94d94e1155705c01231bd6f17 From 1ccc8682168a50e3baea23eccab4e5f2976f80c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:10:53 +0000 Subject: [PATCH 05/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index af747c5..0caf7fc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: d34508a94d94e1155705c01231bd6f17 +config_hash: 0e1291f316b20497ad29b59a231a8680 From e822941c562c663aafeae27a9b5f8553bd5f1a02 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:01:16 +0000 Subject: [PATCH 06/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0caf7fc..6566ecd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 0e1291f316b20497ad29b59a231a8680 +config_hash: 7e37f38c64335d64bc0f2bdd0a655a97 From cd89c83c157db748ce25b9b2cb6c8ddc8e82f2df Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:02:58 +0000 Subject: [PATCH 07/21] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 6566ecd..92721c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 7e37f38c64335d64bc0f2bdd0a655a97 +config_hash: cb5d75abef6264b5d86448caf7295afa From 1f6e085c4e7e9edf497585587fc3e6603b53a3e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 03:07:29 +0000 Subject: [PATCH 08/21] feat(mcp): parse query string as mcp client options in mcp server --- packages/mcp-server/README.md | 13 ++ packages/mcp-server/package.json | 8 +- packages/mcp-server/src/compat.ts | 4 +- packages/mcp-server/src/http.ts | 25 ++- packages/mcp-server/src/index.ts | 4 +- packages/mcp-server/src/options.ts | 121 ++++++++++- packages/mcp-server/tests/options.test.ts | 240 +++++++++++++++++++++- packages/mcp-server/yarn.lock | 116 +++++++++-- 8 files changed, 499 insertions(+), 32 deletions(-) diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 1e3c6ee..6dec8e6 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -150,6 +150,19 @@ A configuration JSON for this server might look like this, assuming the server i } ``` +The command-line arguments for filtering tools and specifying clients can also be used as query parameters in the URL. +For example, to exclude specific tools while including others, use the URL: + +``` +http://localhost:3000?resource=cards&resource=accounts&no_tool=create_cards +``` + +Or, to configure for the Cursor client, with a custom max tool name length, use the URL: + +``` +http://localhost:3000?client=cursor&capability=tool-name-length%3D40 +``` + ## Importing the tools and server individually ```js diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 7b10b87..d1e3e6c 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -28,20 +28,22 @@ }, "dependencies": { "cas-parser-node": "file:../../dist/", + "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.11.5", "express": "^5.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz", + "qs": "^6.14.0", "yargs": "^17.7.2", - "@cloudflare/cabidela": "^0.2.4", "zod": "^3.25.20", - "zod-to-json-schema": "^3.24.5" + "zod-to-json-schema": "^3.24.5", + "zod-validation-error": "^4.0.1" }, "bin": { "mcp-server": "dist/index.js" }, "devDependencies": { - "@types/jest": "^29.4.0", "@types/express": "^5.0.3", + "@types/jest": "^29.4.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", diff --git a/packages/mcp-server/src/compat.ts b/packages/mcp-server/src/compat.ts index 7afd77f..1df7a7a 100644 --- a/packages/mcp-server/src/compat.ts +++ b/packages/mcp-server/src/compat.ts @@ -1,4 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; import { Endpoint } from './tools'; export interface ClientCapabilities { @@ -19,7 +20,8 @@ export const defaultClientCapabilities: ClientCapabilities = { toolNameLength: undefined, }; -export type ClientType = 'openai-agents' | 'claude' | 'claude-code' | 'cursor'; +export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor']); +export type ClientType = z.infer; // Client presets for compatibility // Note that these could change over time as models get better, so this is diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index e188c9e..ca6d3d2 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,13 +4,33 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; -import { McpOptions } from './options'; +import { fromError } from 'zod-validation-error/v3'; +import { McpOptions, parseQueryOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; import { Endpoint } from './tools'; -const newServer = (mcpOptions: McpOptions, req: express.Request, res: express.Response): McpServer | null => { +const newServer = ( + defaultMcpOptions: McpOptions, + req: express.Request, + res: express.Response, +): McpServer | null => { const server = newMcpServer(); + + let mcpOptions: McpOptions; + try { + mcpOptions = parseQueryOptions(defaultMcpOptions, req.query); + } catch (error) { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: `Invalid request: ${fromError(error)}`, + }, + }); + return null; + } + try { const authOptions = parseAuthHeaders(req); initMcpServer({ @@ -71,6 +91,7 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = (options: McpOptions): express.Express => { const app = express(); + app.set('query parser', 'extended'); app.use(express.json()); app.get('/', get); diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 4c71a3b..05b2ba6 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -2,7 +2,7 @@ import { selectTools } from './server'; import { Endpoint, endpoints } from './tools'; -import { McpOptions, parseOptions } from './options'; +import { McpOptions, parseCLIOptions } from './options'; import { launchStdioServer } from './stdio'; import { launchStreamableHTTPServer } from './http'; @@ -40,7 +40,7 @@ if (require.main === module) { function parseOptionsOrError() { try { - return parseOptions(); + return parseCLIOptions(); } catch (error) { console.error('Error parsing options:', error); process.exit(1); diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index c290ca5..0768d93 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,5 +1,7 @@ +import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import z from 'zod'; import { endpoints, Filter } from './tools'; import { ClientCapabilities, knownClients, ClientType } from './compat'; @@ -47,7 +49,7 @@ function parseCapabilityValue(cap: string): { name: Capability; value?: number } return { name: cap as Capability }; } -export function parseOptions(): CLIOptions { +export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) .option('tools', { type: 'string', @@ -271,6 +273,123 @@ export function parseOptions(): CLIOptions { }; } +const coerceArray = (zodType: T) => + z.preprocess( + (val) => + Array.isArray(val) ? val + : val ? [val] + : val, + z.array(zodType).optional(), + ); + +const QueryOptions = z.object({ + tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Use dynamic tools or all tools'), + no_tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Do not use dynamic tools or all tools'), + 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( + 'Include tools matching the specified operations', + ), + tag: coerceArray(z.string()).describe('Include tools with the specified tags'), + no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'), + no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'), + no_operation: coerceArray(z.enum(['read', 'write'])).describe( + 'Exclude tools matching the specified operations', + ), + no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'), + client: ClientType.optional().describe('Specify the MCP client being used'), + capability: coerceArray(z.string()).describe('Specify client capabilities'), + no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'), +}); + +export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions { + const queryObject = typeof query === 'string' ? qs.parse(query) : query; + const queryOptions = QueryOptions.parse(queryObject); + + const filters: Filter[] = [...defaultOptions.filters]; + + for (const resource of queryOptions.resource || []) { + filters.push({ type: 'resource', op: 'include', value: resource }); + } + for (const operation of queryOptions.operation || []) { + filters.push({ type: 'operation', op: 'include', value: operation }); + } + for (const tag of queryOptions.tag || []) { + filters.push({ type: 'tag', op: 'include', value: tag }); + } + for (const tool of queryOptions.tool || []) { + filters.push({ type: 'tool', op: 'include', value: tool }); + } + for (const resource of queryOptions.no_resource || []) { + filters.push({ type: 'resource', op: 'exclude', value: resource }); + } + for (const operation of queryOptions.no_operation || []) { + filters.push({ type: 'operation', op: 'exclude', value: operation }); + } + for (const tag of queryOptions.no_tag || []) { + filters.push({ type: 'tag', op: 'exclude', value: tag }); + } + for (const tool of queryOptions.no_tool || []) { + filters.push({ type: 'tool', op: 'exclude', value: tool }); + } + + // Parse client capabilities + const clientCapabilities: ClientCapabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + ...defaultOptions.capabilities, + }; + + for (const cap of queryOptions.capability || []) { + const parsed = parseCapabilityValue(cap); + if (parsed.name === 'top-level-unions') { + clientCapabilities.topLevelUnions = true; + } else if (parsed.name === 'valid-json') { + clientCapabilities.validJson = true; + } else if (parsed.name === 'refs') { + clientCapabilities.refs = true; + } else if (parsed.name === 'unions') { + clientCapabilities.unions = true; + } else if (parsed.name === 'formats') { + clientCapabilities.formats = true; + } else if (parsed.name === 'tool-name-length') { + clientCapabilities.toolNameLength = parsed.value; + } + } + + for (const cap of queryOptions.no_capability || []) { + if (cap === 'top-level-unions') { + clientCapabilities.topLevelUnions = false; + } else if (cap === 'valid-json') { + clientCapabilities.validJson = false; + } else if (cap === 'refs') { + clientCapabilities.refs = false; + } else if (cap === 'unions') { + clientCapabilities.unions = false; + } else if (cap === 'formats') { + clientCapabilities.formats = false; + } else if (cap === 'tool-name-length') { + clientCapabilities.toolNameLength = undefined; + } + } + + return { + client: queryOptions.client ?? defaultOptions.client, + includeDynamicTools: + defaultOptions.includeDynamicTools ?? + (queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic')), + includeAllTools: + defaultOptions.includeAllTools ?? + (queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all')), + filters, + capabilities: clientCapabilities, + }; +} + function getCapabilitiesExplanation(): string { return ` Client Capabilities Explanation: diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 264aca5..08ea1f1 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -1,4 +1,4 @@ -import { parseOptions } from '../src/options'; +import { parseCLIOptions, parseQueryOptions } from '../src/options'; import { Filter } from '../src/tools'; import { parseEmbeddedJSON } from '../src/compat'; @@ -11,7 +11,7 @@ const mockArgv = (args: string[]) => { }; }; -describe('parseOptions', () => { +describe('parseCLIOptions', () => { it('should parse basic filter options', () => { const cleanup = mockArgv([ '--tool=test-tool', @@ -20,7 +20,7 @@ describe('parseOptions', () => { '--tag=test-tag', ]); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.filters).toEqual([ { type: 'tag', op: 'include', value: 'test-tag' }, @@ -52,7 +52,7 @@ describe('parseOptions', () => { '--no-tag=exclude-tag', ]); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.filters).toEqual([ { type: 'tag', op: 'exclude', value: 'exclude-tag' }, @@ -76,7 +76,7 @@ describe('parseOptions', () => { it('should parse client presets', () => { const cleanup = mockArgv(['--client=openai-agents']); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.client).toEqual('openai-agents'); @@ -92,7 +92,7 @@ describe('parseOptions', () => { '--capability=tool-name-length=40', ]); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.capabilities).toEqual({ topLevelUnions: true, @@ -109,7 +109,7 @@ describe('parseOptions', () => { it('should handle list option', () => { const cleanup = mockArgv(['--list']); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.list).toBe(true); @@ -119,7 +119,7 @@ describe('parseOptions', () => { it('should handle multiple filters of the same type', () => { const cleanup = mockArgv(['--tool=tool1', '--tool=tool2', '--resource=res1', '--resource=res2']); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.filters).toEqual([ { type: 'resource', op: 'include', value: 'res1' }, @@ -138,7 +138,7 @@ describe('parseOptions', () => { '--capability=top-level-unions,valid-json,unions', ]); - const result = parseOptions(); + const result = parseCLIOptions(); expect(result.filters).toEqual([ { type: 'resource', op: 'include', value: 'res1' }, @@ -166,7 +166,7 @@ describe('parseOptions', () => { const originalError = console.error; console.error = jest.fn(); - expect(() => parseOptions()).toThrow(); + expect(() => parseCLIOptions()).toThrow(); console.error = originalError; cleanup(); @@ -179,13 +179,231 @@ describe('parseOptions', () => { const originalError = console.error; console.error = jest.fn(); - expect(() => parseOptions()).toThrow(); + expect(() => parseCLIOptions()).toThrow(); console.error = originalError; cleanup(); }); }); +describe('parseQueryOptions', () => { + const defaultOptions = { + client: undefined, + includeDynamicTools: undefined, + includeAllTools: undefined, + filters: [], + capabilities: { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }, + }; + + it('should parse basic filter options from query string', () => { + const query = 'tool=test-tool&resource=test-resource&operation=read&tag=test-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'test-resource' }, + { type: 'operation', op: 'include', value: 'read' }, + { type: 'tag', op: 'include', value: 'test-tag' }, + { type: 'tool', op: 'include', value: 'test-tool' }, + ]); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }); + }); + + it('should parse exclusion filters from query string', () => { + const query = 'no_tool=exclude-tool&no_resource=exclude-resource&no_operation=write&no_tag=exclude-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'exclude', value: 'exclude-resource' }, + { type: 'operation', op: 'exclude', value: 'write' }, + { type: 'tag', op: 'exclude', value: 'exclude-tag' }, + { type: 'tool', op: 'exclude', value: 'exclude-tool' }, + ]); + }); + + it('should parse client option from query string', () => { + const query = 'client=openai-agents'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.client).toBe('openai-agents'); + }); + + it('should parse client capabilities from query string', () => { + const query = 'capability=top-level-unions&capability=valid-json&capability=tool-name-length%3D40'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: 40, + }); + }); + + it('should parse no-capability options from query string', () => { + const query = 'no_capability=top-level-unions&no_capability=refs&no_capability=formats'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: false, + validJson: true, + refs: false, + unions: true, + formats: false, + toolNameLength: undefined, + }); + }); + + it('should parse tools options from query string', () => { + const query = 'tools=dynamic&tools=all'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.includeDynamicTools).toBe(true); + expect(result.includeAllTools).toBe(true); + }); + + it('should parse no-tools options from query string', () => { + const query = 'tools=dynamic&tools=all&no_tools=dynamic'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.includeDynamicTools).toBe(false); + expect(result.includeAllTools).toBe(true); + }); + + it('should handle array values in query string', () => { + const query = 'tool[]=tool1&tool[]=tool2&resource[]=res1&resource[]=res2'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'res1' }, + { type: 'resource', op: 'include', value: 'res2' }, + { type: 'tool', op: 'include', value: 'tool1' }, + { type: 'tool', op: 'include', value: 'tool2' }, + ]); + }); + + it('should merge with default options', () => { + const defaultWithFilters = { + ...defaultOptions, + filters: [{ type: 'tag' as const, op: 'include' as const, value: 'existing-tag' }], + client: 'cursor' as const, + includeDynamicTools: true, + }; + + const query = 'tool=new-tool&resource=new-resource'; + const result = parseQueryOptions(defaultWithFilters, query); + + expect(result.filters).toEqual([ + { type: 'tag', op: 'include', value: 'existing-tag' }, + { type: 'resource', op: 'include', value: 'new-resource' }, + { type: 'tool', op: 'include', value: 'new-tool' }, + ]); + + expect(result.client).toBe('cursor'); + expect(result.includeDynamicTools).toBe(true); + }); + + it('should override client from default options', () => { + const defaultWithClient = { + ...defaultOptions, + client: 'cursor' as const, + }; + + const query = 'client=openai-agents'; + const result = parseQueryOptions(defaultWithClient, query); + + expect(result.client).toBe('openai-agents'); + }); + + it('should merge capabilities with default options', () => { + const defaultWithCapabilities = { + ...defaultOptions, + capabilities: { + topLevelUnions: false, + validJson: false, + refs: true, + unions: true, + formats: true, + toolNameLength: 30, + }, + }; + + const query = 'capability=top-level-unions&no_capability=refs'; + const result = parseQueryOptions(defaultWithCapabilities, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: false, + refs: false, + unions: true, + formats: true, + toolNameLength: 30, + }); + }); + + it('should handle empty query string', () => { + const query = ''; + const result = parseQueryOptions(defaultOptions, query); + + expect(result).toEqual(defaultOptions); + }); + + it('should handle invalid query string gracefully', () => { + const query = 'invalid=value&operation=invalid-operation'; + + // Should throw due to Zod validation for invalid operation + expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); + }); + + it('should preserve default undefined values when not specified', () => { + const defaultWithUndefined = { + ...defaultOptions, + client: undefined, + includeDynamicTools: undefined, + includeAllTools: undefined, + }; + + const query = 'tool=test-tool'; + const result = parseQueryOptions(defaultWithUndefined, query); + + expect(result.client).toBeUndefined(); + expect(result.includeDynamicTools).toBeFalsy(); + expect(result.includeAllTools).toBeFalsy(); + }); + + it('should handle complex query with mixed include and exclude filters', () => { + const query = + 'tool=include-tool&no_tool=exclude-tool&resource=include-res&no_resource=exclude-res&operation=read&tag=include-tag&no_tag=exclude-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'include-res' }, + { type: 'operation', op: 'include', value: 'read' }, + { type: 'tag', op: 'include', value: 'include-tag' }, + { type: 'tool', op: 'include', value: 'include-tool' }, + { type: 'resource', op: 'exclude', value: 'exclude-res' }, + { type: 'tag', op: 'exclude', value: 'exclude-tag' }, + { type: 'tool', op: 'exclude', value: 'exclude-tool' }, + ]); + }); +}); + describe('parseEmbeddedJSON', () => { it('should not change non-string values', () => { const args = { diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock index 9970ec3..707a2de 100644 --- a/packages/mcp-server/yarn.lock +++ b/packages/mcp-server/yarn.lock @@ -584,15 +584,17 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@modelcontextprotocol/sdk@^1.6.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz#c7f4a1432872ef10130f5d9b0072060c17a3946b" - integrity sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ== +"@modelcontextprotocol/sdk@^1.11.5": + version "1.17.3" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz#cf92354220f0183d28179e96a9bf3a8f6d3211ae" + integrity sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg== dependencies: + ajv "^6.12.6" content-type "^1.0.5" cors "^2.8.5" - cross-spawn "^7.0.3" + cross-spawn "^7.0.5" eventsource "^3.0.2" + eventsource-parser "^3.0.0" express "^5.0.1" express-rate-limit "^7.5.0" pkce-challenge "^5.0.0" @@ -708,6 +710,40 @@ dependencies: "@babel/types" "^7.20.7" +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -715,6 +751,11 @@ dependencies: "@types/node" "*" +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" @@ -742,6 +783,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/node@*": version "22.15.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055" @@ -749,6 +795,33 @@ dependencies: undici-types "~6.21.0" +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" @@ -885,7 +958,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.12.4: +ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1246,7 +1319,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1514,6 +1587,11 @@ etag@^1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +eventsource-parser@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.3.tgz#e9af1d40b77e6268cdcbc767321e8b9f066adea8" + integrity sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA== + eventsource-parser@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.1.tgz#5e358dba9a55ba64ca90da883c4ca35bd82467bd" @@ -1562,7 +1640,7 @@ express-rate-limit@^7.5.0: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz#6a67990a724b4fbbc69119419feef50c51e8b28f" integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== -express@^5.0.1: +express@^5.0.1, express@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== @@ -2404,6 +2482,10 @@ 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" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3305,9 +3387,9 @@ ts-node@^10.5.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -"tsc-multi@https://github.com/stainless-api/tsc-multi/releases/download/v1.1.7/tsc-multi.tgz": - version "1.1.7" - resolved "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.7/tsc-multi.tgz#52f40adf8b808bd0b633346d11cc4a8aeea465cd" +"tsc-multi@https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz": + version "1.1.8" + resolved "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz#f544b359b8f05e607771ffacc280e58201476b04" dependencies: debug "^4.3.7" fast-glob "^3.3.2" @@ -3508,7 +3590,17 @@ zod-to-json-schema@^3.24.1, zod-to-json-schema@^3.24.5: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== -zod@^3.23.8, zod@^3.24.4: +zod-validation-error@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.1.tgz#a105723eb40299578a6a38cb86647068f6d005b1" + integrity sha512-F3rdaCOHs5ViJ5YTz5zzRtfkQdMdIeKudJAoxy7yB/2ZMEHw73lmCAcQw11r7++20MyGl4WV59EVh7A9rNAyog== + +zod@^3.23.8: version "3.24.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f" integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== + +zod@^3.25.20: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== From 00cc94fb85746fc3a79136d22214864673a3c93f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 03:09:50 +0000 Subject: [PATCH 09/21] chore(internal): refactor array check --- packages/mcp-server/src/headers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index e973883..1d7399c 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -6,6 +6,6 @@ import { IncomingMessage } from 'node:http'; export const parseAuthHeaders = (req: IncomingMessage): Partial => { const apiKey = - req.headers['x-api-key'] instanceof Array ? req.headers['x-api-key'][0] : req.headers['x-api-key']; + Array.isArray(req.headers['x-api-key']) ? req.headers['x-api-key'][0] : req.headers['x-api-key']; return { apiKey }; }; From 3833155c5d3bcca5a7bf6ca0f513e8365db316cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:32:01 +0000 Subject: [PATCH 10/21] chore(mcp): add cors to oauth metadata route --- packages/mcp-server/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index d1e3e6c..778b898 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -30,6 +30,7 @@ "cas-parser-node": "file:../../dist/", "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.11.5", + "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", "qs": "^6.14.0", @@ -42,6 +43,7 @@ "mcp-server": "dist/index.js" }, "devDependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", "@types/yargs": "^17.0.8", From 84904a7f558053aa211b8f483a7b4f6c2b13b8f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:34:32 +0000 Subject: [PATCH 11/21] feat(mcp): add code execution tool --- package.json | 2 +- packages/mcp-server/package.json | 3 +- packages/mcp-server/src/code-tool-paths.cts | 3 + packages/mcp-server/src/code-tool-types.ts | 14 ++ packages/mcp-server/src/code-tool-worker.ts | 46 ++++++ packages/mcp-server/src/code-tool.ts | 146 ++++++++++++++++++++ packages/mcp-server/src/options.ts | 18 ++- packages/mcp-server/src/server.ts | 3 + yarn.lock | 7 +- 9 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 packages/mcp-server/src/code-tool-paths.cts create mode 100644 packages/mcp-server/src/code-tool-types.ts create mode 100644 packages/mcp-server/src/code-tool-worker.ts create mode 100644 packages/mcp-server/src/code-tool.ts diff --git a/package.json b/package.json index aa24a10..89a4ff3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "publint": "^0.2.12", "ts-jest": "^29.1.0", "ts-node": "^10.5.0", - "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz", + "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", "typescript-eslint": "8.31.1" diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 778b898..639874f 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -30,6 +30,7 @@ "cas-parser-node": "file:../../dist/", "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.11.5", + "@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", @@ -57,7 +58,7 @@ "ts-jest": "^29.1.0", "ts-morph": "^19.0.0", "ts-node": "^10.5.0", - "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz", + "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" }, diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts new file mode 100644 index 0000000..15ce7f5 --- /dev/null +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const workerPath = require.resolve('./code-tool-worker.mjs'); diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts new file mode 100644 index 0000000..a30ed96 --- /dev/null +++ b/packages/mcp-server/src/code-tool-types.ts @@ -0,0 +1,14 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { type ClientOptions } from 'cas-parser-node/client'; + +export type WorkerInput = { + opts: ClientOptions; + code: string; +}; +export type WorkerSuccess = { + result: unknown | null; + logLines: string[]; + errLines: string[]; +}; +export type WorkerError = { message: string | undefined }; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts new file mode 100644 index 0000000..f34d5ab --- /dev/null +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import util from 'node:util'; +import { WorkerInput, WorkerSuccess, WorkerError } from './code-tool-types'; +import { CasParser } from 'cas-parser-node'; + +const fetch = async (req: Request): Promise => { + const { opts, code } = (await req.json()) as WorkerInput; + const client = new CasParser({ + ...opts, + }); + + const logLines: string[] = []; + const errLines: string[] = []; + const console = { + log: (...args: unknown[]) => { + logLines.push(util.format(...args)); + }, + error: (...args: unknown[]) => { + errLines.push(util.format(...args)); + }, + }; + try { + let run_ = async (client: any) => {}; + eval(` + ${code} + run_ = run; + `); + const result = await run_(client); + return Response.json({ + result, + logLines, + errLines, + } satisfies WorkerSuccess); + } catch (e) { + const message = e instanceof Error ? e.message : undefined; + return Response.json( + { + message, + } satisfies WorkerError, + { status: 400, statusText: 'Code execution error' }, + ); + } +}; + +export default { fetch }; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts new file mode 100644 index 0000000..faae8f3 --- /dev/null +++ b/packages/mcp-server/src/code-tool.ts @@ -0,0 +1,146 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { type ClientOptions } from 'cas-parser-node/client'; + +import { dirname } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import CasParser from 'cas-parser-node'; +import { Endpoint, ContentBlock, Metadata } from './tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +import { newDenoHTTPWorker } from '@valtown/deno-http-worker'; +import { WorkerInput, WorkerError, WorkerSuccess } from './code-tool-types'; +import { workerPath } from './code-tool-paths.cjs'; + +/** + * 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, + * 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. + * + * @param endpoints - The endpoints to include in the list. + */ +export function codeTool(): Endpoint { + const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; + 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.', + inputSchema: { type: 'object', properties: { code: { type: 'string' } } }, + }; + + const handler = async (client: CasParser, args: unknown) => { + const baseURLHostname = new URL(client.baseURL).hostname; + const { code } = args as { code: string }; + + const worker = await newDenoHTTPWorker(pathToFileURL(workerPath), { + runFlags: [ + `--node-modules-dir=manual`, + `--allow-read=code-tool-worker.mjs,${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, + `--allow-net=${baseURLHostname}`, + // Allow environment variables because instantiating the client will try to read from them, + // even though they are not set. + '--allow-env', + ], + printOutput: true, + spawnOptions: { + cwd: dirname(workerPath), + }, + }); + + try { + const resp = await new Promise((resolve, reject) => { + worker.addEventListener('exit', (exitCode) => { + reject(new Error(`Worker exited with code ${exitCode}`)); + }); + + const opts: ClientOptions = { + baseURL: client.baseURL, + apiKey: client.apiKey, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }; + + const req = worker.request( + 'http://localhost', + { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }, + (resp) => { + const body: Uint8Array[] = []; + resp.on('error', (err) => { + reject(err); + }); + resp.on('data', (chunk) => { + body.push(chunk); + }); + resp.on('end', () => { + resolve( + new Response(Buffer.concat(body).toString(), { + status: resp.statusCode ?? 200, + headers: resp.headers as any, + }), + ); + }); + }, + ); + + const body = JSON.stringify({ + opts, + code, + } satisfies WorkerInput); + + req.write(body, (err) => { + if (err !== null && err !== undefined) { + reject(err); + } + }); + + req.end(); + }); + + if (resp.status === 200) { + const { result, logLines, errLines } = (await resp.json()) as WorkerSuccess; + const returnOutput: ContentBlock | null = + result === null ? null + : result === undefined ? null + : { + type: 'text', + text: typeof result === 'string' ? (result as string) : JSON.stringify(result), + }; + 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: [returnOutput, logOutput, errOutput].filter((block) => block !== null), + }; + } else { + const { message } = (await resp.json()) as WorkerError; + throw new Error(message); + } + } catch (e) { + throw e; + } finally { + worker.terminate(); + } + }; + + return { metadata, tool, handler }; +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 0768d93..a174591 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -16,6 +16,7 @@ export type McpOptions = { client: ClientType | undefined; includeDynamicTools: boolean | undefined; includeAllTools: boolean | undefined; + includeCodeTools: boolean | undefined; filters: Filter[]; capabilities?: Partial; }; @@ -54,13 +55,13 @@ export function parseCLIOptions(): CLIOptions { .option('tools', { type: 'string', array: true, - choices: ['dynamic', 'all'], + choices: ['dynamic', 'all', 'code'], description: 'Use dynamic tools or all tools', }) .option('no-tools', { type: 'string', array: true, - choices: ['dynamic', 'all'], + choices: ['dynamic', 'all', 'code'], description: 'Do not use any dynamic or all tools', }) .option('tool', { @@ -251,11 +252,13 @@ export function parseCLIOptions(): CLIOptions { } } + const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code') => + explicitTools ? argv.tools?.includes(toolType) && !argv.noTools?.includes(toolType) : undefined; + const explicitTools = Boolean(argv.tools || argv.noTools); - const includeDynamicTools = - explicitTools ? argv.tools?.includes('dynamic') && !argv.noTools?.includes('dynamic') : undefined; - const includeAllTools = - explicitTools ? argv.tools?.includes('all') && !argv.noTools?.includes('all') : undefined; + const includeDynamicTools = shouldIncludeToolType('dynamic'); + const includeAllTools = shouldIncludeToolType('all'); + const includeCodeTools = shouldIncludeToolType('code'); const transport = argv.transport as 'stdio' | 'http'; @@ -264,6 +267,7 @@ export function parseCLIOptions(): CLIOptions { client: client && knownClients[client] ? client : undefined, includeDynamicTools, includeAllTools, + includeCodeTools, filters, capabilities: clientCapabilities, list: argv.list || false, @@ -385,6 +389,8 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M includeAllTools: defaultOptions.includeAllTools ?? (queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all')), + // Never include code tools on remote server. + includeCodeTools: undefined, filters, capabilities: clientCapabilities, }; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 9a17e7f..0f54711 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -14,6 +14,7 @@ import { parseEmbeddedJSON, } from './compat'; import { dynamicTools } from './dynamic-tools'; +import { codeTool } from './code-tool'; import { McpOptions } from './options'; export { McpOptions } from './options'; @@ -116,6 +117,8 @@ export function selectTools(endpoints: Endpoint[], options: McpOptions): Endpoin includedTools = endpoints; } else if (options.includeDynamicTools) { includedTools = dynamicTools(endpoints); + } else if (options.includeCodeTools) { + includedTools = [codeTool()]; } else { includedTools = endpoints; } diff --git a/yarn.lock b/yarn.lock index fd164dc..8311caf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3283,10 +3283,9 @@ ts-node@^10.5.0: v8-compile-cache-lib "^3.0.0" yn "3.1.1" -"tsc-multi@https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz": - version "1.1.8" - uid f544b359b8f05e607771ffacc280e58201476b04 - resolved "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz#f544b359b8f05e607771ffacc280e58201476b04" +"tsc-multi@https://github.com/stainless-api/tsc-multi/releases/download/v1.1.9/tsc-multi.tgz": + version "1.1.9" + resolved "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.9/tsc-multi.tgz#777f6f5d9e26bf0e94e5170990dd3a841d6707cd" dependencies: debug "^4.3.7" fast-glob "^3.3.2" From 4bbdb3749a70bc35a4e9fdd4b9763a6f79f78a24 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 04:12:55 +0000 Subject: [PATCH 12/21] chore(internal): make mcp-server publishing public by defaut --- packages/mcp-server/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 639874f..c686086 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -15,6 +15,9 @@ "license": "Apache-2.0", "packageManager": "yarn@1.22.22", "private": false, + "publishConfig": { + "access": "public" + }, "scripts": { "test": "jest", "build": "bash ./build", From c190ccdd5cb6342dc620c26a3d06c0514f693a37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 04:22:23 +0000 Subject: [PATCH 13/21] feat(mcp): add option to infer mcp client --- packages/mcp-server/src/compat.ts | 4 +- packages/mcp-server/src/http.ts | 7 +- packages/mcp-server/src/index.ts | 4 +- packages/mcp-server/src/options.ts | 46 +++++------- packages/mcp-server/src/server.ts | 85 ++++++++++++++--------- packages/mcp-server/src/stdio.ts | 7 +- packages/mcp-server/tests/options.test.ts | 25 +------ 7 files changed, 78 insertions(+), 100 deletions(-) diff --git a/packages/mcp-server/src/compat.ts b/packages/mcp-server/src/compat.ts index 1df7a7a..f84053c 100644 --- a/packages/mcp-server/src/compat.ts +++ b/packages/mcp-server/src/compat.ts @@ -20,13 +20,13 @@ export const defaultClientCapabilities: ClientCapabilities = { toolNameLength: undefined, }; -export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor']); +export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor', 'infer']); export type ClientType = z.infer; // Client presets for compatibility // Note that these could change over time as models get better, so this is // a best effort. -export const knownClients: Record = { +export const knownClients: Record, ClientCapabilities> = { 'openai-agents': { topLevelUnions: false, validJson: true, diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index ca6d3d2..c11185b 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -8,7 +8,6 @@ import { fromError } from 'zod-validation-error/v3'; import { McpOptions, parseQueryOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; -import { Endpoint } from './tools'; const newServer = ( defaultMcpOptions: McpOptions, @@ -101,11 +100,7 @@ export const streamableHTTPApp = (options: McpOptions): express.Express => { return app; }; -export const launchStreamableHTTPServer = async ( - options: McpOptions, - endpoints: Endpoint[], - port: number | string | undefined, -) => { +export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { const app = streamableHTTPApp(options); const server = app.listen(port); const address = server.address(); diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 05b2ba6..c450e4b 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -23,10 +23,10 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(options, selectedTools); + await launchStdioServer(options); break; case 'http': - await launchStreamableHTTPServer(options, selectedTools, options.port ?? options.socket); + await launchStreamableHTTPServer(options, options.port ?? options.socket); break; } } diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index a174591..9eb00b4 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -13,12 +13,12 @@ export type CLIOptions = McpOptions & { }; export type McpOptions = { - client: ClientType | undefined; - includeDynamicTools: boolean | undefined; - includeAllTools: boolean | undefined; - includeCodeTools: boolean | undefined; - filters: Filter[]; - capabilities?: Partial; + client?: ClientType | undefined; + includeDynamicTools?: boolean | undefined; + includeAllTools?: boolean | undefined; + includeCodeTools?: boolean | undefined; + filters?: Filter[] | undefined; + capabilities?: Partial | undefined; }; const CAPABILITY_CHOICES = [ @@ -204,14 +204,7 @@ export function parseCLIOptions(): CLIOptions { } // Parse client capabilities - const clientCapabilities: ClientCapabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }; + const clientCapabilities: Partial = {}; // Apply individual capability overrides if (Array.isArray(argv.capability)) { @@ -264,7 +257,7 @@ export function parseCLIOptions(): CLIOptions { const client = argv.client as ClientType; return { - client: client && knownClients[client] ? client : undefined, + client: client && client !== 'infer' && knownClients[client] ? client : undefined, includeDynamicTools, includeAllTools, includeCodeTools, @@ -310,7 +303,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M const queryObject = typeof query === 'string' ? qs.parse(query) : query; const queryOptions = QueryOptions.parse(queryObject); - const filters: Filter[] = [...defaultOptions.filters]; + const filters: Filter[] = [...(defaultOptions.filters ?? [])]; for (const resource of queryOptions.resource || []) { filters.push({ type: 'resource', op: 'include', value: resource }); @@ -338,15 +331,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M } // Parse client capabilities - const clientCapabilities: ClientCapabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - ...defaultOptions.capabilities, - }; + const clientCapabilities: Partial = { ...defaultOptions.capabilities }; for (const cap of queryOptions.capability || []) { const parsed = parseCapabilityValue(cap); @@ -384,12 +369,13 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M return { client: queryOptions.client ?? defaultOptions.client, includeDynamicTools: - defaultOptions.includeDynamicTools ?? - (queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic')), + defaultOptions.includeDynamicTools === false ? + false + : queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic'), includeAllTools: - defaultOptions.includeAllTools ?? - (queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all')), - // Never include code tools on remote server. + defaultOptions.includeAllTools === false ? + false + : queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all'), includeCodeTools: undefined, filters, capabilities: clientCapabilities, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 0f54711..d99a492 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -3,7 +3,12 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { Endpoint, endpoints, HandlerFunction, query } from './tools'; -import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'; +import { + CallToolRequestSchema, + Implementation, + ListToolsRequestSchema, + Tool, +} from '@modelcontextprotocol/sdk/types.js'; import { ClientOptions } from 'cas-parser-node'; import CasParser from 'cas-parser-node'; import { @@ -41,29 +46,29 @@ export const server = newMcpServer(); */ export function initMcpServer(params: { server: Server | McpServer; - clientOptions: ClientOptions; - mcpOptions: McpOptions; - endpoints?: { tool: Tool; handler: HandlerFunction }[]; -}) { - const transformedEndpoints = selectTools(endpoints, params.mcpOptions); - const client = new CasParser(params.clientOptions); - const capabilities = { - ...defaultClientCapabilities, - ...(params.mcpOptions.client ? knownClients[params.mcpOptions.client] : params.mcpOptions.capabilities), - }; - init({ server: params.server, client, endpoints: transformedEndpoints, capabilities }); -} - -export function init(params: { - server: Server | McpServer; - client?: CasParser; - endpoints?: { tool: Tool; handler: HandlerFunction }[]; - capabilities?: Partial; + clientOptions?: ClientOptions; + mcpOptions?: McpOptions; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; - const providedEndpoints = params.endpoints || endpoints; - - const endpointMap = Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint])); + const mcpOptions = params.mcpOptions ?? {}; + + let providedEndpoints: Endpoint[] | null = null; + let endpointMap: Record | null = null; + + const initTools = (implementation?: Implementation) => { + if (implementation && (!mcpOptions.client || mcpOptions.client === 'infer')) { + mcpOptions.client = + implementation.name.toLowerCase().includes('claude') ? 'claude' + : implementation.name.toLowerCase().includes('cursor') ? 'cursor' + : undefined; + mcpOptions.capabilities = { + ...(mcpOptions.client && knownClients[mcpOptions.client]), + ...mcpOptions.capabilities, + }; + } + providedEndpoints = selectTools(endpoints, mcpOptions); + endpointMap = Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint])); + }; const logAtLevel = (level: 'debug' | 'info' | 'warning' | 'error') => @@ -80,51 +85,63 @@ export function init(params: { error: logAtLevel('error'), }; - const client = - params.client || new CasParser({ defaultHeaders: { 'X-Stainless-MCP': 'true' }, logger: logger }); + const client = new CasParser({ + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); server.setRequestHandler(ListToolsRequestSchema, async () => { + if (providedEndpoints === null) { + initTools(server.getClientVersion()); + } return { - tools: providedEndpoints.map((endpoint) => endpoint.tool), + tools: providedEndpoints!.map((endpoint) => endpoint.tool), }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (endpointMap === null) { + initTools(server.getClientVersion()); + } const { name, arguments: args } = request.params; - const endpoint = endpointMap[name]; + const endpoint = endpointMap![name]; if (!endpoint) { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(endpoint.tool, endpoint.handler, client, args, params.capabilities); + return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities); }); } /** * Selects the tools to include in the MCP Server based on the provided options. */ -export function selectTools(endpoints: Endpoint[], options: McpOptions): Endpoint[] { - const filteredEndpoints = query(options.filters, endpoints); +export function selectTools(endpoints: Endpoint[], options?: McpOptions): Endpoint[] { + const filteredEndpoints = query(options?.filters ?? [], endpoints); let includedTools = filteredEndpoints; if (includedTools.length > 0) { - if (options.includeDynamicTools) { + if (options?.includeDynamicTools) { includedTools = dynamicTools(includedTools); } } else { - if (options.includeAllTools) { + if (options?.includeAllTools) { includedTools = endpoints; - } else if (options.includeDynamicTools) { + } else if (options?.includeDynamicTools) { includedTools = dynamicTools(endpoints); - } else if (options.includeCodeTools) { + } else if (options?.includeCodeTools) { includedTools = [codeTool()]; } else { includedTools = endpoints; } } - const capabilities = { ...defaultClientCapabilities, ...options.capabilities }; + const capabilities = { ...defaultClientCapabilities, ...options?.capabilities }; return applyCompatibilityTransformations(includedTools, capabilities); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index b269163..d902a5b 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,12 +1,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { init, newMcpServer } from './server'; -import { Endpoint } from './tools'; +import { initMcpServer, newMcpServer } from './server'; import { McpOptions } from './options'; -export const launchStdioServer = async (options: McpOptions, endpoints: Endpoint[]) => { +export const launchStdioServer = async (options: McpOptions) => { const server = newMcpServer(); - init({ server, endpoints }); + initMcpServer({ server, mcpOptions: options }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 08ea1f1..24604b8 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -29,15 +29,7 @@ describe('parseCLIOptions', () => { { type: 'operation', op: 'include', value: 'read' }, ] as Filter[]); - // Default client capabilities - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }); + expect(result.capabilities).toEqual({}); expect(result.list).toBe(false); @@ -61,14 +53,7 @@ describe('parseCLIOptions', () => { { type: 'operation', op: 'exclude', value: 'write' }, ] as Filter[]); - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }); + expect(result.capabilities).toEqual({}); cleanup(); }); @@ -99,7 +84,6 @@ describe('parseCLIOptions', () => { validJson: true, refs: true, unions: true, - formats: true, toolNameLength: 40, }); @@ -150,10 +134,7 @@ describe('parseCLIOptions', () => { expect(result.capabilities).toEqual({ topLevelUnions: true, validJson: true, - refs: true, unions: true, - formats: true, - toolNameLength: undefined, }); cleanup(); @@ -316,7 +297,7 @@ describe('parseQueryOptions', () => { ]); expect(result.client).toBe('cursor'); - expect(result.includeDynamicTools).toBe(true); + expect(result.includeDynamicTools).toBe(undefined); }); it('should override client from default options', () => { From 02b1e541dcf8d3a253c91f55266be3edb3db60e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:36:38 +0000 Subject: [PATCH 14/21] chore(mcp): update package.json --- packages/mcp-server/package.json | 1 + packages/mcp-server/src/headers.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index c686086..9999126 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -50,6 +50,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", + "@types/qs": "^6.14.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index 1d7399c..8dbae70 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -1,8 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { type ClientOptions } from 'cas-parser-node/client'; - import { IncomingMessage } from 'node:http'; +import { ClientOptions } from 'cas-parser-node'; export const parseAuthHeaders = (req: IncomingMessage): Partial => { const apiKey = From db1c76c3d2a76789d4dc3e5de57a875b7fa091db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:37:29 +0000 Subject: [PATCH 15/21] chore(mcp): update types --- packages/mcp-server/src/code-tool-types.ts | 2 +- packages/mcp-server/src/code-tool.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts index a30ed96..583728d 100644 --- a/packages/mcp-server/src/code-tool-types.ts +++ b/packages/mcp-server/src/code-tool-types.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { type ClientOptions } from 'cas-parser-node/client'; +import { ClientOptions } from 'cas-parser-node'; export type WorkerInput = { opts: ClientOptions; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index faae8f3..9fd6919 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,10 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { type ClientOptions } from 'cas-parser-node/client'; - import { dirname } from 'node:path'; import { pathToFileURL } from 'node:url'; -import CasParser from 'cas-parser-node'; +import CasParser, { ClientOptions } from 'cas-parser-node'; import { Endpoint, ContentBlock, Metadata } from './tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; From 9a166fa5baf70f7d0114d1c4dce14a1ac8bf956f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:40:25 +0000 Subject: [PATCH 16/21] chore: add package to package.json --- package.json | 1 + scripts/bootstrap | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 89a4ff3..7e0fddf 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "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", + "tslib": "^2.8.1", "typescript": "5.8.3", "typescript-eslint": "8.31.1" }, diff --git a/scripts/bootstrap b/scripts/bootstrap index 0af58e2..062a034 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -15,4 +15,4 @@ echo "==> Installing Node dependencies…" PACKAGE_MANAGER=$(command -v yarn >/dev/null 2>&1 && echo "yarn" || echo "npm") -$PACKAGE_MANAGER install +$PACKAGE_MANAGER install "$@" From 8463673b3bdd581f5d58b7ed18fb782b42cb94d9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:46:37 +0000 Subject: [PATCH 17/21] chore(internal): codegen related update --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5e1357..d09bc8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Bootstrap run: ./scripts/bootstrap @@ -46,7 +46,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Bootstrap run: ./scripts/bootstrap From 902939be47f74826e00ca6b8f73e94f969d96952 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:52:39 +0000 Subject: [PATCH 18/21] chore(client): qualify global Blob --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index bbfe9c2..b95114c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -689,7 +689,7 @@ export class CasParser { // Preserve legacy string encoding behavior for now headers.values.has('content-type')) || // `Blob` is superset of `File` - body instanceof Blob || + ((globalThis as any).Blob && body instanceof (globalThis as any).Blob) || // `FormData` -> `multipart/form-data` body instanceof FormData || // `URLSearchParams` -> `application/x-www-form-urlencoded` From b359ebce43e80eead9d0ff40b45a13e6af4c82b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 03:51:40 +0000 Subject: [PATCH 19/21] chore: update CI script --- .github/workflows/ci.yml | 9 +++++++++ scripts/utils/upload-artifact.sh | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d09bc8d..c60f7af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,15 @@ jobs: AUTH: ${{ steps.github-oidc.outputs.github_token }} SHA: ${{ github.sha }} run: ./scripts/utils/upload-artifact.sh + + - name: Upload MCP Server tarball + if: github.repository == 'stainless-sdks/cas-parser-typescript' + env: + URL: https://pkg.stainless.com/s?subpackage=mcp-server + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + BUILD_PATH: packages/mcp-server/dist + run: ./scripts/utils/upload-artifact.sh test: timeout-minutes: 10 name: test diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 0e9e0e5..65e625e 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -12,7 +12,7 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz dist | curl -v -X PUT \ +UPLOAD_RESPONSE=$(tar -cz "${BUILD_PATH:-dist}" | curl -v -X PUT \ -H "Content-Type: application/gzip" \ --data-binary @- "$SIGNED_URL" 2>&1) From 033f0ef5142b7f37ec8ccf9c276d5bd9bb3dbe76 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 02:15:50 +0000 Subject: [PATCH 20/21] chore(internal): codegen related update --- packages/mcp-server/src/options.ts | 20 ++++++++++++-------- packages/mcp-server/tests/options.test.ts | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 9eb00b4..2100cf5 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -366,16 +366,20 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M } } + let dynamicTools: boolean | undefined = + queryOptions.no_tools && !queryOptions.no_tools?.includes('dynamic') ? false + : queryOptions.tools?.includes('dynamic') ? true + : defaultOptions.includeDynamicTools; + + let allTools: boolean | undefined = + queryOptions.no_tools && !queryOptions.no_tools?.includes('all') ? false + : queryOptions.tools?.includes('all') ? true + : defaultOptions.includeAllTools; + return { client: queryOptions.client ?? defaultOptions.client, - includeDynamicTools: - defaultOptions.includeDynamicTools === false ? - false - : queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic'), - includeAllTools: - defaultOptions.includeAllTools === false ? - false - : queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all'), + includeDynamicTools: dynamicTools, + includeAllTools: allTools, includeCodeTools: undefined, filters, capabilities: clientCapabilities, diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 24604b8..a8a5b81 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -297,7 +297,7 @@ describe('parseQueryOptions', () => { ]); expect(result.client).toBe('cursor'); - expect(result.includeDynamicTools).toBe(undefined); + expect(result.includeDynamicTools).toBe(true); }); it('should override client from default options', () => { From e98c59a3bcff8f76f56404fbd9ef171c45164c2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 02:16:08 +0000 Subject: [PATCH 21/21] release: 1.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ package.json | 2 +- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3f1463..96f1cd9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index edcbd1d..30486fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 1.3.0 (2025-08-24) + +Full Changelog: [v1.2.0...v1.3.0](https://github.com/CASParser/cas-parser-node/compare/v1.2.0...v1.3.0) + +### Features + +* **mcp:** add code execution tool ([84904a7](https://github.com/CASParser/cas-parser-node/commit/84904a7f558053aa211b8f483a7b4f6c2b13b8f6)) +* **mcp:** add option to infer mcp client ([c190ccd](https://github.com/CASParser/cas-parser-node/commit/c190ccdd5cb6342dc620c26a3d06c0514f693a37)) +* **mcp:** parse query string as mcp client options in mcp server ([1f6e085](https://github.com/CASParser/cas-parser-node/commit/1f6e085c4e7e9edf497585587fc3e6603b53a3e3)) + + +### Chores + +* add package to package.json ([9a166fa](https://github.com/CASParser/cas-parser-node/commit/9a166fa5baf70f7d0114d1c4dce14a1ac8bf956f)) +* **client:** qualify global Blob ([902939b](https://github.com/CASParser/cas-parser-node/commit/902939be47f74826e00ca6b8f73e94f969d96952)) +* **internal:** codegen related update ([033f0ef](https://github.com/CASParser/cas-parser-node/commit/033f0ef5142b7f37ec8ccf9c276d5bd9bb3dbe76)) +* **internal:** codegen related update ([8463673](https://github.com/CASParser/cas-parser-node/commit/8463673b3bdd581f5d58b7ed18fb782b42cb94d9)) +* **internal:** make mcp-server publishing public by defaut ([4bbdb37](https://github.com/CASParser/cas-parser-node/commit/4bbdb3749a70bc35a4e9fdd4b9763a6f79f78a24)) +* **internal:** refactor array check ([00cc94f](https://github.com/CASParser/cas-parser-node/commit/00cc94fb85746fc3a79136d22214864673a3c93f)) +* **mcp:** add cors to oauth metadata route ([3833155](https://github.com/CASParser/cas-parser-node/commit/3833155c5d3bcca5a7bf6ca0f513e8365db316cc)) +* **mcp:** update package.json ([02b1e54](https://github.com/CASParser/cas-parser-node/commit/02b1e541dcf8d3a253c91f55266be3edb3db60e8)) +* **mcp:** update types ([db1c76c](https://github.com/CASParser/cas-parser-node/commit/db1c76c3d2a76789d4dc3e5de57a875b7fa091db)) +* update CI script ([b359ebc](https://github.com/CASParser/cas-parser-node/commit/b359ebce43e80eead9d0ff40b45a13e6af4c82b1)) + ## 1.2.0 (2025-08-18) Full Changelog: [v1.1.0...v1.2.0](https://github.com/CASParser/cas-parser-node/compare/v1.1.0...v1.2.0) diff --git a/package.json b/package.json index 7e0fddf..c6bb8b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cas-parser-node", - "version": "1.2.0", + "version": "1.3.0", "description": "The official TypeScript library for the Cas Parser API", "author": "Cas Parser ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 9999126..b5dfa07 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "cas-parser-node-mcp", - "version": "1.2.0", + "version": "1.3.0", "description": "The official MCP Server for the Cas Parser API", "author": "Cas Parser ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index d99a492..25b60f9 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -32,7 +32,7 @@ export const newMcpServer = () => new McpServer( { name: 'cas_parser_node_api', - version: '1.2.0', + version: '1.3.0', }, { capabilities: { tools: {}, logging: {} } }, ); diff --git a/src/version.ts b/src/version.ts index 54c8a47..39fa5bc 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.2.0'; // x-release-please-version +export const VERSION = '1.3.0'; // x-release-please-version