Skip to content

Commit c3b5362

Browse files
chore(internal): codegen related update
1 parent fa2b60a commit c3b5362

11 files changed

Lines changed: 193 additions & 50 deletions

File tree

packages/mcp-server/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
"express": "^5.1.0",
4040
"fuse.js": "^7.1.0",
4141
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz",
42-
"morgan": "^1.10.0",
43-
"morgan-body": "^2.6.9",
42+
"pino": "^10.3.1",
43+
"pino-http": "^11.0.0",
44+
"pino-pretty": "^13.1.3",
4445
"qs": "^6.14.1",
4546
"typescript": "5.8.3",
4647
"yargs": "^17.7.2",
@@ -57,7 +58,6 @@
5758
"@types/cors": "^2.8.19",
5859
"@types/express": "^5.0.3",
5960
"@types/jest": "^29.4.0",
60-
"@types/morgan": "^1.9.10",
6161
"@types/qs": "^6.14.0",
6262
"@types/yargs": "^17.0.8",
6363
"@typescript-eslint/eslint-plugin": "8.31.1",

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import {
1717
import { Tool } from '@modelcontextprotocol/sdk/types.js';
1818
import { readEnv } from './util';
1919
import { WorkerInput, WorkerOutput } from './code-tool-types';
20+
import { getLogger } from './logger';
2021
import { SdkMethod } from './methods';
2122
import { McpCodeExecutionMode } from './options';
2223
import { ClientOptions } from '@tryfinch/finch-api';
2324

2425
const prompt = `Runs JavaScript code to interact with the Finch API.
2526
26-
You are a skilled programmer writing code to interface with the service.
27+
You are a skilled TypeScript programmer writing code to interface with the service.
2728
Define an async function named "run" that takes a single parameter of an initialized SDK client and it will be run.
2829
For example:
2930
@@ -40,7 +41,9 @@ You will be returned anything that your function returns, plus the results of an
4041
Do not add try-catch blocks for single API calls. The tool will handle errors for you.
4142
Do not add comments unless necessary for generating better code.
4243
Code will run in a container, and cannot interact with the network outside of the given SDK client.
43-
Variables will not persist between calls, so make sure to return or log any data you might need later.`;
44+
Variables will not persist between calls, so make sure to return or log any data you might need later.
45+
Remember that you are writing TypeScript code, so you need to be careful with your types.
46+
Always type dynamic key-value stores explicitly as Record<string, YourValueType> instead of {}.`;
4447

4548
/**
4649
* A tool that runs code against a copy of the SDK.
@@ -82,6 +85,8 @@ export function codeTool({
8285
},
8386
};
8487

88+
const logger = getLogger();
89+
8590
const handler = async ({
8691
reqContext,
8792
args,
@@ -106,11 +111,27 @@ export function codeTool({
106111
}
107112
}
108113

114+
let result: ToolCallResult;
115+
const startTime = Date.now();
116+
109117
if (codeExecutionMode === 'local') {
110-
return await localDenoHandler({ reqContext, args });
118+
logger.debug('Executing code in local Deno environment');
119+
result = await localDenoHandler({ reqContext, args });
111120
} else {
112-
return await remoteStainlessHandler({ reqContext, args });
121+
logger.debug('Executing code in remote Stainless environment');
122+
result = await remoteStainlessHandler({ reqContext, args });
113123
}
124+
125+
logger.info(
126+
{
127+
codeExecutionMode,
128+
durationMs: Date.now() - startTime,
129+
isError: result.isError,
130+
contentRows: result.content?.length ?? 0,
131+
},
132+
'Got code tool execution result',
133+
);
134+
return result;
114135
};
115136

116137
return { metadata, tool, handler };
@@ -135,7 +156,7 @@ const remoteStainlessHandler = async ({
135156
headers: {
136157
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
137158
'Content-Type': 'application/json',
138-
client_envs: JSON.stringify({
159+
'x-stainless-mcp-client-envs': JSON.stringify({
139160
FINCH_CLIENT_ID: readEnv('FINCH_CLIENT_ID') ?? client.clientID ?? undefined,
140161
FINCH_CLIENT_SECRET: readEnv('FINCH_CLIENT_SECRET') ?? client.clientSecret ?? undefined,
141162
FINCH_WEBHOOK_SECRET: readEnv('FINCH_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
@@ -151,6 +172,11 @@ const remoteStainlessHandler = async ({
151172
});
152173

153174
if (!res.ok) {
175+
if (res.status === 404 && !reqContext.stainlessApiKey) {
176+
throw new Error(
177+
'Could not access code tool for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
178+
);
179+
}
154180
throw new Error(
155181
`${res.status}: ${
156182
res.statusText

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

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

3-
import { Metadata, McpRequestContext, asTextContentResult } from './types';
43
import { Tool } from '@modelcontextprotocol/sdk/types.js';
4+
import { Metadata, McpRequestContext, asTextContentResult } from './types';
5+
import { getLogger } from './logger';
56

67
export const metadata: Metadata = {
78
resource: 'all',
@@ -50,19 +51,49 @@ export const handler = async ({
5051
}) => {
5152
const body = args as any;
5253
const query = new URLSearchParams(body).toString();
54+
55+
const startTime = Date.now();
5356
const result = await fetch(`${docsSearchURL}?${query}`, {
5457
headers: {
5558
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
5659
},
5760
});
5861

62+
const logger = getLogger();
63+
5964
if (!result.ok) {
65+
const errorText = await result.text();
66+
logger.warn(
67+
{
68+
durationMs: Date.now() - startTime,
69+
query: body.query,
70+
status: result.status,
71+
statusText: result.statusText,
72+
errorText,
73+
},
74+
'Got error response from docs search tool',
75+
);
76+
77+
if (result.status === 404 && !reqContext.stainlessApiKey) {
78+
throw new Error(
79+
'Could not find docs for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
80+
);
81+
}
82+
6083
throw new Error(
61-
`${result.status}: ${result.statusText} when using doc search tool. Details: ${await result.text()}`,
84+
`${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`,
6285
);
6386
}
6487

65-
return asTextContentResult(await result.json());
88+
const resultBody = await result.json();
89+
logger.info(
90+
{
91+
durationMs: Date.now() - startTime,
92+
query: body.query,
93+
},
94+
'Got docs search result',
95+
);
96+
return asTextContentResult(resultBody);
6697
};
6798

6899
export default { metadata, tool, handler };

packages/mcp-server/src/http.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
44
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
55
import { ClientOptions } from '@tryfinch/finch-api';
66
import express from 'express';
7-
import morgan from 'morgan';
8-
import morganBody from 'morgan-body';
7+
import pino from 'pino';
8+
import pinoHttp from 'pino-http';
99
import { getStainlessApiKey, parseClientAuthHeaders } from './auth';
10+
import { getLogger } from './logger';
1011
import { McpOptions } from './options';
1112
import { initMcpServer, newMcpServer } from './server';
1213

@@ -70,29 +71,60 @@ const del = async (req: express.Request, res: express.Response) => {
7071
});
7172
};
7273

74+
const redactHeaders = (headers: Record<string, any>) => {
75+
const hiddenHeaders = /auth|cookie|key|token/i;
76+
const filtered = { ...headers };
77+
Object.keys(filtered).forEach((key) => {
78+
if (hiddenHeaders.test(key)) {
79+
filtered[key] = '[REDACTED]';
80+
}
81+
});
82+
return filtered;
83+
};
84+
7385
export const streamableHTTPApp = ({
7486
clientOptions = {},
7587
mcpOptions,
76-
debug,
7788
}: {
7889
clientOptions?: ClientOptions;
7990
mcpOptions: McpOptions;
80-
debug: boolean;
8191
}): express.Express => {
8292
const app = express();
8393
app.set('query parser', 'extended');
8494
app.use(express.json());
85-
86-
if (debug) {
87-
morganBody(app, {
88-
logAllReqHeader: true,
89-
logAllResHeader: true,
90-
logRequestBody: true,
91-
logResponseBody: true,
92-
});
93-
} else {
94-
app.use(morgan('combined'));
95-
}
95+
app.use(
96+
pinoHttp({
97+
logger: getLogger(),
98+
customLogLevel: (req, res) => {
99+
if (res.statusCode >= 500) {
100+
return 'error';
101+
} else if (res.statusCode >= 400) {
102+
return 'warn';
103+
}
104+
return 'info';
105+
},
106+
customSuccessMessage: function (req, res) {
107+
return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`;
108+
},
109+
customErrorMessage: function (req, res, err) {
110+
return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`;
111+
},
112+
serializers: {
113+
req: pino.stdSerializers.wrapRequestSerializer((req) => {
114+
return {
115+
...req,
116+
headers: redactHeaders(req.raw.headers),
117+
};
118+
}),
119+
res: pino.stdSerializers.wrapResponseSerializer((res) => {
120+
return {
121+
...res,
122+
headers: redactHeaders(res.headers),
123+
};
124+
}),
125+
},
126+
}),
127+
);
96128

97129
app.get('/health', async (req: express.Request, res: express.Response) => {
98130
res.status(200).send('OK');
@@ -106,22 +138,22 @@ export const streamableHTTPApp = ({
106138

107139
export const launchStreamableHTTPServer = async ({
108140
mcpOptions,
109-
debug,
110141
port,
111142
}: {
112143
mcpOptions: McpOptions;
113-
debug: boolean;
114144
port: number | string | undefined;
115145
}) => {
116-
const app = streamableHTTPApp({ mcpOptions, debug });
146+
const app = streamableHTTPApp({ mcpOptions });
117147
const server = app.listen(port);
118148
const address = server.address();
119149

150+
const logger = getLogger();
151+
120152
if (typeof address === 'string') {
121-
console.error(`MCP Server running on streamable HTTP at ${address}`);
153+
logger.info(`MCP Server running on streamable HTTP at ${address}`);
122154
} else if (address !== null) {
123-
console.error(`MCP Server running on streamable HTTP on port ${address.port}`);
155+
logger.info(`MCP Server running on streamable HTTP on port ${address.port}`);
124156
} else {
125-
console.error(`MCP Server running on streamable HTTP on port ${port}`);
157+
logger.info(`MCP Server running on streamable HTTP on port ${port}`);
126158
}
127159
};

packages/mcp-server/src/index.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ import { McpOptions, parseCLIOptions } from './options';
55
import { launchStdioServer } from './stdio';
66
import { launchStreamableHTTPServer } from './http';
77
import type { McpTool } from './types';
8+
import { configureLogger, getLogger } from './logger';
89

910
async function main() {
1011
const options = parseOptionsOrError();
12+
configureLogger({
13+
level: options.debug ? 'debug' : 'info',
14+
pretty: options.logFormat === 'pretty',
15+
});
1116

1217
const selectedTools = await selectToolsOrError(options);
1318

14-
console.error(
15-
`MCP Server starting with ${selectedTools.length} tools:`,
16-
selectedTools.map((e) => e.tool.name),
19+
getLogger().info(
20+
{ tools: selectedTools.map((e) => e.tool.name) },
21+
`MCP Server starting with ${selectedTools.length} tools`,
1722
);
1823

1924
switch (options.transport) {
@@ -23,7 +28,6 @@ async function main() {
2328
case 'http':
2429
await launchStreamableHTTPServer({
2530
mcpOptions: options,
26-
debug: options.debug,
2731
port: options.socket ?? options.port,
2832
});
2933
break;
@@ -32,7 +36,8 @@ async function main() {
3236

3337
if (require.main === module) {
3438
main().catch((error) => {
35-
console.error('Fatal error in main():', error);
39+
// Logger might not be initialized yet
40+
console.error('Fatal error in main()', error);
3641
process.exit(1);
3742
});
3843
}
@@ -41,7 +46,8 @@ function parseOptionsOrError() {
4146
try {
4247
return parseCLIOptions();
4348
} catch (error) {
44-
console.error('Error parsing options:', error);
49+
// Logger is initialized after options, so use console.error here
50+
console.error('Error parsing options', error);
4551
process.exit(1);
4652
}
4753
}
@@ -50,16 +56,12 @@ async function selectToolsOrError(options: McpOptions): Promise<McpTool[]> {
5056
try {
5157
const includedTools = selectTools(options);
5258
if (includedTools.length === 0) {
53-
console.error('No tools match the provided filters.');
59+
getLogger().error('No tools match the provided filters');
5460
process.exit(1);
5561
}
5662
return includedTools;
5763
} catch (error) {
58-
if (error instanceof Error) {
59-
console.error('Error filtering tools:', error.message);
60-
} else {
61-
console.error('Error filtering tools:', error);
62-
}
64+
getLogger().error({ error }, 'Error filtering tools');
6365
process.exit(1);
6466
}
6567
}

packages/mcp-server/src/instructions.ts

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

33
import { readEnv } from './util';
4+
import { getLogger } from './logger';
45

56
const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
67

@@ -50,7 +51,7 @@ async function fetchLatestInstructions(stainlessApiKey: string | undefined): Pro
5051

5152
let instructions: string | undefined;
5253
if (!response.ok) {
53-
console.warn(
54+
getLogger().warn(
5455
'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...',
5556
);
5657

0 commit comments

Comments
 (0)