Skip to content

Commit b54dd75

Browse files
chore(internal): allow setting x-stainless-api-key header on mcp server requests
1 parent 1ac688c commit b54dd75

File tree

8 files changed

+109
-46
lines changed

8 files changed

+109
-46
lines changed

packages/mcp-server/src/auth.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { IncomingMessage } from 'node:http';
44
import { ClientOptions } from '@tryfinch/finch-api';
5+
import { McpOptions } from './options';
56

6-
export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
7+
export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
78
if (req.headers.authorization) {
89
const scheme = req.headers.authorization.split(' ')[0]!;
910
const value = req.headers.authorization.slice(scheme.length + 1);
@@ -35,3 +36,17 @@ export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Part
3536
: req.headers['x-finch-client-secret'];
3637
return { clientID, clientSecret };
3738
};
39+
40+
export const getStainlessApiKey = (req: IncomingMessage, mcpOptions: McpOptions): string | undefined => {
41+
// Try to get the key from the x-stainless-api-key header
42+
const headerKey =
43+
Array.isArray(req.headers['x-stainless-api-key']) ?
44+
req.headers['x-stainless-api-key'][0]
45+
: req.headers['x-stainless-api-key'];
46+
if (headerKey && typeof headerKey === 'string') {
47+
return headerKey;
48+
}
49+
50+
// Fall back to value set in the mcpOptions (e.g. from environment variable), if provided
51+
return mcpOptions.stainlessApiKey;
52+
};

packages/mcp-server/src/code-tool.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types';
3+
import {
4+
McpRequestContext,
5+
McpTool,
6+
Metadata,
7+
ToolCallResult,
8+
asErrorResult,
9+
asTextContentResult,
10+
} from './types';
411
import { Tool } from '@modelcontextprotocol/sdk/types.js';
512
import { readEnv } from './util';
613
import { WorkerInput, WorkerOutput } from './code-tool-types';
714
import { SdkMethod } from './methods';
8-
import { Finch } from '@tryfinch/finch-api';
915

1016
const prompt = `Runs JavaScript code to interact with the Finch API.
1117
@@ -37,7 +43,7 @@ Variables will not persist between calls, so make sure to return or log any data
3743
*
3844
* @param endpoints - The endpoints to include in the list.
3945
*/
40-
export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool {
46+
export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool {
4147
const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] };
4248
const tool: Tool = {
4349
name: 'execute',
@@ -57,19 +63,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M
5763
required: ['code'],
5864
},
5965
};
60-
const handler = async (client: Finch, args: any): Promise<ToolCallResult> => {
66+
const handler = async ({
67+
reqContext,
68+
args,
69+
}: {
70+
reqContext: McpRequestContext;
71+
args: any;
72+
}): Promise<ToolCallResult> => {
6173
const code = args.code as string;
6274
const intent = args.intent as string | undefined;
75+
const client = reqContext.client;
6376

6477
// Do very basic blocking of code that includes forbidden method names.
6578
//
6679
// WARNING: This is not secure against obfuscation and other evasion methods. If
6780
// stronger security blocks are required, then these should be enforced in the downstream
6881
// API (e.g., by having users call the MCP server with API keys with limited permissions).
69-
if (params.blockedMethods) {
70-
const blockedMatches = params.blockedMethods.filter((method) =>
71-
code.includes(method.fullyQualifiedName),
72-
);
82+
if (blockedMethods) {
83+
const blockedMatches = blockedMethods.filter((method) => code.includes(method.fullyQualifiedName));
7384
if (blockedMatches.length > 0) {
7485
return asErrorResult(
7586
`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
7990
}
8091
}
8192

82-
// this is not required, but passing a Stainless API key for the matching project_name
83-
// will allow you to run code-mode queries against non-published versions of your SDK.
84-
const stainlessAPIKey = readEnv('STAINLESS_API_KEY');
8593
const codeModeEndpoint =
8694
readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
8795

