@cjavdev/believe-mcp 0.20.0 → 0.21.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.
Files changed (80) hide show
  1. package/README.md +3 -3
  2. package/code-tool-worker.d.mts.map +1 -1
  3. package/code-tool-worker.d.ts.map +1 -1
  4. package/code-tool-worker.js +43 -2
  5. package/code-tool-worker.js.map +1 -1
  6. package/code-tool-worker.mjs +10 -2
  7. package/code-tool-worker.mjs.map +1 -1
  8. package/code-tool.js +4 -4
  9. package/code-tool.js.map +1 -1
  10. package/code-tool.mjs +4 -4
  11. package/code-tool.mjs.map +1 -1
  12. package/docs-search-tool.d.mts +2 -0
  13. package/docs-search-tool.d.mts.map +1 -1
  14. package/docs-search-tool.d.ts +2 -0
  15. package/docs-search-tool.d.ts.map +1 -1
  16. package/docs-search-tool.js +32 -2
  17. package/docs-search-tool.js.map +1 -1
  18. package/docs-search-tool.mjs +31 -2
  19. package/docs-search-tool.mjs.map +1 -1
  20. package/http.d.mts.map +1 -1
  21. package/http.d.ts.map +1 -1
  22. package/http.js +24 -1
  23. package/http.js.map +1 -1
  24. package/http.mjs +24 -1
  25. package/http.mjs.map +1 -1
  26. package/instructions.d.mts +4 -1
  27. package/instructions.d.mts.map +1 -1
  28. package/instructions.d.ts +4 -1
  29. package/instructions.d.ts.map +1 -1
  30. package/instructions.js +23 -4
  31. package/instructions.js.map +1 -1
  32. package/instructions.mjs +20 -4
  33. package/instructions.mjs.map +1 -1
  34. package/local-docs-search.d.mts +28 -0
  35. package/local-docs-search.d.mts.map +1 -0
  36. package/local-docs-search.d.ts +28 -0
  37. package/local-docs-search.d.ts.map +1 -0
  38. package/local-docs-search.js +4539 -0
  39. package/local-docs-search.js.map +1 -0
  40. package/local-docs-search.mjs +4499 -0
  41. package/local-docs-search.mjs.map +1 -0
  42. package/options.d.mts +3 -0
  43. package/options.d.mts.map +1 -1
  44. package/options.d.ts +3 -0
  45. package/options.d.ts.map +1 -1
  46. package/options.js +19 -0
  47. package/options.js.map +1 -1
  48. package/options.mjs +19 -0
  49. package/options.mjs.map +1 -1
  50. package/package.json +13 -2
  51. package/server.d.mts +9 -1
  52. package/server.d.mts.map +1 -1
  53. package/server.d.ts +9 -1
  54. package/server.d.ts.map +1 -1
  55. package/server.js +12 -3
  56. package/server.js.map +1 -1
  57. package/server.mjs +12 -3
  58. package/server.mjs.map +1 -1
  59. package/src/code-tool-worker.ts +10 -2
  60. package/src/code-tool.ts +7 -9
  61. package/src/docs-search-tool.ts +46 -8
  62. package/src/http.ts +27 -1
  63. package/src/instructions.ts +27 -4
  64. package/src/local-docs-search.ts +5392 -0
  65. package/src/options.ts +24 -0
  66. package/src/server.ts +21 -3
  67. package/src/stdio.ts +4 -1
  68. package/src/types.ts +2 -0
  69. package/stdio.d.mts.map +1 -1
  70. package/stdio.d.ts.map +1 -1
  71. package/stdio.js +4 -1
  72. package/stdio.js.map +1 -1
  73. package/stdio.mjs +4 -1
  74. package/stdio.mjs.map +1 -1
  75. package/types.d.mts +5 -0
  76. package/types.d.mts.map +1 -1
  77. package/types.d.ts +5 -0
  78. package/types.d.ts.map +1 -1
  79. package/types.js.map +1 -1
  80. package/types.mjs.map +1 -1
@@ -7,6 +7,10 @@ import ts from 'typescript';
7
7
  import { WorkerOutput } from './code-tool-types';
