@cjavdev/believe-mcp 0.19.0 → 0.20.1
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.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 +58 -3
- package/http.js.map +1 -1
- package/http.mjs +58 -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 +28 -13
- package/instructions.js.map +1 -1
- package/instructions.mjs +25 -13
- 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 +3932 -0
- package/local-docs-search.js.map +1 -0
- package/local-docs-search.mjs +3892 -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.ts +27 -16
- package/src/docs-search-tool.ts +46 -8
- package/src/http.ts +62 -3
- package/src/instructions.ts +32 -14
- package/src/local-docs-search.ts +4636 -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/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/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,
|
|
@@ -150,19 +145,23 @@ const remoteStainlessHandler = async ({
|
|
|
150
145
|
|
|
151
146
|
const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
|
|
152
147
|
|
|
148
|
+
const localClientEnvs = {
|
|
149
|
+
BELIEVE_API_KEY: requireValue(
|
|
150
|
+
readEnv('BELIEVE_API_KEY') ?? client.apiKey,
|
|
151
|
+
'set BELIEVE_API_KEY environment variable or provide apiKey client option',
|
|
152
|
+
),
|
|
153
|
+
BELIEVE_BASE_URL: readEnv('BELIEVE_BASE_URL') ?? client.baseURL ?? undefined,
|
|
154
|
+
};
|
|
155
|
+
// Merge any upstream client envs from the request header, with upstream values taking precedence.
|
|
156
|
+
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };
|
|
157
|
+
|
|
153
158
|
// Setting a Stainless API key authenticates requests to the code tool endpoint.
|
|
154
159
|
const res = await fetch(codeModeEndpoint, {
|
|
155
160
|
method: 'POST',
|
|
156
161
|
headers: {
|
|
157
162
|
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
|
|
158
163
|
'Content-Type': 'application/json',
|
|
159
|
-
'x-stainless-mcp-client-envs': JSON.stringify(
|
|
160
|
-
BELIEVE_API_KEY: requireValue(
|
|
161
|
-
readEnv('BELIEVE_API_KEY') ?? client.apiKey,
|
|
162
|
-
'set BELIEVE_API_KEY environment variable or provide apiKey client option',
|
|
163
|
-
),
|
|
164
|
-
BELIEVE_BASE_URL: readEnv('BELIEVE_BASE_URL') ?? client.baseURL ?? undefined,
|
|
165
|
-
}),
|
|
164
|
+
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
|
|
166
165
|
},
|
|
167
166
|
body: JSON.stringify({
|
|
168
167
|
project_name: 'believe',
|
|
@@ -205,6 +204,13 @@ const localDenoHandler = async ({
|
|
|
205
204
|
reqContext: McpRequestContext;
|
|
206
205
|
args: unknown;
|
|
207
206
|
}): Promise<ToolCallResult> => {
|
|
207
|
+
const fs = await import('node:fs');
|
|
208
|
+
const path = await import('node:path');
|
|
209
|
+
const url = await import('node:url');
|
|
210
|
+
const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
|
|
211
|
+
const { getWorkerPath } = await import('./code-tool-paths.cjs');
|
|
212
|
+
const workerPath = getWorkerPath();
|
|
213
|
+
|
|
208
214
|
const client = reqContext.client;
|
|
209
215
|
const baseURLHostname = new URL(client.baseURL).hostname;
|
|
210
216
|
const { code } = args as { code: string };
|
|
@@ -266,6 +272,9 @@ const localDenoHandler = async ({
|
|
|
266
272
|
printOutput: true,
|
|
267
273
|
spawnOptions: {
|
|
268
274
|
cwd: path.dirname(workerPath),
|
|
275
|
+
// Merge any upstream client envs into the Deno subprocess environment,
|
|
276
|
+
// with the upstream env vars taking precedence.
|
|
277
|
+
env: { ...process.env, ...reqContext.upstreamClientEnvs },
|
|
269
278
|
},
|
|
270
279
|
});
|
|
271
280
|
|
|
@@ -275,13 +284,15 @@ const localDenoHandler = async ({
|
|
|
275
284
|
reject(new Error(`Worker exited with code ${exitCode}`));
|
|
276
285
|
});
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
287
|
+
// Strip null/undefined values so that the worker SDK client can fall back to
|
|
288
|
+
// reading from environment variables (including any upstreamClientEnvs).
|
|
289
|
+
const opts = {
|
|
290
|
+
...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
|
|
291
|
+
...(client.apiKey != null ? { apiKey: client.apiKey } : undefined),
|
|
281
292
|
defaultHeaders: {
|
|
282
293
|
'X-Stainless-MCP': 'true',
|
|
283
294
|
},
|
|
284
|
-
};
|
|
295
|
+
} satisfies Partial<ClientOptions> as ClientOptions;
|
|
285
296
|
|
|
286
297
|
const req = worker.request(
|
|
287
298
|
'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/believe/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: 5,
|
|
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,18 +23,66 @@ 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
|
+
|
|
30
72
|
await initMcpServer({
|
|
31
73
|
server: server,
|
|
32
|
-
mcpOptions:
|
|
74
|
+
mcpOptions: effectiveMcpOptions,
|
|
33
75
|
clientOptions: {
|
|
34
76
|
...clientOptions,
|
|
35
77
|
...authOptions,
|
|
36
78
|
},
|
|
37
79
|
stainlessApiKey: stainlessApiKey,
|
|
80
|
+
upstreamClientEnvs,
|
|
81
|
+
mcpSessionId: (req as any).mcpSessionId,
|
|
82
|
+
mcpClientInfo:
|
|
83
|
+
typeof req.body?.params?.clientInfo?.name === 'string' ?
|
|
84
|
+
{ name: req.body.params.clientInfo.name, version: String(req.body.params.clientInfo.version ?? '') }
|
|
85
|
+
: undefined,
|
|
38
86
|
});
|
|
39
87
|
|
|
40
88
|
return server;
|
|
@@ -72,7 +120,7 @@ const del = async (req: express.Request, res: express.Response) => {
|
|
|
72
120
|
};
|
|
73
121
|
|
|
74
122
|
const redactHeaders = (headers: Record<string, any>) => {
|
|
75
|
-
const hiddenHeaders = /auth|cookie|key|token/i;
|
|
123
|
+
const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
|
|
76
124
|
const filtered = { ...headers };
|
|
77
125
|
Object.keys(filtered).forEach((key) => {
|
|
78
126
|
if (hiddenHeaders.test(key)) {
|
|
@@ -92,6 +140,17 @@ export const streamableHTTPApp = ({
|
|
|
92
140
|
const app = express();
|
|
93
141
|
app.set('query parser', 'extended');
|
|
94
142
|
app.use(express.json());
|
|
143
|
+
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
144
|
+
const existing = req.headers['mcp-session-id'];
|
|
145
|
+
const sessionId = (Array.isArray(existing) ? existing[0] : existing) || crypto.randomUUID();
|
|
146
|
+
(req as any).mcpSessionId = sessionId;
|
|
147
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
148
|
+
res.writeHead = function (statusCode: number, ...rest: any[]) {
|
|
149
|
+
res.setHeader('mcp-session-id', sessionId);
|
|
150
|
+
return origWriteHead(statusCode, ...rest);
|
|
151
|
+
} as typeof res.writeHead;
|
|
152
|
+
next();
|
|
153
|
+
});
|
|
95
154
|
app.use(
|
|
96
155
|
pinoHttp({
|
|
97
156
|
logger: getLogger(),
|
package/src/instructions.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
|
+
import fs from 'fs/promises';
|
|
3
4
|
import { readEnv } from './util';
|
|
4
5
|
import { getLogger } from './logger';
|
|
5
6
|
|
|
@@ -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(
|