@metronome/mcp 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/code-tool-paths.cjs +4 -2
- package/code-tool-paths.cjs.map +1 -1
- package/code-tool-paths.d.cts +1 -1
- package/code-tool-paths.d.cts.map +1 -1
- package/code-tool-worker.d.mts.map +1 -1
- package/code-tool-worker.d.ts.map +1 -1
- package/code-tool-worker.js +45 -3
- package/code-tool-worker.js.map +1 -1
- package/code-tool-worker.mjs +12 -3
- package/code-tool-worker.mjs.map +1 -1
- package/code-tool.d.mts.map +1 -1
- package/code-tool.d.ts.map +1 -1
- package/code-tool.js +31 -25
- package/code-tool.js.map +1 -1
- package/code-tool.mjs +22 -13
- package/code-tool.mjs.map +1 -1
- package/docs-search-tool.d.mts +2 -0
- package/docs-search-tool.d.mts.map +1 -1
- package/docs-search-tool.d.ts +2 -0
- package/docs-search-tool.d.ts.map +1 -1
- package/docs-search-tool.js +32 -2
- package/docs-search-tool.js.map +1 -1
- package/docs-search-tool.mjs +31 -2
- package/docs-search-tool.mjs.map +1 -1
- package/http.d.mts.map +1 -1
- package/http.d.ts.map +1 -1
- package/http.js +65 -3
- package/http.js.map +1 -1
- package/http.mjs +65 -3
- package/http.mjs.map +1 -1
- package/instructions.d.mts +4 -1
- package/instructions.d.mts.map +1 -1
- package/instructions.d.ts +4 -1
- package/instructions.d.ts.map +1 -1
- package/instructions.js +29 -14
- package/instructions.js.map +1 -1
- package/instructions.mjs +26 -14
- package/instructions.mjs.map +1 -1
- package/local-docs-search.d.mts +28 -0
- package/local-docs-search.d.mts.map +1 -0
- package/local-docs-search.d.ts +28 -0
- package/local-docs-search.d.ts.map +1 -0
- package/local-docs-search.js +4780 -0
- package/local-docs-search.js.map +1 -0
- package/local-docs-search.mjs +4740 -0
- package/local-docs-search.mjs.map +1 -0
- package/options.d.mts +3 -0
- package/options.d.mts.map +1 -1
- package/options.d.ts +3 -0
- package/options.d.ts.map +1 -1
- package/options.js +19 -0
- package/options.js.map +1 -1
- package/options.mjs +19 -0
- package/options.mjs.map +1 -1
- package/package.json +13 -2
- package/server.d.mts +10 -1
- package/server.d.mts.map +1 -1
- package/server.d.ts +10 -1
- package/server.d.ts.map +1 -1
- package/server.js +13 -3
- package/server.js.map +1 -1
- package/server.mjs +13 -3
- package/server.mjs.map +1 -1
- package/src/code-tool-paths.cts +3 -1
- package/src/code-tool-worker.ts +12 -3
- package/src/code-tool.ts +29 -18
- package/src/docs-search-tool.ts +46 -8
- package/src/http.ts +71 -3
- package/src/instructions.ts +33 -15
- package/src/local-docs-search.ts +5674 -0
- package/src/options.ts +24 -0
- package/src/server.ts +23 -3
- package/src/stdio.ts +4 -1
- package/src/types.ts +3 -0
- package/src/util.ts +2 -2
- package/stdio.d.mts.map +1 -1
- package/stdio.d.ts.map +1 -1
- package/stdio.js +4 -1
- package/stdio.js.map +1 -1
- package/stdio.mjs +4 -1
- package/stdio.mjs.map +1 -1
- package/types.d.mts +6 -0
- package/types.d.mts.map +1 -1
- package/types.d.ts +6 -0
- package/types.d.ts.map +1 -1
- package/types.js.map +1 -1
- package/types.mjs.map +1 -1
- package/util.js +2 -2
- package/util.js.map +1 -1
- package/util.mjs +2 -2
- package/util.mjs.map +1 -1
package/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","sourceRoot":"","sources":["src/server.ts"],"names":[],"mappings":"AAAA,sFAAsF;OAG/E,EAAE,SAAS,EAAE,MAAM,yCAAyC;OAC5D,EACL,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,oCAAoC;OAEpC,SAAS,MAAM,gBAAgB;OAC/B,EAAE,QAAQ,EAAE;OACZ,cAAc;OACd,EAAE,eAAe,EAAE;OAEnB,EAAE,yBAAyB,EAAE;AAGpC,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"server.mjs","sourceRoot":"","sources":["src/server.ts"],"names":[],"mappings":"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"}
|
package/src/code-tool-paths.cts
CHANGED
package/src/code-tool-worker.ts
CHANGED
|
@@ -7,6 +7,10 @@ import ts from 'typescript';
|
|
|
7
7
|
import { WorkerOutput } from './code-tool-types';
|
|
8
8
|
import { Metronome, ClientOptions } from '@metronome/sdk';
|
|
9
9
|
|
|
10
|
+
async function tseval(code: string) {
|
|
11
|
+
return import('data:application/typescript;charset=utf-8;base64,' + Buffer.from(code).toString('base64'));
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
function getRunFunctionSource(code: string): {
|
|
11
15
|
type: 'declaration' | 'expression';
|
|
12
16
|
client: string | undefined;
|
|
@@ -297,7 +301,8 @@ function makeSdkProxy<T extends object>(obj: T, { path, isBelievedBad = false }:
|
|
|
297
301
|
|
|
298
302
|
function parseError(code: string, error: unknown): string | undefined {
|
|
299
303
|
if (!(error instanceof Error)) return;
|
|
300
|
-
const
|
|
304
|
+
const cause = error.cause instanceof Error ? `: ${error.cause.message}` : '';
|
|
305
|
+
const message = error.name ? `${error.name}: ${error.message}${cause}` : `${error.message}${cause}`;
|
|
301
306
|
try {
|
|
302
307
|
// Deno uses V8; the first "<anonymous>:LINE:COLUMN" is the top of stack.
|
|
303
308
|
const lineNumber = error.stack?.match(/<anonymous>:([0-9]+):[0-9]+/)?.[1];
|
|
@@ -353,7 +358,9 @@ const fetch = async (req: Request): Promise<Response> => {
|
|
|
353
358
|
|
|
354
359
|
const log_lines: string[] = [];
|
|
355
360
|
const err_lines: string[] = [];
|
|
356
|
-
const
|
|
361
|
+
const originalConsole = globalThis.console;
|
|
362
|
+
globalThis.console = {
|
|
363
|
+
...originalConsole,
|
|
357
364
|
log: (...args: unknown[]) => {
|
|
358
365
|
log_lines.push(util.format(...args));
|
|
359
366
|
},
|
|
@@ -363,7 +370,7 @@ const fetch = async (req: Request): Promise<Response> => {
|
|
|
363
370
|
};
|
|
364
371
|
try {
|
|
365
372
|
let run_ = async (client: any) => {};
|
|
366
|
-
|
|
373
|
+
run_ = (await tseval(`${code}\nexport default run;`)).default;
|
|
367
374
|
const result = await run_(makeSdkProxy(client, { path: ['client'] }));
|
|
368
375
|
return Response.json({
|
|
369
376
|
is_error: false,
|
|
@@ -381,6 +388,8 @@ const fetch = async (req: Request): Promise<Response> => {
|
|
|
381
388
|
} satisfies WorkerOutput,
|
|
382
389
|
{ status: 400, statusText: 'Code execution error' },
|
|
383
390
|
);
|
|
391
|
+
} finally {
|
|
392
|
+
globalThis.console = originalConsole;
|
|
384
393
|
}
|
|
385
394
|
};
|
|
386
395
|
|
package/src/code-tool.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import url from 'node:url';
|
|
6
|
-
import { newDenoHTTPWorker } from '@valtown/deno-http-worker';
|
|
7
|
-
import { workerPath } from './code-tool-paths.cjs';
|
|
8
3
|
import {
|
|
9
4
|
ContentBlock,
|
|
10
5
|
McpRequestContext,
|
|
@@ -157,20 +152,24 @@ const remoteStainlessHandler = async ({
|
|
|
157
152
|
|
|
158
153
|
const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
|
|
159
154
|
|
|
155
|
+
const localClientEnvs = {
|
|
156
|
+
METRONOME_BEARER_TOKEN: requireValue(
|
|
157
|
+
readEnv('METRONOME_BEARER_TOKEN') ?? client.bearerToken,
|
|
158
|
+
'set METRONOME_BEARER_TOKEN environment variable or provide bearerToken client option',
|
|
159
|
+
),
|
|
160
|
+
METRONOME_WEBHOOK_SECRET: readEnv('METRONOME_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
|
|
161
|
+
METRONOME_BASE_URL: readEnv('METRONOME_BASE_URL') ?? client.baseURL ?? undefined,
|
|
162
|
+
};
|
|
163
|
+
// Merge any upstream client envs from the request header, with upstream values taking precedence.
|
|
164
|
+
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };
|
|
165
|
+
|
|
160
166
|
// Setting a Stainless API key authenticates requests to the code tool endpoint.
|
|
161
167
|
const res = await fetch(codeModeEndpoint, {
|
|
162
168
|
method: 'POST',
|
|
163
169
|
headers: {
|
|
164
170
|
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
|
|
165
171
|
'Content-Type': 'application/json',
|
|
166
|
-
'x-stainless-mcp-client-envs': JSON.stringify(
|
|
167
|
-
METRONOME_BEARER_TOKEN: requireValue(
|
|
168
|
-
readEnv('METRONOME_BEARER_TOKEN') ?? client.bearerToken,
|
|
169
|
-
'set METRONOME_BEARER_TOKEN environment variable or provide bearerToken client option',
|
|
170
|
-
),
|
|
171
|
-
METRONOME_WEBHOOK_SECRET: readEnv('METRONOME_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
|
|
172
|
-
METRONOME_BASE_URL: readEnv('METRONOME_BASE_URL') ?? client.baseURL ?? undefined,
|
|
173
|
-
}),
|
|
172
|
+
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
|
|
174
173
|
},
|
|
175
174
|
body: JSON.stringify({
|
|
176
175
|
project_name: 'metronome',
|
|
@@ -213,6 +212,13 @@ const localDenoHandler = async ({
|
|
|
213
212
|
reqContext: McpRequestContext;
|
|
214
213
|
args: unknown;
|
|
215
214
|
}): Promise<ToolCallResult> => {
|
|
215
|
+
const fs = await import('node:fs');
|
|
216
|
+
const path = await import('node:path');
|
|
217
|
+
const url = await import('node:url');
|
|
218
|
+
const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
|
|
219
|
+
const { getWorkerPath } = await import('./code-tool-paths.cjs');
|
|
220
|
+
const workerPath = getWorkerPath();
|
|
221
|
+
|
|
216
222
|
const client = reqContext.client;
|
|
217
223
|
const baseURLHostname = new URL(client.baseURL).hostname;
|
|
218
224
|
const { code } = args as { code: string };
|
|
@@ -274,6 +280,9 @@ const localDenoHandler = async ({
|
|
|
274
280
|
printOutput: true,
|
|
275
281
|
spawnOptions: {
|
|
276
282
|
cwd: path.dirname(workerPath),
|
|
283
|
+
// Merge any upstream client envs into the Deno subprocess environment,
|
|
284
|
+
// with the upstream env vars taking precedence.
|
|
285
|
+
env: { ...process.env, ...reqContext.upstreamClientEnvs },
|
|
277
286
|
},
|
|
278
287
|
});
|
|
279
288
|
|
|
@@ -283,14 +292,16 @@ const localDenoHandler = async ({
|
|
|
283
292
|
reject(new Error(`Worker exited with code ${exitCode}`));
|
|
284
293
|
});
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
// Strip null/undefined values so that the worker SDK client can fall back to
|
|
296
|
+
// reading from environment variables (including any upstreamClientEnvs).
|
|
297
|
+
const opts = {
|
|
298
|
+
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
|
|
299
|
+
...(client.bearerToken != null ? { bearerToken: client.bearerToken } : undefined),
|
|
300
|
+
...(client.webhookSecret != null ? { webhookSecret: client.webhookSecret } : undefined),
|
|
290
301
|
defaultHeaders: {
|
|
291
302
|
'X-Stainless-MCP': 'true',
|
|
292
303
|
},
|
|
293
|
-
};
|
|
304
|
+
} satisfies Partial<ClientOptions> as ClientOptions;
|
|
294
305
|
|
|
295
306
|
const req = worker.request(
|
|
296
307
|
'http://localhost',
|
package/src/docs-search-tool.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { Metadata, McpRequestContext, asTextContentResult } from './types';
|
|
5
5
|
import { getLogger } from './logger';
|
|
6
|
+
import type { LocalDocsSearch } from './local-docs-search';
|
|
6
7
|
|
|
7
8
|
export const metadata: Metadata = {
|
|
8
9
|
resource: 'all',
|
|
@@ -43,13 +44,30 @@ export const tool: Tool = {
|
|
|
43
44
|
const docsSearchURL =
|
|
44
45
|
process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/metronome/docs/search';
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
let _localSearch: LocalDocsSearch | undefined;
|
|
48
|
+
|
|
49
|
+
export function setLocalSearch(search: LocalDocsSearch): void {
|
|
50
|
+
_localSearch = search;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function searchLocal(args: Record<string, unknown>): Promise<unknown> {
|
|
54
|
+
if (!_localSearch) {
|
|
55
|
+
throw new Error('Local search not initialized');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const query = (args['query'] as string) ?? '';
|
|
59
|
+
const language = (args['language'] as string) ?? 'typescript';
|
|
60
|
+
const detail = (args['detail'] as string) ?? 'default';
|
|
61
|
+
|
|
62
|
+
return _localSearch.search({
|
|
63
|
+
query,
|
|
64
|
+
language,
|
|
65
|
+
detail,
|
|
66
|
+
maxResults: 10,
|
|
67
|
+
}).results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function searchRemote(args: Record<string, unknown>, reqContext: McpRequestContext): Promise<unknown> {
|
|
53
71
|
const body = args as any;
|
|
54
72
|
const query = new URLSearchParams(body).toString();
|
|
55
73
|
|
|
@@ -57,6 +75,10 @@ export const handler = async ({
|
|
|
57
75
|
const result = await fetch(`${docsSearchURL}?${query}`, {
|
|
58
76
|
headers: {
|
|
59
77
|
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
|
|
78
|
+
...(reqContext.mcpSessionId && { 'x-stainless-mcp-session-id': reqContext.mcpSessionId }),
|
|
79
|
+
...(reqContext.mcpClientInfo && {
|
|
80
|
+
'x-stainless-mcp-client-info': JSON.stringify(reqContext.mcpClientInfo),
|
|
81
|
+
}),
|
|
60
82
|
},
|
|
61
83
|
});
|
|
62
84
|
|
|
@@ -94,7 +116,23 @@ export const handler = async ({
|
|
|
94
116
|
},
|
|
95
117
|
'Got docs search result',
|
|
96
118
|
);
|
|
97
|
-
return
|
|
119
|
+
return resultBody;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const handler = async ({
|
|
123
|
+
reqContext,
|
|
124
|
+
args,
|
|
125
|
+
}: {
|
|
126
|
+
reqContext: McpRequestContext;
|
|
127
|
+
args: Record<string, unknown> | undefined;
|
|
128
|
+
}) => {
|
|
129
|
+
const body = args ?? {};
|
|
130
|
+
|
|
131
|
+
if (_localSearch) {
|
|
132
|
+
return asTextContentResult(await searchLocal(body));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return asTextContentResult(await searchRemote(body, reqContext));
|
|
98
136
|
};
|
|
99
137
|
|
|
100
138
|
export default { metadata, tool, handler };
|
package/src/http.ts
CHANGED
|
@@ -23,20 +23,74 @@ const newServer = async ({
|
|
|
23
23
|
res: express.Response;
|
|
24
24
|
}): Promise<McpServer | null> => {
|
|
25
25
|
const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
|
|
26
|
-
const
|
|
26
|
+
const customInstructionsPath = mcpOptions.customInstructionsPath;
|
|
27
|
+
const server = await newMcpServer({ stainlessApiKey, customInstructionsPath });
|
|
27
28
|
|
|
28
29
|
const authOptions = parseClientAuthHeaders(req, false);
|
|
29
30
|
|
|
31
|
+
let upstreamClientEnvs: Record<string, string> | undefined;
|
|
32
|
+
const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs'];
|
|
33
|
+
if (typeof clientEnvsHeader === 'string') {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(clientEnvsHeader);
|
|
36
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
37
|
+
upstreamClientEnvs = parsed;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore malformed header
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse x-stainless-mcp-client-permissions header to override permission options
|
|
45
|
+
//
|
|
46
|
+
// Note: Permissions are best-effort and intended to prevent clients from doing unexpected things;
|
|
47
|
+
// they're not a hard security boundary, so we allow arbitrary, client-driven overrides.
|
|
48
|
+
//
|
|
49
|
+
// See the Stainless MCP documentation for more details.
|
|
50
|
+
let effectiveMcpOptions = mcpOptions;
|
|
51
|
+
const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions'];
|
|
52
|
+
if (typeof clientPermissionsHeader === 'string') {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(clientPermissionsHeader);
|
|
55
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
56
|
+
effectiveMcpOptions = {
|
|
57
|
+
...mcpOptions,
|
|
58
|
+
...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }),
|
|
59
|
+
...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }),
|
|
60
|
+
...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }),
|
|
61
|
+
};
|
|
62
|
+
getLogger().info(
|
|
63
|
+
{ clientPermissions: parsed },
|
|
64
|
+
'Overriding code execution permissions from x-stainless-mcp-client-permissions header',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const mcpClientInfo =
|
|
73
|
+
typeof req.body?.params?.clientInfo?.name === 'string' ?
|
|
74
|
+
{ name: req.body.params.clientInfo.name, version: String(req.body.params.clientInfo.version ?? '') }
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
30
77
|
await initMcpServer({
|
|
31
78
|
server: server,
|
|
32
|
-
mcpOptions:
|
|
79
|
+
mcpOptions: effectiveMcpOptions,
|
|
33
80
|
clientOptions: {
|
|
34
81
|
...clientOptions,
|
|
35
82
|
...authOptions,
|
|
36
83
|
},
|
|
37
84
|
stainlessApiKey: stainlessApiKey,
|
|
85
|
+
upstreamClientEnvs,
|
|
86
|
+
mcpSessionId: (req as any).mcpSessionId,
|
|
87
|
+
mcpClientInfo,
|
|
38
88
|
});
|
|
39
89
|
|
|
90
|
+
if (mcpClientInfo) {
|
|
91
|
+
getLogger().info({ mcpSessionId: (req as any).mcpSessionId, mcpClientInfo }, 'MCP client connected');
|
|
92
|
+
}
|
|
93
|
+
|
|
40
94
|
return server;
|
|
41
95
|
};
|
|
42
96
|
|
|
@@ -72,7 +126,7 @@ const del = async (req: express.Request, res: express.Response) => {
|
|
|
72
126
|
};
|
|
73
127
|
|
|
74
128
|
const redactHeaders = (headers: Record<string, any>) => {
|
|
75
|
-
const hiddenHeaders = /auth|cookie|key|token/i;
|
|
129
|
+
const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
|
|
76
130
|
const filtered = { ...headers };
|
|
77
131
|
Object.keys(filtered).forEach((key) => {
|
|
78
132
|
if (hiddenHeaders.test(key)) {
|
|
@@ -92,9 +146,23 @@ export const streamableHTTPApp = ({
|
|
|
92
146
|
const app = express();
|
|
93
147
|
app.set('query parser', 'extended');
|
|
94
148
|
app.use(express.json());
|
|
149
|
+
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
150
|
+
const existing = req.headers['mcp-session-id'];
|
|
151
|
+
const sessionId = (Array.isArray(existing) ? existing[0] : existing) || crypto.randomUUID();
|
|
152
|
+
(req as any).mcpSessionId = sessionId;
|
|
153
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
154
|
+
res.writeHead = function (statusCode: number, ...rest: any[]) {
|
|
155
|
+
res.setHeader('mcp-session-id', sessionId);
|
|
156
|
+
return origWriteHead(statusCode, ...rest);
|
|
157
|
+
} as typeof res.writeHead;
|
|
158
|
+
next();
|
|
159
|
+
});
|
|
95
160
|
app.use(
|
|
96
161
|
pinoHttp({
|
|
97
162
|
logger: getLogger(),
|
|
163
|
+
customProps: (req) => ({
|
|
164
|
+
mcpSessionId: (req as any).mcpSessionId,
|
|
165
|
+
}),
|
|
98
166
|
customLogLevel: (req, res) => {
|
|
99
167
|
if (res.statusCode >= 500) {
|
|
100
168
|
return 'error';
|
package/src/instructions.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
4
|
import { getLogger } from './logger';
|
|
5
|
+
import { readEnv } from './util';
|
|
5
6
|
|
|
6
7
|
const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
7
8
|
|
|
@@ -12,33 +13,50 @@ interface InstructionsCacheEntry {
|
|
|
12
13
|
|
|
13
14
|
const instructionsCache = new Map<string, InstructionsCacheEntry>();
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export async function getInstructions({
|
|
17
|
+
stainlessApiKey,
|
|
18
|
+
customInstructionsPath,
|
|
19
|
+
}: {
|
|
20
|
+
stainlessApiKey?: string | undefined;
|
|
21
|
+
customInstructionsPath?: string | undefined;
|
|
22
|
+
}): Promise<string> {
|
|
17
23
|
const now = Date.now();
|
|
24
|
+
const cacheKey = customInstructionsPath ?? stainlessApiKey ?? '';
|
|
25
|
+
const cached = instructionsCache.get(cacheKey);
|
|
26
|
+
|
|
27
|
+
if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
|
|
28
|
+
return cached.fetchedInstructions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Evict stale entries so the cache doesn't grow unboundedly.
|
|
18
32
|
for (const [key, entry] of instructionsCache) {
|
|
19
33
|
if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) {
|
|
20
34
|
instructionsCache.delete(key);
|
|
21
35
|
}
|
|
22
36
|
}
|
|
23
|
-
}, INSTRUCTIONS_CACHE_TTL_MS);
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
_cacheCleanupInterval.unref();
|
|
38
|
+
let fetchedInstructions: string;
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
|
|
33
|
-
return cached.fetchedInstructions;
|
|
40
|
+
if (customInstructionsPath) {
|
|
41
|
+
fetchedInstructions = await fetchLatestInstructionsFromFile(customInstructionsPath);
|
|
42
|
+
} else {
|
|
43
|
+
fetchedInstructions = await fetchLatestInstructionsFromApi(stainlessApiKey);
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() });
|
|
46
|
+
instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now });
|
|
38
47
|
return fetchedInstructions;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
async function
|
|
50
|
+
async function fetchLatestInstructionsFromFile(path: string): Promise<string> {
|
|
51
|
+
try {
|
|
52
|
+
return await fs.readFile(path, 'utf-8');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
getLogger().error({ error, path }, 'Error fetching instructions from file');
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchLatestInstructionsFromApi(stainlessApiKey: string | undefined): Promise<string> {
|
|
42
60
|
// Setting the stainless API key is optional, but may be required
|
|
43
61
|
// to authenticate requests to the Stainless API.
|
|
44
62
|
const response = await fetch(
|