Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cb296df
chore(internal): bump @modelcontextprotocol/sdk, @hono/node-server, a…
stainless-app[bot] Mar 11, 2026
02fd3db
chore(internal): make generated MCP servers compatible with Cloudflar…
stainless-app[bot] Mar 13, 2026
9b52734
chore(internal): support x-stainless-mcp-client-envs header in MCP se…
stainless-app[bot] Mar 13, 2026
28d39dc
chore(internal): tweak CI branches
stainless-app[bot] Mar 16, 2026
262612e
chore(internal): support x-stainless-mcp-client-permissions headers i…
stainless-app[bot] Mar 16, 2026
1c6a81d
codegen metadata
stainless-app[bot] Mar 17, 2026
3ce965c
refactor(tests): switch from prism to steady
stainless-app[bot] Mar 19, 2026
b17943d
chore(tests): bump steady to v0.19.4
stainless-app[bot] Mar 20, 2026
724e159
chore(tests): bump steady to v0.19.5
stainless-app[bot] Mar 20, 2026
7e3dec1
chore(internal): update gitignore
stainless-app[bot] Mar 23, 2026
78e6075
chore(internal): fix MCP server TS errors that occur with required cl…
stainless-app[bot] Mar 23, 2026
d356e29
chore(tests): bump steady to v0.19.6
stainless-app[bot] Mar 23, 2026
21b6a9d
chore(ci): skip lint on metadata-only changes
stainless-app[bot] Mar 24, 2026
3729aa6
chore(tests): bump steady to v0.19.7
stainless-app[bot] Mar 24, 2026
c2d0485
codegen metadata
stainless-app[bot] Mar 24, 2026
136acd6
feat(api): api update
stainless-app[bot] Mar 25, 2026
4c22540
chore(internal): update multipart form array serialization
stainless-app[bot] Mar 26, 2026
54bc6f0
chore(internal): support custom-instructions-path flag in MCP servers
stainless-app[bot] Mar 26, 2026
43f868d
feat(api): api update
stainless-app[bot] Mar 27, 2026
9007942
release: 9.4.0
stainless-app[bot] Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
Expand All @@ -17,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/finch-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6

Expand All @@ -36,7 +38,7 @@ jobs:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/finch-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
permissions:
contents: read
id-token: write
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.prism.log
.stdy.log
node_modules
yarn-error.log
codegen.log
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "9.3.0"
".": "9.4.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 46
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-093ade6f1d3115b654a73b97855fbe334c9f9c5d906081dad2ec973ab0c0b24d.yml
openapi_spec_hash: 7cc27b8e483d9db9c411875289c42eb9
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-63947213d9359808abc05e4c3cb53389325ca23c58d06bf293626f7d5d4fc2b8.yml
openapi_spec_hash: 50e4669590de9a411915a612615017d0
config_hash: d21a244fc073152c8dbecb8ece970209
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Changelog

## 9.4.0 (2026-03-27)