8
8
  import { Believe, ClientOptions } from '@cjavdev/believe';
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;
@@ -311,7 +315,9 @@ const fetch = async (req: Request): Promise<Response> => {
311
315
 
312
316
  const log_lines: string[] = [];
313
317
  const err_lines: string[] = [];
314
- const console = {
318
+ const originalConsole = globalThis.console;
319
+ globalThis.console = {
320
+ ...originalConsole,
315
321
  log: (...args: unknown[]) => {
316
322
  log_lines.push(util.format(...args));
317
323
  },
@@ -321,7 +327,7 @@ const fetch = async (req: Request): Promise<Response> => {
321
327
  };
322
328
  try {
323
329
  let run_ = async (client: any) => {};
324
- eval(`${code}\nrun_ = run;`);
330
+ run_ = (await tseval(`${code}\nexport default run;`)).default;
325
331
  const result = await run_(makeSdkProxy(client, { path: ['client'] }));
326
332
  return Response.json({
327
333
  is_error: false,
@@ -339,6 +345,8 @@ const fetch = async (req: Request): Promise<Response> => {
339
345
  } satisfies WorkerOutput,
340
346
  { status: 400, statusText: 'Code execution error' },
341
347
  );
348
+ } finally {
349
+ globalThis.console = originalConsole;
342
350
  }
343
351
  };
344
352
 
package/src/code-tool.ts CHANGED
@@ -286,15 +286,13 @@ const localDenoHandler = async ({
286
286
 
287
287
  // Strip null/undefined values so that the worker SDK client can fall back to
288
288
  // reading from environment variables (including any upstreamClientEnvs).
289
- const opts: ClientOptions = Object.fromEntries(
290
- Object.entries({
291
- baseURL: client.baseURL,
292
- apiKey: client.apiKey,
293
- defaultHeaders: {
294
- 'X-Stainless-MCP': 'true',
295
- },
296
- }).filter(([_, v]) => v != null),
297
- ) as ClientOptions;
289
+ const opts = {
290
+ ...(client.baseURL != null ? { baseURL: client.baseURL } : undefined),
291
+ ...(client.apiKey != null ? { apiKey: client.apiKey } : undefined),
292
+ defaultHeaders: {
293
+ 'X-Stainless-MCP': 'true',
294
+ },
295
+ } satisfies Partial<ClientOptions> as ClientOptions;
298
296
 
299
297
  const req = worker.request(
300
298
  'http://localhost',
@@ -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
- export const handler = async ({
47
- reqContext,
48
- args,
49
- }: {
50
- reqContext: McpRequestContext;
51
- args: Record<string, unknown> | undefined;
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 asTextContentResult(resultBody);
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,7 +23,8 @@ const newServer = async ({
23
23
  res: express.Response;
24
24
  }): Promise<McpServer | null> => {
25
25
  const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
26
- const server = await newMcpServer(stainlessApiKey);
26
+ const customInstructionsPath = mcpOptions.customInstructionsPath;
27
+ const server = await newMcpServer({ stainlessApiKey, customInstructionsPath });
27
28
 
28
29
  const authOptions = parseClientAuthHeaders(req, false);
29
30
 
@@ -68,6 +69,11 @@ const newServer = async ({
68
69
  }
69
70
  }
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
+
71
77
  await initMcpServer({
72
78
  server: server,
73
79
  mcpOptions: effectiveMcpOptions,
@@ -77,8 +83,14 @@ const newServer = async ({
77
83
  },
78
84
  stainlessApiKey: stainlessApiKey,
79
85
  upstreamClientEnvs,
86
+ mcpSessionId: (req as any).mcpSessionId,
87
+ mcpClientInfo,
80
88
  });
81
89
 
90
+ if (mcpClientInfo) {
91
+ getLogger().info({ mcpSessionId: (req as any).mcpSessionId, mcpClientInfo }, 'MCP client connected');
92
+ }
93
+
82
94
  return server;
83
95
  };
84
96
 
@@ -134,9 +146,23 @@ export const streamableHTTPApp = ({
134
146
  const app = express();
135
147
  app.set('query parser', 'extended');
136
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
+ });
137
160
  app.use(
138
161
  pinoHttp({
139
162
  logger: getLogger(),
163
+ customProps: (req) => ({
164
+ mcpSessionId: (req as any).mcpSessionId,
165
+ }),
140
166
  customLogLevel: (req, res) => {
141
167
  if (res.statusCode >= 500) {
142
168
  return 'error';
@@ -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,9 +13,15 @@ interface InstructionsCacheEntry {
12
13
 
13
14
  const instructionsCache = new Map<string, InstructionsCacheEntry>();
14
15
 
15
- export async function getInstructions(stainlessApiKey: string | undefined): Promise<string> {
16
+ export async function getInstructions({
17
+ stainlessApiKey,
18
+ customInstructionsPath,
19
+ }: {
20
+ stainlessApiKey?: string | undefined;
21
+ customInstructionsPath?: string | undefined;
22
+ }): Promise<string> {
16
23
  const now = Date.now();
17
- const cacheKey = stainlessApiKey ?? '';
24
+ const cacheKey = customInstructionsPath ?? stainlessApiKey ?? '';
18
25
  const cached = instructionsCache.get(cacheKey);
19
26
 
20
27
  if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) {
@@ -28,12 +35,28 @@ export async function getInstructions(stainlessApiKey: string | undefined): Prom
28
35
  }
29
36
  }
30
37
 
31
- const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey);
38
+ let fetchedInstructions: string;
39
+
40
+ if (customInstructionsPath) {
41
+ fetchedInstructions = await fetchLatestInstructionsFromFile(customInstructionsPath);
42
+ } else {
43
+ fetchedInstructions = await fetchLatestInstructionsFromApi(stainlessApiKey);
44
+ }
45
+
32
46
  instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now });
33
47
  return fetchedInstructions;
34
48
  }
35
49
 
36
- async function fetchLatestInstructions(stainlessApiKey: string | undefined): Promise<string> {
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> {
37
60
  // Setting the stainless API key is optional, but may be required
38
61
  // to authenticate requests to the Stainless API.
39
62
  const response = await fetch(