96+
// Setting a Stainless API key authenticates requests to the code tool endpoint.
8897
const res = await fetch(codeModeEndpoint, {
8998
method: 'POST',
9099
headers: {
91-
...(stainlessAPIKey && { Authorization: stainlessAPIKey }),
100+
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
92101
'Content-Type': 'application/json',
93102
client_envs: JSON.stringify({
94103
FINCH_CLIENT_ID: readEnv('FINCH_CLIENT_ID') ?? client.clientID ?? undefined,

packages/mcp-server/src/docs-search-tool.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import { Metadata, asTextContentResult } from './types';
4-
import { readEnv } from './util';
5-
3+
import { Metadata, McpRequestContext, asTextContentResult } from './types';
64
import { Tool } from '@modelcontextprotocol/sdk/types.js';
75

86
export const metadata: Metadata = {
@@ -43,13 +41,18 @@ export const tool: Tool = {
4341
const docsSearchURL =
4442
process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/finch/docs/search';
4543

46-
export const handler = async (_: unknown, args: Record<string, unknown> | undefined) => {
44+
export const handler = async ({
45+
reqContext,
46+
args,
47+
}: {
48+
reqContext: McpRequestContext;
49+
args: Record<string, unknown> | undefined;
50+
}) => {
4751
const body = args as any;
4852
const query = new URLSearchParams(body).toString();
49-
const stainlessAPIKey = readEnv('STAINLESS_API_KEY');
5053
const result = await fetch(`${docsSearchURL}?${query}`, {
5154
headers: {
52-
...(stainlessAPIKey && { Authorization: stainlessAPIKey }),
55+
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
5356
},
5457
});
5558

packages/mcp-server/src/http.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ClientOptions } from '@tryfinch/finch-api';
66
import express from 'express';
77
import morgan from 'morgan';
88
import morganBody from 'morgan-body';
9-
import { parseAuthHeaders } from './auth';
9+
import { getStainlessApiKey, parseClientAuthHeaders } from './auth';
1010
import { McpOptions } from './options';
1111
import { initMcpServer, newMcpServer } from './server';
1212

@@ -21,17 +21,20 @@ const newServer = async ({
2121
req: express.Request;
2222
res: express.Response;
2323
}): Promise<McpServer | null> => {
24-
const server = await newMcpServer();
24+
const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
25+
const server = await newMcpServer(stainlessApiKey);
2526

2627
try {
27-
const authOptions = parseAuthHeaders(req, false);
28+
const authOptions = parseClientAuthHeaders(req, false);
29+
2830
await initMcpServer({
2931
server: server,
3032
mcpOptions: mcpOptions,
3133
clientOptions: {
3234
...clientOptions,
3335
...authOptions,
3436
},
37+
stainlessApiKey: stainlessApiKey,
3538
});
3639
} catch (error) {
3740
res.status(401).json({
@@ -112,20 +115,24 @@ export const streamableHTTPApp = ({
112115
return app;
113116
};
114117

115-
export const launchStreamableHTTPServer = async (params: {
118+
export const launchStreamableHTTPServer = async ({
119+
mcpOptions,
120+
debug,
121+
port,
122+
}: {
116123
mcpOptions: McpOptions;
117124
debug: boolean;
118125
port: number | string | undefined;
119126
}) => {
120-
const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug });
121-
const server = app.listen(params.port);
127+
const app = streamableHTTPApp({ mcpOptions, debug });
128+
const server = app.listen(port);
122129
const address = server.address();
123130

124131
if (typeof address === 'string') {
125132
console.error(`MCP Server running on streamable HTTP at ${address}`);
126133
} else if (address !== null) {
127134
console.error(`MCP Server running on streamable HTTP on port ${address.port}`);
128135
} else {
129-
console.error(`MCP Server running on streamable HTTP on port ${params.port}`);
136+
console.error(`MCP Server running on streamable HTTP on port ${port}`);
130137
}
131138
};

packages/mcp-server/src/options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import qs from 'qs';
44
import yargs from 'yargs';
55
import { hideBin } from 'yargs/helpers';
66
import z from 'zod';
7+
import { readEnv } from './util';
78

89
export type CLIOptions = McpOptions & {
910
debug: boolean;
@@ -14,6 +15,7 @@ export type CLIOptions = McpOptions & {
1415

1516
export type McpOptions = {
1617
includeDocsTools?: boolean | undefined;
18+
stainlessApiKey?: string | undefined;
1719
codeAllowHttpGets?: boolean | undefined;
1820
codeAllowedMethods?: string[] | undefined;
1921
codeBlockedMethods?: string[] | undefined;
@@ -51,6 +53,12 @@ export function parseCLIOptions(): CLIOptions {
5153
description: 'Port to serve on if using http transport',
5254
})
5355
.option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' })
56+
.option('stainless-api-key', {
57+
type: 'string',
58+
default: readEnv('STAINLESS_API_KEY'),
59+
description:
60+
'API key for Stainless. Used to authenticate requests to Stainless-hosted tools endpoints.',
61+
})
5462
.option('tools', {
5563
type: 'string',
5664
array: true,
@@ -81,6 +89,7 @@ export function parseCLIOptions(): CLIOptions {
8189
return {
8290
...(includeDocsTools !== undefined && { includeDocsTools }),
8391
debug: !!argv.debug,
92+
stainlessApiKey: argv.stainlessApiKey,
8493
codeAllowHttpGets: argv.codeAllowHttpGets,
8594
codeAllowedMethods: argv.codeAllowedMethods,
8695
codeBlockedMethods: argv.codeBlockedMethods,

packages/mcp-server/src/server.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ import { codeTool } from './code-tool';
1313
import docsSearchTool from './docs-search-tool';
1414
import { McpOptions } from './options';
1515
import { blockedMethodsForCodeTool } from './methods';
16-
import { HandlerFunction, McpTool } from './types';
16+
import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types';
1717
import { readEnv } from './util';
1818

19-
async function getInstructions() {
20-
// This API key is optional; providing it allows the server to fetch instructions for unreleased versions.
21-
const stainlessAPIKey = readEnv('STAINLESS_API_KEY');
19+
async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
20+
// Setting the stainless API key is optional, but may be required
21+
// to authenticate requests to the Stainless API.
2222
const response = await fetch(
2323
readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/finch',
2424
{
2525
method: 'GET',
26-
headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) },
26+
headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) },
2727
},
2828
);
2929

@@ -52,14 +52,14 @@ async function getInstructions() {
5252
return instructions;
5353
}
5454

55-
export const newMcpServer = async () =>
55+
export const newMcpServer = async (stainlessApiKey: string | undefined) =>
5656
new McpServer(
5757
{
5858
name: 'tryfinch_finch_api_api',
5959
version: '9.0.0',
6060
},
6161
{
62-
instructions: await getInstructions(),
62+
instructions: await getInstructions(stainlessApiKey),
6363
capabilities: { tools: {}, logging: {} },
6464
},
6565
);
@@ -72,6 +72,7 @@ export async function initMcpServer(params: {
7272
server: Server | McpServer;
7373
clientOptions?: ClientOptions;
7474
mcpOptions?: McpOptions;
75+
stainlessApiKey?: string | undefined;
7576
}) {
7677
const server = params.server instanceof McpServer ? params.server.server : params.server;
7778

@@ -116,7 +117,14 @@ export async function initMcpServer(params: {
116117
throw new Error(`Unknown tool: ${name}`);
117118
}
118119

119-
return executeHandler(mcpTool.handler, client, args);
120+
return executeHandler({
121+
handler: mcpTool.handler,
122+
reqContext: {
123+
client,
124+
stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey,
125+
},
126+
args,
127+
});
120128
});
121129

122130
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
@@ -161,10 +169,14 @@ export function selectTools(options?: McpOptions): McpTool[] {
161169
/**
162170
* Runs the provided handler with the given client and arguments.
163171
*/
164-
export async function executeHandler(
165-
handler: HandlerFunction,
166-
client: Finch,
167-
args: Record<string, unknown> | undefined,
168-
) {
169-
return await handler(client, args || {});
172+
export async function executeHandler({
173+
handler,
174+
reqContext,
175+
args,
176+
}: {
177+
handler: HandlerFunction;
178+
reqContext: McpRequestContext;
179+
args: Record<string, unknown> | undefined;
180+
}): Promise<ToolCallResult> {
181+
return await handler({ reqContext, args: args || {} });
170182
}

packages/mcp-server/src/stdio.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { McpOptions } from './options';
33
import { initMcpServer, newMcpServer } from './server';
44

55
export const launchStdioServer = async (mcpOptions: McpOptions) => {
6-
const server = await newMcpServer();
6+
const server = await newMcpServer(mcpOptions.stainlessApiKey);
77

8-
await initMcpServer({ server, mcpOptions });
8+
await initMcpServer({ server, mcpOptions, stainlessApiKey: mcpOptions.stainlessApiKey });
99

1010
const transport = new StdioServerTransport();
1111
await server.connect(transport);

packages/mcp-server/src/types.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,18 @@ export type ToolCallResult = {
4242
isError?: boolean;
4343
};
4444

45-
export type HandlerFunction = (
46-
client: Finch,
47-
args: Record<string, unknown> | undefined,
48-
) => Promise<ToolCallResult>;
45+
export type McpRequestContext = {
46+
client: Finch;
47+
stainlessApiKey?: string | undefined;
48+
};
49+
50+
export type HandlerFunction = ({
51+
reqContext,
52+
args,
53+
}: {
54+
reqContext: McpRequestContext;
55+
args: Record<string, unknown> | undefined;
56+
}) => Promise<ToolCallResult>;
4957

5058
export function asTextContentResult(result: unknown): ToolCallResult {
5159
return {

0 commit comments

Comments
 (0)