Full Changelog: [v9.3.0...v9.4.0](https://github.com/Finch-API/finch-api-node/compare/v9.3.0...v9.4.0)

### Features

* **api:** api update ([43f868d](https://github.com/Finch-API/finch-api-node/commit/43f868dd50cd1eb77f0c9721666c6a1e0a6f18d9))
* **api:** api update ([136acd6](https://github.com/Finch-API/finch-api-node/commit/136acd675b35a62b8927f1f226e1fdd378b2c2ae))


### Chores

* **ci:** skip lint on metadata-only changes ([21b6a9d](https://github.com/Finch-API/finch-api-node/commit/21b6a9d81693134885a545d44f384c21f0f1fa77))
* **internal:** bump @modelcontextprotocol/sdk, @hono/node-server, and minimatch ([cb296df](https://github.com/Finch-API/finch-api-node/commit/cb296df1af875ebcd14eba927afa4db5c1e81ca4))
* **internal:** fix MCP server TS errors that occur with required client options ([78e6075](https://github.com/Finch-API/finch-api-node/commit/78e60750ebfe784f8a5131640331481322ab88f2))
* **internal:** make generated MCP servers compatible with Cloudflare worker environments ([02fd3db](https://github.com/Finch-API/finch-api-node/commit/02fd3db5144319fc1d3a0014a33c5844d5c871b8))
* **internal:** support custom-instructions-path flag in MCP servers ([54bc6f0](https://github.com/Finch-API/finch-api-node/commit/54bc6f0d5f49445f0ceff29e9d1f3401943deecd))
* **internal:** support x-stainless-mcp-client-envs header in MCP servers ([9b52734](https://github.com/Finch-API/finch-api-node/commit/9b52734c95eae05a66c70d0e59f49605a1290e38))
* **internal:** support x-stainless-mcp-client-permissions headers in MCP servers ([262612e](https://github.com/Finch-API/finch-api-node/commit/262612e3cafafd4a0d56b50c05cdd4840e19423c))
* **internal:** tweak CI branches ([28d39dc](https://github.com/Finch-API/finch-api-node/commit/28d39dc3053ee6981d0e2431ff117b361cb5db41))
* **internal:** update gitignore ([7e3dec1](https://github.com/Finch-API/finch-api-node/commit/7e3dec180f32f7d3af9b7bfa1048b7f65f3fe0e9))
* **internal:** update multipart form array serialization ([4c22540](https://github.com/Finch-API/finch-api-node/commit/4c22540369796ab42675176f59095cacbcd234e1))
* **tests:** bump steady to v0.19.4 ([b17943d](https://github.com/Finch-API/finch-api-node/commit/b17943dedc197a6f51ba3d7a99a9525e1d05d28c))
* **tests:** bump steady to v0.19.5 ([724e159](https://github.com/Finch-API/finch-api-node/commit/724e15968a7097123ccfe4b3e6419f580dfdbbaa))
* **tests:** bump steady to v0.19.6 ([d356e29](https://github.com/Finch-API/finch-api-node/commit/d356e29b624adb1c53d5fbcde14d75c878066938))
* **tests:** bump steady to v0.19.7 ([3729aa6](https://github.com/Finch-API/finch-api-node/commit/3729aa6f4970df2224cdc50682dfb347f566de9b))


### Refactors

* **tests:** switch from prism to steady ([3ce965c](https://github.com/Finch-API/finch-api-node/commit/3ce965cd47b579f708a38fe47b52e0e1635a42ea))

## 9.3.0 (2026-03-10)

Full Changelog: [v9.2.0...v9.3.0](https://github.com/Finch-API/finch-api-node/compare/v9.2.0...v9.3.0)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ $ pnpm link --global @tryfinch/finch-api

## Running tests

Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.

```sh
$ ./scripts/mock
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryfinch/finch-api",
"version": "9.3.0",
"version": "9.4.0",
"description": "The official TypeScript library for the Finch API",
"author": "Finch <founders@tryfinch.com>",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "@tryfinch/finch-api-mcp",
"version": "9.3.0",
"version": "9.4.0",
"description": "The official MCP Server for the Finch API",
"author": {
"name": "Finch",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryfinch/finch-api-mcp",
"version": "9.3.0",
"version": "9.4.0",
"description": "The official MCP Server for the Finch API",
"author": "Finch <founders@tryfinch.com>",
"types": "dist/index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion packages/mcp-server/src/code-tool-paths.cts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export const workerPath = require.resolve('./code-tool-worker.mjs');
export function getWorkerPath(): string {
return require.resolve('./code-tool-worker.mjs');
}
47 changes: 29 additions & 18 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
import { newDenoHTTPWorker } from '@valtown/deno-http-worker';
import { workerPath } from './code-tool-paths.cjs';
import {
ContentBlock,
McpRequestContext,
Expand Down Expand Up @@ -150,18 +145,22 @@ const remoteStainlessHandler = async ({

const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';

const localClientEnvs = {
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,
};
// Merge any upstream client envs from the request header, with upstream values taking precedence.
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };

// Setting a Stainless API key authenticates requests to the code tool endpoint.
const res = await fetch(codeModeEndpoint, {
method: 'POST',
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
'Content-Type': 'application/json',
'x-stainless-mcp-client-envs': JSON.stringify({
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,
}),
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
},
body: JSON.stringify({
project_name: 'finch',
Expand Down Expand Up @@ -204,6 +203,13 @@ const localDenoHandler = async ({
reqContext: McpRequestContext;
args: unknown;
}): Promise<ToolCallResult> => {
const fs = await import('node:fs');
const path = await import('node:path');
const url = await import('node:url');
const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
const { getWorkerPath } = await import('./code-tool-paths.cjs');
const workerPath = getWorkerPath();

const client = reqContext.client;
const baseURLHostname = new URL(client.baseURL).hostname;
const { code } = args as { code: string };
Expand Down Expand Up @@ -265,6 +271,9 @@ const localDenoHandler = async ({
printOutput: true,
spawnOptions: {
cwd: path.dirname(workerPath),
// Merge any upstream client envs into the Deno subprocess environment,
// with the upstream env vars taking precedence.
env: { ...process.env, ...reqContext.upstreamClientEnvs },
},
});

Expand All @@ -274,16 +283,18 @@ const localDenoHandler = async ({
reject(new Error(`Worker exited with code ${exitCode}`));
});

const opts: ClientOptions = {
baseURL: client.baseURL,
accessToken: client.accessToken,
clientID: client.clientID,
clientSecret: client.clientSecret,
webhookSecret: client.webhookSecret,
// Strip null/undefined values so that the worker SDK client can fall back to
// reading from environment variables (including any upstreamClientEnvs).
const opts = {
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
...(client.accessToken != null ? { accessToken: client.accessToken } : undefined),
...(client.clientID != null ? { clientID: client.clientID } : undefined),
...(client.clientSecret != null ? { clientSecret: client.clientSecret } : undefined),
...(client.webhookSecret != null ? { webhookSecret: client.webhookSecret } : undefined),
defaultHeaders: {
'X-Stainless-MCP': 'true',
},
};
} satisfies Partial<ClientOptions> as ClientOptions;

const req = worker.request(
'http://localhost',
Expand Down
49 changes: 46 additions & 3 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,61 @@ const newServer = async ({
res: express.Response;
}): Promise<McpServer | null> => {
const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
const server = await newMcpServer(stainlessApiKey);
const customInstructionsPath = mcpOptions.customInstructionsPath;
const server = await newMcpServer({ stainlessApiKey, customInstructionsPath });

const authOptions = parseClientAuthHeaders(req, false);

let upstreamClientEnvs: Record<string, string> | undefined;
const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs'];
if (typeof clientEnvsHeader === 'string') {
try {
const parsed = JSON.parse(clientEnvsHeader);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
upstreamClientEnvs = parsed;
}
} catch {
// Ignore malformed header
}
}

// Parse x-stainless-mcp-client-permissions header to override permission options
//
// Note: Permissions are best-effort and intended to prevent clients from doing unexpected things;
// they're not a hard security boundary, so we allow arbitrary, client-driven overrides.
//
// See the Stainless MCP documentation for more details.
let effectiveMcpOptions = mcpOptions;
const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions'];
if (typeof clientPermissionsHeader === 'string') {
try {
const parsed = JSON.parse(clientPermissionsHeader);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
effectiveMcpOptions = {
...mcpOptions,
...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }),
...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }),
...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }),
};
getLogger().info(
{ clientPermissions: parsed },
'Overriding code execution permissions from x-stainless-mcp-client-permissions header',
);
}
} catch (error) {
getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header');
}
}

await initMcpServer({
server: server,
mcpOptions: mcpOptions,
mcpOptions: effectiveMcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
stainlessApiKey: stainlessApiKey,
upstreamClientEnvs,
});

return server;
Expand Down Expand Up @@ -72,7 +115,7 @@ const del = async (req: express.Request, res: express.Response) => {
};

const redactHeaders = (headers: Record<string, any>) => {
const hiddenHeaders = /auth|cookie|key|token/i;
const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
const filtered = { ...headers };
Object.keys(filtered).forEach((key) => {
if (hiddenHeaders.test(key)) {
Expand Down
46 changes: 32 additions & 14 deletions packages/mcp-server/src/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import fs from 'fs/promises';
import { readEnv } from './util';
import { getLogger } from './logger';

Expand All @@ -12,33 +13,50 @@ interface InstructionsCacheEntry {

const instructionsCache = new Map<string, InstructionsCacheEntry>();

// Periodically evict stale entries so the cache doesn't grow unboundedly.
const _cacheCleanupInterval = setInterval(() => {
export async function getInstructions({
stainlessApiKey,
customInstructionsPath,
}: {
stainlessApiKey?: string | undefined;
customInstructionsPath?: string | undefined;
}): Promise<string> {
const now = Date.now();
const cacheKey = customInstructionsPath ?? stainlessApiKey ?? '';
const cached = instructionsCache.get(cacheKey);

if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
return cached.fetchedInstructions;
}

// Evict stale entries so the cache doesn't grow unboundedly.
for (const [key, entry] of instructionsCache) {
if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) {
instructionsCache.delete(key);
}
}
}, INSTRUCTIONS_CACHE_TTL_MS);

// Don't keep the process alive just for cleanup.
_cacheCleanupInterval.unref();
let fetchedInstructions: string;

export async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
const cacheKey = stainlessApiKey ?? '';
const cached = instructionsCache.get(cacheKey);

if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
return cached.fetchedInstructions;
if (customInstructionsPath) {
fetchedInstructions = await fetchLatestInstructionsFromFile(customInstructionsPath);
} else {
fetchedInstructions = await fetchLatestInstructionsFromApi(stainlessApiKey);
}

const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey);
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() });
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now });
return fetchedInstructions;
}

async function fetchLatestInstructions(stainlessApiKey: string | undefined): Promise<string> {
async function fetchLatestInstructionsFromFile(path: string): Promise<string> {
try {
return await fs.readFile(path, 'utf-8');
} catch (error) {
getLogger().error({ error, path }, 'Error fetching instructions from file');
throw error;
}
}

async function fetchLatestInstructionsFromApi(stainlessApiKey: string | undefined): Promise<string> {
// Setting the stainless API key is optional, but may be required
// to authenticate requests to the Stainless API.
const response = await fetch(
Expand Down
6 changes: 6 additions & 0 deletions packages/mcp-server/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type McpOptions = {
codeAllowedMethods?: string[] | undefined;
codeBlockedMethods?: string[] | undefined;
codeExecutionMode: McpCodeExecutionMode;
customInstructionsPath?: string | undefined;
};

export type McpCodeExecutionMode = 'stainless-sandbox' | 'local';
Expand Down Expand Up @@ -52,6 +53,10 @@ export function parseCLIOptions(): CLIOptions {
description:
"Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.",
})
.option('custom-instructions-path', {
type: 'string',
description: 'Path to custom instructions for the MCP server',
})
.option('debug', { type: 'boolean', description: 'Enable debug logging' })
.option('log-format', {
type: 'string',
Expand Down Expand Up @@ -117,6 +122,7 @@ export function parseCLIOptions(): CLIOptions {
codeAllowedMethods: argv.codeAllowedMethods,
codeBlockedMethods: argv.codeBlockedMethods,
codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode,
customInstructionsPath: argv.customInstructionsPath,
transport,
logFormat,
port: argv.port,
Expand Down
Loading
Loading