@roarkanalytics/sdk-mcp 2.28.0 → 2.30.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/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 +29 -23
- package/code-tool.js.map +1 -1
- package/code-tool.mjs +20 -11
- 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 +1814 -0
- package/local-docs-search.js.map +1 -0
- package/local-docs-search.mjs +1774 -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 +27 -16
- 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 +2114 -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/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,
|
|
@@ -154,19 +149,23 @@ const remoteStainlessHandler = async ({
|
|
|
154
149
|
|
|
155
150
|
const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
|
|
156
151
|
|
|
152
|
+
const localClientEnvs = {
|
|
153
|
+
ROARK_API_BEARER_TOKEN: requireValue(
|
|
154
|
+
readEnv('ROARK_API_BEARER_TOKEN') ?? client.bearerToken,
|
|
155
|
+
'set ROARK_API_BEARER_TOKEN environment variable or provide bearerToken client option',
|
|
156
|
+
),
|
|
157
|
+
ROARK_BASE_URL: readEnv('ROARK_BASE_URL') ?? client.baseURL ?? undefined,
|
|
158
|
+
};
|
|
159
|
+
// Merge any upstream client envs from the request header, with upstream values taking precedence.
|
|
160
|
+
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };
|
|
161
|
+
|
|
157
162
|
// Setting a Stainless API key authenticates requests to the code tool endpoint.
|
|
158
163
|
const res = await fetch(codeModeEndpoint, {
|
|
159
164
|
method: 'POST',
|
|
160
165
|
headers: {
|
|
161
166
|
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
|
|
162
167
|
'Content-Type': 'application/json',
|
|
163
|
-
'x-stainless-mcp-client-envs': JSON.stringify(
|
|
164
|
-
ROARK_API_BEARER_TOKEN: requireValue(
|
|
165
|
-
readEnv('ROARK_API_BEARER_TOKEN') ?? client.bearerToken,
|
|
166
|
-
'set ROARK_API_BEARER_TOKEN environment variable or provide bearerToken client option',
|
|
167
|
-
),
|
|
168
|
-
ROARK_BASE_URL: readEnv('ROARK_BASE_URL') ?? client.baseURL ?? undefined,
|
|
169
|
-
}),
|
|
168
|
+
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
|
|
170
169
|
},
|
|
171
170
|
body: JSON.stringify({
|
|
172
171
|
project_name: 'roark-analytics',
|
|
@@ -209,6 +208,13 @@ const localDenoHandler = async ({
|
|
|
209
208
|
reqContext: McpRequestContext;
|
|
210
209
|
args: unknown;
|
|
211
210
|
}): Promise<ToolCallResult> => {
|
|
211
|
+
const fs = await import('node:fs');
|
|
212
|
+
const path = await import('node:path');
|
|
213
|
+
const url = await import('node:url');
|
|
214
|
+
const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
|
|
215
|
+
const { getWorkerPath } = await import('./code-tool-paths.cjs');
|
|
216
|
+
const workerPath = getWorkerPath();
|
|
217
|
+
|
|
212
218
|
const client = reqContext.client;
|
|
213
219
|
const baseURLHostname = new URL(client.baseURL).hostname;
|
|
214
220
|
const { code } = args as { code: string };
|
|
@@ -270,6 +276,9 @@ const localDenoHandler = async ({
|
|
|
270
276
|
printOutput: true,
|
|
271
277
|
spawnOptions: {
|
|
272
278
|
cwd: path.dirname(workerPath),
|
|
279
|
+
// Merge any upstream client envs into the Deno subprocess environment,
|
|
280
|
+
// with the upstream env vars taking precedence.
|
|
281
|
+
env: { ...process.env, ...reqContext.upstreamClientEnvs },
|
|
273
282
|
},
|
|
274
283
|
});
|
|
275
284
|
|
|
@@ -279,13 +288,15 @@ const localDenoHandler = async ({
|
|
|
279
288
|
reject(new Error(`Worker exited with code ${exitCode}`));
|
|
280
289
|
});
|
|
281
290
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
// Strip null/undefined values so that the worker SDK client can fall back to
|
|
292
|
+
// reading from environment variables (including any upstreamClientEnvs).
|
|
293
|
+
const opts = {
|
|
294
|
+
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
|
|
295
|
+
...(client.bearerToken != null ? { bearerToken: client.bearerToken } : undefined),
|
|
285
296
|
defaultHeaders: {
|
|
286
297
|
'X-Stainless-MCP': 'true',
|
|
287
298
|
},
|
|
288
|
-
};
|
|
299
|
+
} satisfies Partial<ClientOptions> as ClientOptions;
|
|
289
300
|
|
|
290
301
|
const req = worker.request(
|
|
291
302
|
'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/roark-analytics/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(
|