From f39cb7f6c2ad7c20da495ded91c644e432596425 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 04:36:34 +0000 Subject: [PATCH 1/3] chore(internal/client): fix form-urlencoded requests --- src/client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.ts b/src/client.ts index 1bad16f..bc8fec0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -757,6 +757,14 @@ export class CasParser { (Symbol.iterator in body && 'next' in body && typeof body.next === 'function')) ) { return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable) }; + } else if ( + typeof body === 'object' && + headers.values.get('content-type') === 'application/x-www-form-urlencoded' + ) { + return { + bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, + body: this.stringifyQuery(body as Record), + }; } else { return this.#encoder({ body, headers }); } From 5b3baf7ead67ae4a2e371ceda87d5d9e97019c5d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 04:44:12 +0000 Subject: [PATCH 2/3] chore(internal): allow setting x-stainless-api-key header on mcp server requests --- packages/mcp-server/src/auth.ts | 17 ++++++++- packages/mcp-server/src/code-tool.ts | 33 ++++++++++------- packages/mcp-server/src/docs-search-tool.ts | 15 ++++---- packages/mcp-server/src/http.ts | 21 +++++++---- packages/mcp-server/src/options.ts | 9 +++++ packages/mcp-server/src/server.ts | 40 +++++++++++++-------- packages/mcp-server/src/stdio.ts | 4 +-- packages/mcp-server/src/types.ts | 16 ++++++--- 8 files changed, 109 insertions(+), 46 deletions(-) diff --git a/packages/mcp-server/src/auth.ts b/packages/mcp-server/src/auth.ts index a867c58..d54a41f 100644 --- a/packages/mcp-server/src/auth.ts +++ b/packages/mcp-server/src/auth.ts @@ -2,9 +2,24 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from 'cas-parser-node'; +import { McpOptions } from './options'; -export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { +export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { const apiKey = Array.isArray(req.headers['x-api-key']) ? req.headers['x-api-key'][0] : req.headers['x-api-key']; return { apiKey }; }; + +export const getStainlessApiKey = (req: IncomingMessage, mcpOptions: McpOptions): string | undefined => { + // Try to get the key from the x-stainless-api-key header + const headerKey = + Array.isArray(req.headers['x-stainless-api-key']) ? + req.headers['x-stainless-api-key'][0] + : req.headers['x-stainless-api-key']; + if (headerKey && typeof headerKey === 'string') { + return headerKey; + } + + // Fall back to value set in the mcpOptions (e.g. from environment variable), if provided + return mcpOptions.stainlessApiKey; +}; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 0cec16d..0fdea1a 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,11 +1,17 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; +import { + McpRequestContext, + McpTool, + Metadata, + ToolCallResult, + asErrorResult, + asTextContentResult, +} from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; -import { CasParser } from 'cas-parser-node'; const prompt = `Runs JavaScript code to interact with the Cas Parser API. @@ -36,7 +42,7 @@ Variables will not persist between calls, so make sure to return or log any data * * @param endpoints - The endpoints to include in the list. */ -export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -56,19 +62,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M required: ['code'], }, }; - const handler = async (client: CasParser, args: any): Promise => { + const handler = async ({ + reqContext, + args, + }: { + reqContext: McpRequestContext; + args: any; + }): Promise => { const code = args.code as string; const intent = args.intent as string | undefined; + const client = reqContext.client; // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If // stronger security blocks are required, then these should be enforced in the downstream // API (e.g., by having users call the MCP server with API keys with limited permissions). - if (params.blockedMethods) { - const blockedMatches = params.blockedMethods.filter((method) => - code.includes(method.fullyQualifiedName), - ); + if (blockedMethods) { + const blockedMatches = blockedMethods.filter((method) => code.includes(method.fullyQualifiedName)); if (blockedMatches.length > 0) { return asErrorResult( `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches @@ -78,16 +89,14 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M } } - // this is not required, but passing a Stainless API key for the matching project_name - // will allow you to run code-mode queries against non-published versions of your SDK. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + // Setting a Stainless API key authenticates requests to the code tool endpoint. const res = await fetch(codeModeEndpoint, { method: 'POST', headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), 'Content-Type': 'application/json', client_envs: JSON.stringify({ CAS_PARSER_API_KEY: requireValue( diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 402157c..83e2a27 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,8 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './util'; - +import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; export const metadata: Metadata = { @@ -43,13 +41,18 @@ export const tool: Tool = { const docsSearchURL = process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/cas-parser/docs/search'; -export const handler = async (_: unknown, args: Record | undefined) => { +export const handler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const result = await fetch(`${docsSearchURL}?${query}`, { headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), }, }); diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index d808262..e04ede5 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -6,7 +6,7 @@ import { ClientOptions } from 'cas-parser-node'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; -import { parseAuthHeaders } from './auth'; +import { getStainlessApiKey, parseClientAuthHeaders } from './auth'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; @@ -21,10 +21,12 @@ const newServer = async ({ req: express.Request; res: express.Response; }): Promise => { - const server = await newMcpServer(); + const stainlessApiKey = getStainlessApiKey(req, mcpOptions); + const server = await newMcpServer(stainlessApiKey); try { - const authOptions = parseAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); + await initMcpServer({ server: server, mcpOptions: mcpOptions, @@ -32,6 +34,7 @@ const newServer = async ({ ...clientOptions, ...authOptions, }, + stainlessApiKey: stainlessApiKey, }); } catch (error) { res.status(401).json({ @@ -112,13 +115,17 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (params: { +export const launchStreamableHTTPServer = async ({ + mcpOptions, + debug, + port, +}: { mcpOptions: McpOptions; debug: boolean; port: number | string | undefined; }) => { - const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); - const server = app.listen(params.port); + const app = streamableHTTPApp({ mcpOptions, debug }); + const server = app.listen(port); const address = server.address(); if (typeof address === 'string') { @@ -126,6 +133,6 @@ export const launchStreamableHTTPServer = async (params: { } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${params.port}`); + console.error(`MCP Server running on streamable HTTP on port ${port}`); } }; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index cfde21d..32a8871 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import z from 'zod'; +import { readEnv } from './util'; export type CLIOptions = McpOptions & { debug: boolean; @@ -14,6 +15,7 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + stainlessApiKey?: string | undefined; codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; @@ -51,6 +53,12 @@ export function parseCLIOptions(): CLIOptions { description: 'Port to serve on if using http transport', }) .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('stainless-api-key', { + type: 'string', + default: readEnv('STAINLESS_API_KEY'), + description: + 'API key for Stainless. Used to authenticate requests to Stainless-hosted tools endpoints.', + }) .option('tools', { type: 'string', array: true, @@ -81,6 +89,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + stainlessApiKey: argv.stainlessApiKey, codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 96b573c..32a7f2d 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -13,17 +13,17 @@ import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; -import { HandlerFunction, McpTool } from './types'; +import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; import { readEnv } from './util'; -async function getInstructions() { - // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); +async function getInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. const response = await fetch( readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/cas-parser', { method: 'GET', - headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) }, + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, }, ); @@ -52,14 +52,14 @@ async function getInstructions() { return instructions; } -export const newMcpServer = async () => +export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'cas_parser_node_api', version: '1.7.1', }, { - instructions: await getInstructions(), + instructions: await getInstructions(stainlessApiKey), capabilities: { tools: {}, logging: {} }, }, ); @@ -72,6 +72,7 @@ export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; + stainlessApiKey?: string | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -116,7 +117,14 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(mcpTool.handler, client, args); + return executeHandler({ + handler: mcpTool.handler, + reqContext: { + client, + stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + }, + args, + }); }); server.setRequestHandler(SetLevelRequestSchema, async (request) => { @@ -161,10 +169,14 @@ export function selectTools(options?: McpOptions): McpTool[] { /** * Runs the provided handler with the given client and arguments. */ -export async function executeHandler( - handler: HandlerFunction, - client: CasParser, - args: Record | undefined, -) { - return await handler(client, args || {}); +export async function executeHandler({ + handler, + reqContext, + args, +}: { + handler: HandlerFunction; + reqContext: McpRequestContext; + args: Record | undefined; +}): Promise { + return await handler({ reqContext, args: args || {} }); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 57b9912..ceccaed 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -3,9 +3,9 @@ import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async (mcpOptions: McpOptions) => { - const server = await newMcpServer(); + const server = await newMcpServer(mcpOptions.stainlessApiKey); - await initMcpServer({ server, mcpOptions }); + await initMcpServer({ server, mcpOptions, stainlessApiKey: mcpOptions.stainlessApiKey }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index 36dd727..5b8e8a4 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -42,10 +42,18 @@ export type ToolCallResult = { isError?: boolean; }; -export type HandlerFunction = ( - client: CasParser, - args: Record | undefined, -) => Promise; +export type McpRequestContext = { + client: CasParser; + stainlessApiKey?: string | undefined; +}; + +export type HandlerFunction = ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => Promise; export function asTextContentResult(result: unknown): ToolCallResult { return { From ccfc1ba3a3640cf8401de4320fff6776cd9a9179 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 04:44:29 +0000 Subject: [PATCH 3/3] release: 1.7.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ package.json | 2 +- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0972da..f2ada20 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.7.1" + ".": "1.7.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f2d50..1627e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.7.2 (2026-02-18) + +Full Changelog: [v1.7.1...v1.7.2](https://github.com/CASParser/cas-parser-node/compare/v1.7.1...v1.7.2) + +### Chores + +* **internal/client:** fix form-urlencoded requests ([f39cb7f](https://github.com/CASParser/cas-parser-node/commit/f39cb7f6c2ad7c20da495ded91c644e432596425)) +* **internal:** allow setting x-stainless-api-key header on mcp server requests ([5b3baf7](https://github.com/CASParser/cas-parser-node/commit/5b3baf7ead67ae4a2e371ceda87d5d9e97019c5d)) + ## 1.7.1 (2026-02-14) Full Changelog: [v1.7.0...v1.7.1](https://github.com/CASParser/cas-parser-node/compare/v1.7.0...v1.7.1) diff --git a/package.json b/package.json index 74ad47d..8f8f6af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cas-parser-node", - "version": "1.7.1", + "version": "1.7.2", "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 8c4bc09..5b787e2 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "cas-parser-node-mcp", - "version": "1.7.1", + "version": "1.7.2", "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 32a7f2d..906f3f1 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -56,7 +56,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'cas_parser_node_api', - version: '1.7.1', + version: '1.7.2', }, { instructions: await getInstructions(stainlessApiKey), diff --git a/src/version.ts b/src/version.ts index 63fe850..c742ace 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.7.1'; // x-release-please-version +export const VERSION = '1.7.2'; // x-release-please-version