From b3f5e099f190c0dcdd648262398d4629fac02671 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:52:01 +0000 Subject: [PATCH 01/21] chore(internal): codegen related update --- packages/mcp-server/src/code-tool.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 6040f919..1dbc9cb9 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -4,6 +4,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv } from './server'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { Finch } from '@tryfinch/finch-api'; const prompt = `Runs JavaScript code to interact with the Finch API. @@ -55,7 +56,7 @@ export function codeTool(): McpTool { required: ['code'], }, }; - const handler = async (_: unknown, args: any): Promise => { + const handler = async (client: Finch, args: any): Promise => { const code = args.code as string; const intent = args.intent as string | undefined; @@ -71,10 +72,10 @@ export function codeTool(): McpTool { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), 'Content-Type': 'application/json', client_envs: JSON.stringify({ - FINCH_CLIENT_ID: readEnv('FINCH_CLIENT_ID'), - FINCH_CLIENT_SECRET: readEnv('FINCH_CLIENT_SECRET'), - FINCH_WEBHOOK_SECRET: readEnv('FINCH_WEBHOOK_SECRET'), - FINCH_BASE_URL: readEnv('FINCH_BASE_URL'), + FINCH_CLIENT_ID: readEnv('FINCH_CLIENT_ID') ?? client.clientID ?? undefined, + FINCH_CLIENT_SECRET: readEnv('FINCH_CLIENT_SECRET') ?? client.clientSecret ?? undefined, + FINCH_WEBHOOK_SECRET: readEnv('FINCH_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined, + FINCH_BASE_URL: readEnv('FINCH_BASE_URL') ?? client.baseURL ?? undefined, }), }, body: JSON.stringify({ From e2285bffd1403ecbb223daf47f7e1eda40033d54 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:26:54 +0000 Subject: [PATCH 02/21] fix(mcp): allow falling back for required env variables --- packages/mcp-server/src/server.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 065db351..c32acbaf 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -146,3 +146,10 @@ export const readEnvOrError = (env: string): string => { } return envValue; }; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; From 2346e45e51546112f96d29f89904a499c66c61d2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:36:20 +0000 Subject: [PATCH 03/21] fix(docs): fix mcp installation instructions for remote servers --- README.md | 4 ++-- packages/mcp-server/README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bdc25d51..760146bc 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Finch MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl0sImVudiI6eyJGSU5DSF9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4iLCJGSU5DSF9DTElFTlRfSUQiOiI0YWIxNWU1MS0xMWFkLTQ5ZjQtYWNhZS1mMzQzYjc3OTQzNzUiLCJGSU5DSF9DTElFTlRfU0VDUkVUIjoiTXkgQ2xpZW50IFNlY3JldCIsIkZJTkNIX1dFQkhPT0tfU0VDUkVUIjoiTXkgV2ViaG9vayBTZWNyZXQifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%2C%22env%22%3A%7B%22FINCH_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%2C%22FINCH_CLIENT_ID%22%3A%224ab15e51-11ad-49f4-acae-f343b7794375%22%2C%22FINCH_CLIENT_SECRET%22%3A%22My%20Client%20Secret%22%2C%22FINCH_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D) > Note: You may need to set environment variables in your MCP client. diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index bb1e160f..07c2da90 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -45,14 +45,14 @@ For clients with a configuration JSON, it might look something like this: If you use Cursor, you can install the MCP server by using the button below. You will need to set your environment variables in Cursor's `mcp.json`, which can be found in Cursor Settings > Tools & MCP > New MCP Server. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl0sImVudiI6eyJGSU5DSF9BQ0NFU1NfVE9LRU4iOiJTZXQgeW91ciBGSU5DSF9BQ0NFU1NfVE9LRU4gaGVyZS4iLCJGSU5DSF9DTElFTlRfSUQiOiJTZXQgeW91ciBGSU5DSF9DTElFTlRfSUQgaGVyZS4iLCJGSU5DSF9DTElFTlRfU0VDUkVUIjoiU2V0IHlvdXIgRklOQ0hfQ0xJRU5UX1NFQ1JFVCBoZXJlLiIsIkZJTkNIX1dFQkhPT0tfU0VDUkVUIjoiU2V0IHlvdXIgRklOQ0hfV0VCSE9PS19TRUNSRVQgaGVyZS4ifX0) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl0sImVudiI6eyJGSU5DSF9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4iLCJGSU5DSF9DTElFTlRfSUQiOiI0YWIxNWU1MS0xMWFkLTQ5ZjQtYWNhZS1mMzQzYjc3OTQzNzUiLCJGSU5DSF9DTElFTlRfU0VDUkVUIjoiTXkgQ2xpZW50IFNlY3JldCIsIkZJTkNIX1dFQkhPT0tfU0VDUkVUIjoiTXkgV2ViaG9vayBTZWNyZXQifX0) ### VS Code If you use MCP, you can install the MCP server by clicking the link below. You will need to set your environment variables in VS Code's `mcp.json`, which can be found via Command Palette > MCP: Open User Configuration. -[Open VS Code](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%2C%22env%22%3A%7B%22FINCH_ACCESS_TOKEN%22%3A%22Set%20your%20FINCH_ACCESS_TOKEN%20here.%22%2C%22FINCH_CLIENT_ID%22%3A%22Set%20your%20FINCH_CLIENT_ID%20here.%22%2C%22FINCH_CLIENT_SECRET%22%3A%22Set%20your%20FINCH_CLIENT_SECRET%20here.%22%2C%22FINCH_WEBHOOK_SECRET%22%3A%22Set%20your%20FINCH_WEBHOOK_SECRET%20here.%22%7D%7D) +[Open VS Code](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%2C%22env%22%3A%7B%22FINCH_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%2C%22FINCH_CLIENT_ID%22%3A%224ab15e51-11ad-49f4-acae-f343b7794375%22%2C%22FINCH_CLIENT_SECRET%22%3A%22My%20Client%20Secret%22%2C%22FINCH_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D) ### Claude Code @@ -60,7 +60,7 @@ If you use Claude Code, you can install the MCP server by running the command be environment variables in Claude Code's `.claude.json`, which can be found in your home directory. ``` -claude mcp add tryfinch_finch_api_mcp_api --env FINCH_ACCESS_TOKEN="Your FINCH_ACCESS_TOKEN here." FINCH_CLIENT_ID="Your FINCH_CLIENT_ID here." FINCH_CLIENT_SECRET="Your FINCH_CLIENT_SECRET here." FINCH_WEBHOOK_SECRET="Your FINCH_WEBHOOK_SECRET here." -- npx -y @tryfinch/finch-api-mcp +claude mcp add tryfinch_finch_api_mcp_api --env FINCH_ACCESS_TOKEN="My Access Token" FINCH_CLIENT_ID="4ab15e51-11ad-49f4-acae-f343b7794375" FINCH_CLIENT_SECRET="My Client Secret" FINCH_WEBHOOK_SECRET="My Webhook Secret" -- npx -y @tryfinch/finch-api-mcp ``` ## Code Mode From b96b21eccc21170a8c655ac8de964723952e9f93 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:39:18 +0000 Subject: [PATCH 04/21] chore(mcp): up tsconfig lib version to es2022 --- packages/mcp-server/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index cded90d2..308897ae 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -2,8 +2,8 @@ "include": ["src", "tests", "examples"], "exclude": [], "compilerOptions": { - "target": "es2020", - "lib": ["es2020"], + "target": "es2022", + "lib": ["es2022"], "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, From e6cbcb4c380d391797875514114fbd870e680aa9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:35:40 +0000 Subject: [PATCH 05/21] fix(client): avoid memory leak with abort signals --- src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index a93f6575..9d8ee2be 100644 --- a/src/client.ts +++ b/src/client.ts @@ -615,9 +615,10 @@ export class Finch { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - if (signal) signal.addEventListener('abort', () => controller.abort()); + const abort = controller.abort.bind(controller); + if (signal) signal.addEventListener('abort', abort, { once: true }); - const timeout = setTimeout(() => controller.abort(), ms); + const timeout = setTimeout(abort, ms); const isReadableBody = ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) || From 9168b0e266bb4babc638698f27c166d282c5f346 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:31:06 +0000 Subject: [PATCH 06/21] chore(client): do not parse responses with empty content-length --- src/internal/parse.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/internal/parse.ts b/src/internal/parse.ts index 19d4f9db..c66b716b 100644 --- a/src/internal/parse.ts +++ b/src/internal/parse.ts @@ -29,6 +29,12 @@ export async function defaultParseResponse(client: Finch, props: APIResponseP const mediaType = contentType?.split(';')[0]?.trim(); const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); if (isJSON) { + const contentLength = response.headers.get('content-length'); + if (contentLength === '0') { + // if there is no content we can't do anything + return undefined as T; + } + const json = await response.json(); return json as T; } From ed2565a0da646b78e0f3d74bbe7d2f7c8c4c46bb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:17:40 +0000 Subject: [PATCH 07/21] chore(internal): support oauth authorization code flow for MCP servers --- packages/mcp-server/package.json | 4 ++++ packages/mcp-server/src/headers.ts | 4 +++- packages/mcp-server/src/http.ts | 9 +++++---- packages/mcp-server/src/options.ts | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 5a8a75d0..bba2ab5b 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -34,10 +34,12 @@ "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.25.2", "@valtown/deno-http-worker": "^0.0.21", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^5.1.0", "fuse.js": "^7.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", + "morgan": "^1.10.0", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -50,9 +52,11 @@ }, "devDependencies": { "@anthropic-ai/mcpb": "^2.1.2", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", + "@types/morgan": "^1.9.10", "@types/qs": "^6.14.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index e127d4e8..5127806d 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -3,7 +3,7 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from '@tryfinch/finch-api'; -export const parseAuthHeaders = (req: IncomingMessage): Partial => { +export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { if (req.headers.authorization) { const scheme = req.headers.authorization.split(' ')[0]!; const value = req.headers.authorization.slice(scheme.length + 1); @@ -21,6 +21,8 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial = 'Unsupported authorization scheme. Expected the "Authorization" header to be a supported scheme (Bearer, Basic).', ); } + } else if (required) { + throw new Error('Missing required Authorization header; see WWW-Authenticate header for details.'); } const clientID = diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index dcfeba6a..42eb5002 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,8 +2,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; - import express from 'express'; +import morgan from 'morgan'; import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; @@ -20,7 +20,7 @@ const newServer = ({ const server = newMcpServer(); try { - const authOptions = parseAuthHeaders(req); + const authOptions = parseAuthHeaders(req, false); initMcpServer({ server: server, clientOptions: { @@ -75,14 +75,15 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, - mcpOptions = {}, + mcpOptions, }: { clientOptions?: ClientOptions; - mcpOptions?: McpOptions; + mcpOptions: McpOptions; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); + app.use(morgan('combined')); app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index c66ad8ce..025280ec 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -35,6 +35,7 @@ export function parseCLIOptions(): CLIOptions { }) .option('port', { type: 'number', + default: 3000, description: 'Port to serve on if using http transport', }) .option('socket', { From 03776eb5c0329b6580fb3c2e58ce4d811fbde635 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:40:57 +0000 Subject: [PATCH 08/21] chore(client): restructure abort controller binding --- src/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 9d8ee2be..fb422e2b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -615,7 +615,7 @@ export class Finch { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - const abort = controller.abort.bind(controller); + const abort = this._makeAbort(controller); if (signal) signal.addEventListener('abort', abort, { once: true }); const timeout = setTimeout(abort, ms); @@ -641,6 +641,7 @@ export class Finch { return await this.fetch.call(undefined, url, fetchOptions); } finally { clearTimeout(timeout); + if (signal) signal.removeEventListener('abort', abort); } } @@ -786,6 +787,12 @@ export class Finch { return headers.values; } + private _makeAbort(controller: AbortController) { + // note: we can't just inline this method inside `fetchWithTimeout()` because then the closure + // would capture all request options, and cause a memory leak. + return () => controller.abort(); + } + private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): { bodyHeaders: HeadersLike; body: BodyInit | undefined; From c3685d56e5988db461e09f3ec90e693dbec3bdfa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:13:14 +0000 Subject: [PATCH 09/21] chore(internal): refactor flag parsing for MCP servers and add debug flag --- packages/mcp-server/package.json | 1 + packages/mcp-server/src/http.ts | 27 ++++++++++++++++++++++----- packages/mcp-server/src/index.ts | 6 +++++- packages/mcp-server/src/options.ts | 28 +++++++++++++++------------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index bba2ab5b..61a3cbde 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -40,6 +40,7 @@ "fuse.js": "^7.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", "morgan": "^1.10.0", + "morgan-body": "^2.6.9", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 42eb5002..3d728ecc 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,6 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import morgan from 'morgan'; +import morganBody from 'morgan-body'; import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; @@ -76,14 +77,26 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, mcpOptions, + debug, }: { clientOptions?: ClientOptions; mcpOptions: McpOptions; + debug: boolean; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); - app.use(morgan('combined')); + + if (debug) { + morganBody(app, { + logAllReqHeader: true, + logAllResHeader: true, + logRequestBody: true, + logResponseBody: true, + }); + } else { + app.use(morgan('combined')); + } app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); @@ -92,9 +105,13 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { - const app = streamableHTTPApp({ mcpOptions: options }); - const server = app.listen(port); +export const launchStreamableHTTPServer = async (params: { + mcpOptions: McpOptions; + debug: boolean; + port: number | string | undefined; +}) => { + const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); + const server = app.listen(params.port); const address = server.address(); if (typeof address === 'string') { @@ -102,6 +119,6 @@ export const launchStreamableHTTPServer = async (options: McpOptions, port: numb } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${port}`); + console.error(`MCP Server running on streamable HTTP on port ${params.port}`); } }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 0f6dd426..d75968e3 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -21,7 +21,11 @@ async function main() { await launchStdioServer(); break; case 'http': - await launchStreamableHTTPServer(options, options.port ?? options.socket); + await launchStreamableHTTPServer({ + mcpOptions: options, + debug: options.debug, + port: options.port ?? options.socket, + }); break; } } diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 025280ec..74380833 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers'; import z from 'zod'; export type CLIOptions = McpOptions & { + debug: boolean; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; @@ -15,17 +16,24 @@ export type McpOptions = { export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) - .option('tools', { + .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('no-tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Use dynamic tools or all tools', + description: 'Tools to explicitly disable', }) - .option('no-tools', { + .option('port', { + type: 'number', + default: 3000, + description: 'Port to serve on if using http transport', + }) + .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Do not use any dynamic or all tools', + description: 'Tools to explicitly enable', }) .option('transport', { type: 'string', @@ -33,15 +41,8 @@ export function parseCLIOptions(): CLIOptions { default: 'stdio', description: 'What transport to use; stdio for local servers or http for remote servers', }) - .option('port', { - type: 'number', - default: 3000, - description: 'Port to serve on if using http transport', - }) - .option('socket', { - type: 'string', - description: 'Unix socket to serve on if using http transport', - }) + .env('MCP_SERVER') + .version(true) .help(); const argv = opts.parseSync(); @@ -57,6 +58,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), + debug: !!argv.debug, transport, port: argv.port, socket: argv.socket, From 41a62e7da2c6f6fe1b59afe8d87066489d707bc4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:31:42 +0000 Subject: [PATCH 10/21] feat(mcp): add initial server instructions Adds generated MCP server instructions, to help agents get easy tasks on the first try. --- packages/mcp-server/src/http.ts | 10 +++---- packages/mcp-server/src/server.ts | 48 +++++++++++++++++++++++++++---- packages/mcp-server/src/stdio.ts | 4 +-- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 3d728ecc..1f851cb6 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -9,7 +9,7 @@ import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; -const newServer = ({ +const newServer = async ({ clientOptions, req, res, @@ -17,12 +17,12 @@ const newServer = ({ clientOptions: ClientOptions; req: express.Request; res: express.Response; -}): McpServer | null => { - const server = newMcpServer(); +}): Promise => { + const server = await newMcpServer(); try { const authOptions = parseAuthHeaders(req, false); - initMcpServer({ + await initMcpServer({ server: server, clientOptions: { ...clientOptions, @@ -46,7 +46,7 @@ const newServer = ({ const post = (options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) => async (req: express.Request, res: express.Response) => { - const server = newServer({ ...options, req, res }); + const server = await newServer({ ...options, req, res }); // If we return null, we already set the authorization error. if (server === null) return; const transport = new StreamableHTTPServerTransport(); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index c32acbaf..f4e0acec 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -17,23 +17,59 @@ import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; export { ClientOptions } from '@tryfinch/finch-api'; -export const newMcpServer = () => +async function getInstructions() { + // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/finch', + { + method: 'GET', + headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + console.warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = ` + This is the finch MCP server. You will use Code Mode to help the user perform + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! + `; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + instructions = ` + The current time in Unix timestamps is ${Date.now()}. + + ${instructions} + `; + + return instructions; +} + +export const newMcpServer = async () => new McpServer( { name: 'tryfinch_finch_api_api', version: '9.0.0', }, - { capabilities: { tools: {}, logging: {} } }, + { + instructions: await getInstructions(), + capabilities: { tools: {}, logging: {} }, + }, ); -// Create server instance -export const server = newMcpServer(); - /** * Initializes the provided MCP Server with the given tools and handlers. * If not provided, the default client, tools and handlers will be used. */ -export function initMcpServer(params: { +export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index f07696f3..47aeb0c9 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -2,9 +2,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async () => { - const server = newMcpServer(); + const server = await newMcpServer(); - initMcpServer({ server }); + await initMcpServer({ server }); const transport = new StdioServerTransport(); await server.connect(transport); From 834599b8d10c0cc7858861504ebbc6142d28768d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:10:49 +0000 Subject: [PATCH 11/21] fix(client): avoid removing abort listener too early --- src/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index fb422e2b..1f858467 100644 --- a/src/client.ts +++ b/src/client.ts @@ -641,7 +641,6 @@ export class Finch { return await this.fetch.call(undefined, url, fetchOptions); } finally { clearTimeout(timeout); - if (signal) signal.removeEventListener('abort', abort); } } From 6b7520906d83f1ebeaf76c2b827f34f8ebeeeca2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:19:52 +0000 Subject: [PATCH 12/21] chore(internal): fix pagination internals not accepting option promises --- src/client.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 1f858467..18c23fe0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -592,9 +592,14 @@ export class Finch { getAPIList = Pagination.AbstractPage>( path: string, Page: new (...args: any[]) => PageClass, - opts?: RequestOptions, + opts?: PromiseOrValue, ): Pagination.PagePromise { - return this.requestAPIList(Page, { method: 'get', path, ...opts }); + return this.requestAPIList( + Page, + opts && 'then' in opts ? + opts.then((opts) => ({ method: 'get', path, ...opts })) + : { method: 'get', path, ...opts }, + ); } requestAPIList< @@ -602,7 +607,7 @@ export class Finch { PageClass extends Pagination.AbstractPage = Pagination.AbstractPage, >( Page: new (...args: ConstructorParameters) => PageClass, - options: FinalRequestOptions, + options: PromiseOrValue, ): Pagination.PagePromise { const request = this.makeRequest(options, null, undefined); return new Pagination.PagePromise(this as any as Finch, request, Page); From c6b1999a1895466414ad1c3975b27ee317faa473 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:56:17 +0000 Subject: [PATCH 13/21] chore(internal): add health check to MCP server when running in HTTP mode --- packages/mcp-server/src/http.ts | 3 +++ packages/mcp-server/src/server.ts | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 1f851cb6..b2031368 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -98,6 +98,9 @@ export const streamableHTTPApp = ({ app.use(morgan('combined')); } + app.get('/health', async (req: express.Request, res: express.Response) => { + res.status(200).send('OK'); + }); app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); app.delete('/', del); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index f4e0acec..ec0d5760 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -36,10 +36,10 @@ async function getInstructions() { instructions = ` This is the finch MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! `; } From d7863401c96e70f832d74a7eca4b7ef0850791e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:32:55 +0000 Subject: [PATCH 14/21] chore(internal): always generate MCP server dockerfiles and upgrade associated dependencies --- .dockerignore | 59 ++++++++++++++++++++++++++++ packages/mcp-server/Dockerfile | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 .dockerignore create mode 100644 packages/mcp-server/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..12ff1e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules/ +**/node_modules/ + +# Build outputs +dist/ +**/dist/ + +# Git +.git/ +.gitignore + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +test/ +tests/ +__tests__/ +*.test.js +*.spec.js +coverage/ +.nyc_output/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.* + +# Temporary files +*.tmp +*.temp +.cache/ + +# Examples and scripts +examples/ +bin/ + +# Other packages (we only need mcp-server) +packages/*/ +!packages/mcp-server/ diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile new file mode 100644 index 00000000..193b657e --- /dev/null +++ b/packages/mcp-server/Dockerfile @@ -0,0 +1,71 @@ +# Dockerfile for Finch MCP Server +# +# This Dockerfile builds a Docker image for the MCP Server. +# +# To build the image locally: +# docker build -f packages/mcp-server/Dockerfile -t @tryfinch/finch-api-mcp:local . +# +# To run the image: +# docker run -i @tryfinch/finch-api-mcp:local [OPTIONS] +# +# Common options: +# --tool= Include specific tools +# --resource= Include tools for specific resources +# --operation=read|write Filter by operation type +# --client= Set client compatibility (e.g., claude, cursor) +# --transport= Set transport type (stdio or http) +# +# For a full list of options: +# docker run -i @tryfinch/finch-api-mcp:local --help +# +# Note: The MCP server uses stdio transport by default. Docker's -i flag +# enables interactive mode, allowing the container to communicate over stdin/stdout. + +# Build stage +FROM node:24-alpine AS builder + +# Install bash for build script +RUN apk add --no-cache bash openssl + +# Set working directory +WORKDIR /build + +# Copy entire repository +COPY . . + +# Install all dependencies and build everything +RUN yarn install --frozen-lockfile && \ + yarn build + +# Production stage +FROM node:24-alpine + +# Add non-root user +RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 + +# Set working directory +WORKDIR /app + +# Copy the built mcp-server dist directory +COPY --from=builder /build/packages/mcp-server/dist ./ + +# Copy node_modules from mcp-server (includes all production deps) +COPY --from=builder /build/packages/mcp-server/node_modules ./node_modules + +# Copy the built @tryfinch/finch-api into node_modules +COPY --from=builder /build/dist ./node_modules/@tryfinch/finch-api + +# Change ownership to nodejs user +RUN chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# The MCP server uses stdio transport by default +# No exposed ports needed for stdio communication + +# Set the entrypoint to the MCP server +ENTRYPOINT ["node", "index.js"] + +# Allow passing arguments to the MCP server +CMD [] From c6921b76e9db6ddd6019353a6da69c12a43f8570 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:25:41 +0000 Subject: [PATCH 15/21] chore(internal): allow basic filtering of methods allowed for MCP code mode --- packages/mcp-server/src/code-tool.ts | 21 +- packages/mcp-server/src/http.ts | 3 + packages/mcp-server/src/index.ts | 2 +- packages/mcp-server/src/methods.ts | 364 +++++++++++++++++++++++++++ packages/mcp-server/src/options.ts | 23 ++ packages/mcp-server/src/server.ts | 7 +- packages/mcp-server/src/stdio.ts | 5 +- 7 files changed, 420 insertions(+), 5 deletions(-) create mode 100644 packages/mcp-server/src/methods.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 1dbc9cb9..bff47edd 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -4,6 +4,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv } from './server'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { SdkMethod } from './methods'; import { Finch } from '@tryfinch/finch-api'; const prompt = `Runs JavaScript code to interact with the Finch API. @@ -36,7 +37,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(): McpTool { +export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -60,6 +61,24 @@ export function codeTool(): McpTool { const code = args.code as string; const intent = args.intent as string | undefined; + // 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 (blockedMatches.length > 0) { + return asErrorResult( + `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches + .map((m) => m.fullyQualifiedName) + .join(', ')}`, + ); + } + } + // 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'); diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index b2031368..8ca827c2 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -11,10 +11,12 @@ import { parseAuthHeaders } from './headers'; const newServer = async ({ clientOptions, + mcpOptions, req, res, }: { clientOptions: ClientOptions; + mcpOptions: McpOptions; req: express.Request; res: express.Response; }): Promise => { @@ -24,6 +26,7 @@ const newServer = async ({ const authOptions = parseAuthHeaders(req, false); await initMcpServer({ server: server, + mcpOptions: mcpOptions, clientOptions: { ...clientOptions, ...authOptions, diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index d75968e3..003a7655 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -18,7 +18,7 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(); + await launchStdioServer(options); break; case 'http': await launchStreamableHTTPServer({ diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts new file mode 100644 index 00000000..827dcf34 --- /dev/null +++ b/packages/mcp-server/src/methods.ts @@ -0,0 +1,364 @@ +import { McpOptions } from './options'; + +export type SdkMethod = { + clientCallName: string; + fullyQualifiedName: string; + httpMethod?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'query'; + httpPath?: string; +}; + +export const sdkMethods: SdkMethod[] = [ + { + clientCallName: 'client.accessTokens.create', + fullyQualifiedName: 'accessTokens.create', + httpMethod: 'post', + httpPath: '/auth/token', + }, + { + clientCallName: 'client.hris.company.retrieve', + fullyQualifiedName: 'hris.company.retrieve', + httpMethod: 'get', + httpPath: '/employer/company', + }, + { + clientCallName: 'client.hris.company.payStatementItem.list', + fullyQualifiedName: 'hris.company.payStatementItem.list', + httpMethod: 'get', + httpPath: '/employer/pay-statement-item', + }, + { + clientCallName: 'client.hris.company.payStatementItem.rules.create', + fullyQualifiedName: 'hris.company.payStatementItem.rules.create', + httpMethod: 'post', + httpPath: '/employer/pay-statement-item/rule', + }, + { + clientCallName: 'client.hris.company.payStatementItem.rules.update', + fullyQualifiedName: 'hris.company.payStatementItem.rules.update', + httpMethod: 'put', + httpPath: '/employer/pay-statement-item/rule/{rule_id}', + }, + { + clientCallName: 'client.hris.company.payStatementItem.rules.list', + fullyQualifiedName: 'hris.company.payStatementItem.rules.list', + httpMethod: 'get', + httpPath: '/employer/pay-statement-item/rule', + }, + { + clientCallName: 'client.hris.company.payStatementItem.rules.delete', + fullyQualifiedName: 'hris.company.payStatementItem.rules.delete', + httpMethod: 'delete', + httpPath: '/employer/pay-statement-item/rule/{rule_id}', + }, + { + clientCallName: 'client.hris.directory.list', + fullyQualifiedName: 'hris.directory.list', + httpMethod: 'get', + httpPath: '/employer/directory', + }, + { + clientCallName: 'client.hris.directory.listIndividuals', + fullyQualifiedName: 'hris.directory.listIndividuals', + }, + { + clientCallName: 'client.hris.individuals.retrieveMany', + fullyQualifiedName: 'hris.individuals.retrieveMany', + httpMethod: 'post', + httpPath: '/employer/individual', + }, + { + clientCallName: 'client.hris.employments.retrieveMany', + fullyQualifiedName: 'hris.employments.retrieveMany', + httpMethod: 'post', + httpPath: '/employer/employment', + }, + { + clientCallName: 'client.hris.payments.list', + fullyQualifiedName: 'hris.payments.list', + httpMethod: 'get', + httpPath: '/employer/payment', + }, + { + clientCallName: 'client.hris.payStatements.retrieveMany', + fullyQualifiedName: 'hris.payStatements.retrieveMany', + httpMethod: 'post', + httpPath: '/employer/pay-statement', + }, + { + clientCallName: 'client.hris.documents.list', + fullyQualifiedName: 'hris.documents.list', + httpMethod: 'get', + httpPath: '/employer/documents', + }, + { + clientCallName: 'client.hris.documents.retreive', + fullyQualifiedName: 'hris.documents.retreive', + httpMethod: 'get', + httpPath: '/employer/documents/{document_id}', + }, + { + clientCallName: 'client.hris.benefits.create', + fullyQualifiedName: 'hris.benefits.create', + httpMethod: 'post', + httpPath: '/employer/benefits', + }, + { + clientCallName: 'client.hris.benefits.retrieve', + fullyQualifiedName: 'hris.benefits.retrieve', + httpMethod: 'get', + httpPath: '/employer/benefits/{benefit_id}', + }, + { + clientCallName: 'client.hris.benefits.update', + fullyQualifiedName: 'hris.benefits.update', + httpMethod: 'post', + httpPath: '/employer/benefits/{benefit_id}', + }, + { + clientCallName: 'client.hris.benefits.list', + fullyQualifiedName: 'hris.benefits.list', + httpMethod: 'get', + httpPath: '/employer/benefits', + }, + { + clientCallName: 'client.hris.benefits.listSupportedBenefits', + fullyQualifiedName: 'hris.benefits.listSupportedBenefits', + httpMethod: 'get', + httpPath: '/employer/benefits/meta', + }, + { + clientCallName: 'client.hris.benefits.individuals.enrollMany', + fullyQualifiedName: 'hris.benefits.individuals.enrollMany', + httpMethod: 'post', + httpPath: '/employer/benefits/{benefit_id}/individuals', + }, + { + clientCallName: 'client.hris.benefits.individuals.enrolledIDs', + fullyQualifiedName: 'hris.benefits.individuals.enrolledIDs', + httpMethod: 'get', + httpPath: '/employer/benefits/{benefit_id}/enrolled', + }, + { + clientCallName: 'client.hris.benefits.individuals.retrieveManyBenefits', + fullyQualifiedName: 'hris.benefits.individuals.retrieveManyBenefits', + httpMethod: 'get', + httpPath: '/employer/benefits/{benefit_id}/individuals', + }, + { + clientCallName: 'client.hris.benefits.individuals.unenrollMany', + fullyQualifiedName: 'hris.benefits.individuals.unenrollMany', + httpMethod: 'delete', + httpPath: '/employer/benefits/{benefit_id}/individuals', + }, + { + clientCallName: 'client.providers.list', + fullyQualifiedName: 'providers.list', + httpMethod: 'get', + httpPath: '/providers', + }, + { + clientCallName: 'client.account.disconnect', + fullyQualifiedName: 'account.disconnect', + httpMethod: 'post', + httpPath: '/disconnect', + }, + { + clientCallName: 'client.account.introspect', + fullyQualifiedName: 'account.introspect', + httpMethod: 'get', + httpPath: '/introspect', + }, + { + clientCallName: 'client.requestForwarding.forward', + fullyQualifiedName: 'requestForwarding.forward', + httpMethod: 'post', + httpPath: '/forward', + }, + { + clientCallName: 'client.jobs.automated.create', + fullyQualifiedName: 'jobs.automated.create', + httpMethod: 'post', + httpPath: '/jobs/automated', + }, + { + clientCallName: 'client.jobs.automated.retrieve', + fullyQualifiedName: 'jobs.automated.retrieve', + httpMethod: 'get', + httpPath: '/jobs/automated/{job_id}', + }, + { + clientCallName: 'client.jobs.automated.list', + fullyQualifiedName: 'jobs.automated.list', + httpMethod: 'get', + httpPath: '/jobs/automated', + }, + { + clientCallName: 'client.jobs.manual.retrieve', + fullyQualifiedName: 'jobs.manual.retrieve', + httpMethod: 'get', + httpPath: '/jobs/manual/{job_id}', + }, + { + clientCallName: 'client.sandbox.connections.create', + fullyQualifiedName: 'sandbox.connections.create', + httpMethod: 'post', + httpPath: '/sandbox/connections', + }, + { + clientCallName: 'client.sandbox.connections.accounts.create', + fullyQualifiedName: 'sandbox.connections.accounts.create', + httpMethod: 'post', + httpPath: '/sandbox/connections/accounts', + }, + { + clientCallName: 'client.sandbox.connections.accounts.update', + fullyQualifiedName: 'sandbox.connections.accounts.update', + httpMethod: 'put', + httpPath: '/sandbox/connections/accounts', + }, + { + clientCallName: 'client.sandbox.company.update', + fullyQualifiedName: 'sandbox.company.update', + httpMethod: 'put', + httpPath: '/sandbox/company', + }, + { + clientCallName: 'client.sandbox.directory.create', + fullyQualifiedName: 'sandbox.directory.create', + httpMethod: 'post', + httpPath: '/sandbox/directory', + }, + { + clientCallName: 'client.sandbox.individual.update', + fullyQualifiedName: 'sandbox.individual.update', + httpMethod: 'put', + httpPath: '/sandbox/individual/{individual_id}', + }, + { + clientCallName: 'client.sandbox.employment.update', + fullyQualifiedName: 'sandbox.employment.update', + httpMethod: 'put', + httpPath: '/sandbox/employment/{individual_id}', + }, + { + clientCallName: 'client.sandbox.payment.create', + fullyQualifiedName: 'sandbox.payment.create', + httpMethod: 'post', + httpPath: '/sandbox/payment', + }, + { + clientCallName: 'client.sandbox.jobs.create', + fullyQualifiedName: 'sandbox.jobs.create', + httpMethod: 'post', + httpPath: '/sandbox/jobs', + }, + { + clientCallName: 'client.sandbox.jobs.configuration.retrieve', + fullyQualifiedName: 'sandbox.jobs.configuration.retrieve', + httpMethod: 'get', + httpPath: '/sandbox/jobs/configuration', + }, + { + clientCallName: 'client.sandbox.jobs.configuration.update', + fullyQualifiedName: 'sandbox.jobs.configuration.update', + httpMethod: 'put', + httpPath: '/sandbox/jobs/configuration', + }, + { + clientCallName: 'client.payroll.payGroups.retrieve', + fullyQualifiedName: 'payroll.payGroups.retrieve', + httpMethod: 'get', + httpPath: '/employer/pay-groups/{pay_group_id}', + }, + { + clientCallName: 'client.payroll.payGroups.list', + fullyQualifiedName: 'payroll.payGroups.list', + httpMethod: 'get', + httpPath: '/employer/pay-groups', + }, + { + clientCallName: 'client.connect.sessions.new', + fullyQualifiedName: 'connect.sessions.new', + httpMethod: 'post', + httpPath: '/connect/sessions', + }, + { + clientCallName: 'client.connect.sessions.reauthenticate', + fullyQualifiedName: 'connect.sessions.reauthenticate', + httpMethod: 'post', + httpPath: '/connect/sessions/reauthenticate', + }, +]; + +function allowedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + if (!options) { + return undefined; + } + + let allowedMethods: SdkMethod[]; + + if (options.codeAllowHttpGets || options.codeAllowedMethods) { + // Start with nothing allowed and then add into it from options + let allowedMethodsSet = new Set(); + + if (options.codeAllowHttpGets) { + // Add all methods that map to an HTTP GET + sdkMethods + .filter((method) => method.httpMethod === 'get') + .forEach((method) => allowedMethodsSet.add(method)); + } + + if (options.codeAllowedMethods) { + // Add all methods that match any of the allowed regexps + const allowedRegexps = options.codeAllowedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for allowed method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + sdkMethods + .filter((method) => allowedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName))) + .forEach((method) => allowedMethodsSet.add(method)); + } + + allowedMethods = Array.from(allowedMethodsSet); + } else { + // Start with everything allowed + allowedMethods = [...sdkMethods]; + } + + if (options.codeBlockedMethods) { + // Filter down based on blocked regexps + const blockedRegexps = options.codeBlockedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for blocked method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + allowedMethods = allowedMethods.filter( + (method) => !blockedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName)), + ); + } + + return allowedMethods; +} + +export function blockedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + const allowedMethods = allowedMethodsForCodeTool(options); + if (!allowedMethods) { + return undefined; + } + + const allowedSet = new Set(allowedMethods.map((method) => method.fullyQualifiedName)); + + // Return any methods that are not explicitly allowed + return sdkMethods.filter((method) => !allowedSet.has(method.fullyQualifiedName)); +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 74380833..92d1b074 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -12,10 +12,30 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + codeAllowHttpGets?: boolean | undefined; + codeAllowedMethods?: string[] | undefined; + codeBlockedMethods?: string[] | undefined; }; export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) + .option('code-allow-http-gets', { + type: 'boolean', + description: + 'Allow all code tool methods that map to HTTP GET operations. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-allowed-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly allow for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-blocked-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) .option('no-tools', { type: 'string', @@ -59,6 +79,9 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + codeAllowHttpGets: argv.codeAllowHttpGets, + codeAllowedMethods: argv.codeAllowedMethods, + codeBlockedMethods: argv.codeBlockedMethods, transport, port: argv.port, socket: argv.socket, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index ec0d5760..b906312a 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -12,6 +12,7 @@ import Finch from '@tryfinch/finch-api'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; +import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; @@ -148,7 +149,11 @@ export async function initMcpServer(params: { * Selects the tools to include in the MCP Server based on the provided options. */ export function selectTools(options?: McpOptions): McpTool[] { - const includedTools = [codeTool()]; + const includedTools = [ + codeTool({ + blockedMethods: blockedMethodsForCodeTool(options), + }), + ]; if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 47aeb0c9..57b99126 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,10 +1,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; -export const launchStdioServer = async () => { +export const launchStdioServer = async (mcpOptions: McpOptions) => { const server = await newMcpServer(); - await initMcpServer({ server }); + await initMcpServer({ server, mcpOptions }); const transport = new StdioServerTransport(); await server.connect(transport); From ec270924322d2fbb055eb36ae6b8b75c2102f89a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:10:13 +0000 Subject: [PATCH 16/21] chore(internal): avoid type checking errors with ts-reset --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 18c23fe0..46c8f7b2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -555,7 +555,7 @@ export class Finch { loggerFor(this).info(`${responseInfo} - ${retryMessage}`); const errText = await response.text().catch((err: any) => castToError(err).message); - const errJSON = safeJSON(errText); + const errJSON = safeJSON(errText) as any; const errMessage = errJSON ? undefined : errText; loggerFor(this).debug( From e307e2e1c806385c6aa5a3bdea69e690fde06d73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:33:57 +0000 Subject: [PATCH 17/21] chore(mcp): forward STAINLESS_API_KEY to docs search endpoint --- packages/mcp-server/src/docs-search-tool.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 4874ac4b..85d9c216 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; +import { readEnv } from './server'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -45,7 +46,12 @@ const docsSearchURL = export const handler = async (_: unknown, args: Record | undefined) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const result = await fetch(`${docsSearchURL}?${query}`); + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const result = await fetch(`${docsSearchURL}?${query}`, { + headers: { + ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + }, + }); if (!result.ok) { throw new Error( From efb8e625da8e577178a22350215ef3931ad657db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:12:47 +0000 Subject: [PATCH 18/21] chore(internal): improve layout of generated MCP server files --- .../mcp-server/src/{headers.ts => auth.ts} | 0 packages/mcp-server/src/code-tool.ts | 2 +- packages/mcp-server/src/docs-search-tool.ts | 2 +- packages/mcp-server/src/http.ts | 5 ++-- packages/mcp-server/src/methods.ts | 2 ++ packages/mcp-server/src/options.ts | 2 ++ packages/mcp-server/src/server.ts | 28 +------------------ packages/mcp-server/src/util.ts | 25 +++++++++++++++++ 8 files changed, 35 insertions(+), 31 deletions(-) rename packages/mcp-server/src/{headers.ts => auth.ts} (100%) create mode 100644 packages/mcp-server/src/util.ts diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/auth.ts similarity index 100% rename from packages/mcp-server/src/headers.ts rename to packages/mcp-server/src/auth.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index bff47edd..4e562414 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -2,7 +2,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { readEnv } from './server'; +import { readEnv } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; import { Finch } from '@tryfinch/finch-api'; diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 85d9c216..551dd739 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './server'; +import { readEnv } from './util'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 8ca827c2..05b3f4d9 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,12 +2,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ClientOptions } from '@tryfinch/finch-api'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; +import { parseAuthHeaders } from './auth'; import { McpOptions } from './options'; -import { ClientOptions, initMcpServer, newMcpServer } from './server'; -import { parseAuthHeaders } from './headers'; +import { initMcpServer, newMcpServer } from './server'; const newServer = async ({ clientOptions, diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts index 827dcf34..82258b2f 100644 --- a/packages/mcp-server/src/methods.ts +++ b/packages/mcp-server/src/methods.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import { McpOptions } from './options'; export type SdkMethod = { diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 92d1b074..cfde21d0 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index b906312a..0f13fe26 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -14,9 +14,7 @@ import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; - -export { McpOptions } from './options'; -export { ClientOptions } from '@tryfinch/finch-api'; +import { readEnv } from './util'; async function getInstructions() { // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. @@ -170,27 +168,3 @@ export async function executeHandler( ) { return await handler(client, args || {}); } - -export const readEnv = (env: string): string | undefined => { - if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim(); - } else if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); - } - return; -}; - -export const readEnvOrError = (env: string): string => { - let envValue = readEnv(env); - if (envValue === undefined) { - throw new Error(`Environment variable ${env} is not set`); - } - return envValue; -}; - -export const requireValue = (value: T | undefined, description: string): T => { - if (value === undefined) { - throw new Error(`Missing required value: ${description}`); - } - return value; -}; diff --git a/packages/mcp-server/src/util.ts b/packages/mcp-server/src/util.ts new file mode 100644 index 00000000..40ed5501 --- /dev/null +++ b/packages/mcp-server/src/util.ts @@ -0,0 +1,25 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim(); + } else if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); + } + return; +}; + +export const readEnvOrError = (env: string): string => { + let envValue = readEnv(env); + if (envValue === undefined) { + throw new Error(`Environment variable ${env} is not set`); + } + return envValue; +}; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; From 1ac688c453fda8fb75435c7640baee617ebe2391 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:37:27 +0000 Subject: [PATCH 19/21] 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 46c8f7b2..666a5963 100644 --- a/src/client.ts +++ b/src/client.ts @@ -829,6 +829,14 @@ export class Finch { (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 b54dd7599a900f5a37968d409581aa09092389e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:20:48 +0000 Subject: [PATCH 20/21] 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 5127806d..35e3b55e 100644 --- a/packages/mcp-server/src/auth.ts +++ b/packages/mcp-server/src/auth.ts @@ -2,8 +2,9 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from '@tryfinch/finch-api'; +import { McpOptions } from './options'; -export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { +export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { if (req.headers.authorization) { const scheme = req.headers.authorization.split(' ')[0]!; const value = req.headers.authorization.slice(scheme.length + 1); @@ -35,3 +36,17 @@ export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Part : req.headers['x-finch-client-secret']; return { clientID, clientSecret }; }; + +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 4e562414..af61d306 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 } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; -import { Finch } from '@tryfinch/finch-api'; const prompt = `Runs JavaScript code to interact with the Finch API. @@ -37,7 +43,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', @@ -57,19 +63,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M required: ['code'], }, }; - const handler = async (client: Finch, 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 @@ -79,16 +90,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({ FINCH_CLIENT_ID: readEnv('FINCH_CLIENT_ID') ?? client.clientID ?? undefined, diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 551dd739..76fc0b96 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/finch/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 05b3f4d9..a3ce7eb2 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -6,7 +6,7 @@ import { ClientOptions } from '@tryfinch/finch-api'; 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 cfde21d0..32a88713 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 0f13fe26..3656fe61 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/finch', { 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: 'tryfinch_finch_api_api', version: '9.0.0', }, { - 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: Finch, - 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 57b99126..ceccaed3 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 cdd3afa4..b42e9782 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: Finch, - args: Record | undefined, -) => Promise; +export type McpRequestContext = { + client: Finch; + stainlessApiKey?: string | undefined; +}; + +export type HandlerFunction = ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => Promise; export function asTextContentResult(result: unknown): ToolCallResult { return { From 10c20ac3322d3ad09d3ed1e7e92d7191b1c4d0b0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:21:19 +0000 Subject: [PATCH 21/21] release: 9.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++ package.json | 2 +- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 92856693..9695e0ec 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "9.0.0" + ".": "9.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7589c280..96a5e6d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 9.1.0 (2026-02-17) + +Full Changelog: [v9.0.0...v9.1.0](https://github.com/Finch-API/finch-api-node/compare/v9.0.0...v9.1.0) + +### Features + +* **mcp:** add initial server instructions ([41a62e7](https://github.com/Finch-API/finch-api-node/commit/41a62e7da2c6f6fe1b59afe8d87066489d707bc4)) + + +### Bug Fixes + +* **client:** avoid memory leak with abort signals ([e6cbcb4](https://github.com/Finch-API/finch-api-node/commit/e6cbcb4c380d391797875514114fbd870e680aa9)) +* **client:** avoid removing abort listener too early ([834599b](https://github.com/Finch-API/finch-api-node/commit/834599b8d10c0cc7858861504ebbc6142d28768d)) +* **docs:** fix mcp installation instructions for remote servers ([2346e45](https://github.com/Finch-API/finch-api-node/commit/2346e45e51546112f96d29f89904a499c66c61d2)) +* **mcp:** allow falling back for required env variables ([e2285bf](https://github.com/Finch-API/finch-api-node/commit/e2285bffd1403ecbb223daf47f7e1eda40033d54)) + + +### Chores + +* **client:** do not parse responses with empty content-length ([9168b0e](https://github.com/Finch-API/finch-api-node/commit/9168b0e266bb4babc638698f27c166d282c5f346)) +* **client:** restructure abort controller binding ([03776eb](https://github.com/Finch-API/finch-api-node/commit/03776eb5c0329b6580fb3c2e58ce4d811fbde635)) +* **internal/client:** fix form-urlencoded requests ([1ac688c](https://github.com/Finch-API/finch-api-node/commit/1ac688c453fda8fb75435c7640baee617ebe2391)) +* **internal:** add health check to MCP server when running in HTTP mode ([c6b1999](https://github.com/Finch-API/finch-api-node/commit/c6b1999a1895466414ad1c3975b27ee317faa473)) +* **internal:** allow basic filtering of methods allowed for MCP code mode ([c6921b7](https://github.com/Finch-API/finch-api-node/commit/c6921b76e9db6ddd6019353a6da69c12a43f8570)) +* **internal:** allow setting x-stainless-api-key header on mcp server requests ([b54dd75](https://github.com/Finch-API/finch-api-node/commit/b54dd7599a900f5a37968d409581aa09092389e4)) +* **internal:** always generate MCP server dockerfiles and upgrade associated dependencies ([d786340](https://github.com/Finch-API/finch-api-node/commit/d7863401c96e70f832d74a7eca4b7ef0850791e2)) +* **internal:** avoid type checking errors with ts-reset ([ec27092](https://github.com/Finch-API/finch-api-node/commit/ec270924322d2fbb055eb36ae6b8b75c2102f89a)) +* **internal:** codegen related update ([b3f5e09](https://github.com/Finch-API/finch-api-node/commit/b3f5e099f190c0dcdd648262398d4629fac02671)) +* **internal:** fix pagination internals not accepting option promises ([6b75209](https://github.com/Finch-API/finch-api-node/commit/6b7520906d83f1ebeaf76c2b827f34f8ebeeeca2)) +* **internal:** improve layout of generated MCP server files ([efb8e62](https://github.com/Finch-API/finch-api-node/commit/efb8e625da8e577178a22350215ef3931ad657db)) +* **internal:** refactor flag parsing for MCP servers and add debug flag ([c3685d5](https://github.com/Finch-API/finch-api-node/commit/c3685d56e5988db461e09f3ec90e693dbec3bdfa)) +* **internal:** support oauth authorization code flow for MCP servers ([ed2565a](https://github.com/Finch-API/finch-api-node/commit/ed2565a0da646b78e0f3d74bbe7d2f7c8c4c46bb)) +* **mcp:** forward STAINLESS_API_KEY to docs search endpoint ([e307e2e](https://github.com/Finch-API/finch-api-node/commit/e307e2e1c806385c6aa5a3bdea69e690fde06d73)) +* **mcp:** up tsconfig lib version to es2022 ([b96b21e](https://github.com/Finch-API/finch-api-node/commit/b96b21eccc21170a8c655ac8de964723952e9f93)) + ## 9.0.0 (2026-01-27) Full Changelog: [v8.2.1...v9.0.0](https://github.com/Finch-API/finch-api-node/compare/v8.2.1...v9.0.0) diff --git a/package.json b/package.json index 727a86fb..2cabc142 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryfinch/finch-api", - "version": "9.0.0", + "version": "9.1.0", "description": "The official TypeScript library for the Finch API", "author": "Finch ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 61a3cbde..9a60afd5 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@tryfinch/finch-api-mcp", - "version": "9.0.0", + "version": "9.1.0", "description": "The official MCP Server for the Finch API", "author": "Finch ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 3656fe61..be717b81 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: 'tryfinch_finch_api_api', - version: '9.0.0', + version: '9.1.0', }, { instructions: await getInstructions(stainlessApiKey), diff --git a/src/version.ts b/src/version.ts index b0a42860..024a40df 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '9.0.0'; // x-release-please-version +export const VERSION = '9.1.0'; // x-release-please-version