@convertcom/mcp-server 0.0.1-beta.15 → 0.0.1-beta.16

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 (42) hide show
  1. package/build/api-config.js +21 -3
  2. package/build/api-config.js.map +1 -1
  3. package/build/auth-context.d.ts +3 -0
  4. package/build/auth-context.js.map +1 -1
  5. package/build/catalog-change.d.ts +6 -2
  6. package/build/catalog-change.js +5 -1
  7. package/build/catalog-change.js.map +1 -1
  8. package/build/executor.d.ts +6 -0
  9. package/build/executor.js +73 -22
  10. package/build/executor.js.map +1 -1
  11. package/build/generated-tool-validators.js +141 -158
  12. package/build/generated-tool-validators.js.map +1 -1
  13. package/build/index.js +19 -12
  14. package/build/index.js.map +1 -1
  15. package/build/kb-hybrid.d.ts +13 -5
  16. package/build/kb-hybrid.js +16 -10
  17. package/build/kb-hybrid.js.map +1 -1
  18. package/build/kb-search.d.ts +9 -3
  19. package/build/kb-search.js +95 -4
  20. package/build/kb-search.js.map +1 -1
  21. package/build/kb-semantic.d.ts +2 -2
  22. package/build/kb-semantic.js +11 -2
  23. package/build/kb-semantic.js.map +1 -1
  24. package/build/local-docs.d.ts +12 -0
  25. package/build/local-docs.js +9 -0
  26. package/build/local-docs.js.map +1 -0
  27. package/build/local-execution-context.d.ts +7 -0
  28. package/build/local-execution-context.js +18 -0
  29. package/build/local-execution-context.js.map +1 -0
  30. package/build/mcp-server-compat.d.ts +8 -0
  31. package/build/mcp-server-compat.js +28 -0
  32. package/build/mcp-server-compat.js.map +1 -0
  33. package/build/modern-runtime.d.ts +13 -0
  34. package/build/modern-runtime.js +148 -0
  35. package/build/modern-runtime.js.map +1 -0
  36. package/build/namespaced.js +2 -2
  37. package/build/namespaced.js.map +1 -1
  38. package/build/runtime.d.ts +221 -5
  39. package/build/runtime.js +169 -67
  40. package/build/runtime.js.map +1 -1
  41. package/package.json +6 -6
  42. package/server.json +2 -2
@@ -1,16 +1,18 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { type CallToolRequest, type CallToolResult, type Implementation, type ReadResourceResult, type Resource, type Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
+ import { type CallToolRequest, type CallToolResult, type GetPromptRequest, type ReadResourceRequest, type ClientRequest, type Implementation, type ListResourcesResult, type ReadResourceResult, type Resource, type ServerNotification, type Tool } from '@modelcontextprotocol/sdk/types.js';
3
4
  import type { ToolExecutionContext } from './auth-context.js';
4
5
  import { type CatalogChangeRequest } from './catalog-change.js';
5
6
  import { type KbChunk, type KbDocument, type SearchResult } from './kb-search.js';
6
7
  export declare const SERVER_NAME = "convert-api";
7
- export declare const SERVER_VERSION_FALLBACK = "0.0.1-beta.15";
8
+ export declare const SERVER_VERSION_FALLBACK = "0.0.1-beta.16";
9
+ export declare const MAX_SEARCH_QUERY_CHARACTERS = 500;
8
10
  export type ExecutionContextResolver = (extra: any, request: CallToolRequest) => ToolExecutionContext | undefined | Promise<ToolExecutionContext | undefined>;
9
11
  export type ToolCatalogContextResolver = (extra: any) => ToolExecutionContext | undefined | Promise<ToolExecutionContext | undefined>;
10
12
  export type DocsSearchProvider = {
11
- search(query: string, limit?: number): SearchResult[] | Promise<SearchResult[]>;
12
- getById(id: string): KbChunk | null | Promise<KbChunk | null>;
13
- getDocumentById?(id: string): KbDocument | null | Promise<KbDocument | null>;
13
+ search(query: string, limit?: number, signal?: AbortSignal): SearchResult[] | Promise<SearchResult[]>;
14
+ getById(id: string, signal?: AbortSignal): KbChunk | null | Promise<KbChunk | null>;
15
+ getDocumentById?(id: string, signal?: AbortSignal): KbDocument | null | Promise<KbDocument | null>;
14
16
  };
15
17
  export type RuntimeLogger = {
16
18
  log(event: string, payload?: unknown): void;
@@ -34,6 +36,8 @@ export type RuntimeResourceProvider = {
34
36
  };
35
37
  export type ConvertMcpRuntimeOptions = {
36
38
  docs: DocsSearchProvider;
39
+ /** Disable only where the serving transport cannot scope list-change delivery safely. */
40
+ advertiseListChanged?: boolean;
37
41
  executionContextResolver?: ExecutionContextResolver;
38
42
  toolCatalogContextResolver?: ToolCatalogContextResolver;
39
43
  logger?: RuntimeLogger;
@@ -47,12 +51,224 @@ export type ConvertMcpRuntimeOptions = {
47
51
  toolExtensions?: RuntimeToolExtension[];
48
52
  resourceProvider?: RuntimeResourceProvider;
49
53
  toolDecorator?: (tool: Tool) => Tool;
54
+ /** Bounded authorization-shape cache supplied only by transports with stable configuration. */
55
+ toolCatalogCache?: Map<string, RuntimeToolCatalogSnapshot>;
56
+ /** Shares deterministic tool-catalog digests across stateless modern requests. */
57
+ toolCatalogRevisionCache?: Map<string, Promise<string>>;
58
+ /** Must cover every dynamic input used by toolDecorator; never include bearer or principal data. */
59
+ toolCatalogDecoratorCacheKey?: () => string;
50
60
  serverInstructions?: string | ((toolSet: string) => string);
51
61
  catalogChangeNotifier?: (request: CatalogChangeRequest) => Promise<void>;
52
62
  };
63
+ export type RuntimeToolCatalogSnapshot = {
64
+ tools: Tool[];
65
+ toolsByName: ReadonlyMap<string, Tool>;
66
+ };
67
+ export type RuntimeRequestExtra = Pick<RequestHandlerExtra<ClientRequest, ServerNotification>, 'sendNotification' | 'signal' | 'requestId'> & {
68
+ clientInfo?: Implementation;
69
+ };
70
+ export type RuntimeToolResolutionObserver = (tool: Tool | undefined) => void;
53
71
  export declare const SERVER_INSTRUCTIONS: string;
54
72
  export declare function serverInstructionsForToolSet(toolSet: string, renderToolNames?: readonly string[]): string;
55
73
  export declare const canonicalSearchTool: Tool;
56
74
  export declare const canonicalFetchTool: Tool;
57
75
  export declare function getToolsForClient(toolSet?: string, feedbackEnabled?: boolean, executionContext?: ToolExecutionContext): Tool[];
76
+ export type ConvertMcpServerIdentityOptions = Pick<ConvertMcpRuntimeOptions, 'serverVersion' | 'serverTitle' | 'iconBaseUrl' | 'websiteUrl' | 'toolsForClient' | 'serverInstructions'>;
77
+ export declare function convertMcpServerDefinition(options: ConvertMcpServerIdentityOptions): {
78
+ implementation: {
79
+ name: string;
80
+ version: string;
81
+ title: string;
82
+ description: string;
83
+ websiteUrl: string;
84
+ icons: {
85
+ src: string;
86
+ mimeType?: string | undefined;
87
+ sizes?: string[] | undefined;
88
+ theme?: "light" | "dark" | undefined;
89
+ }[];
90
+ };
91
+ options: {
92
+ capabilities: {
93
+ tools: {
94
+ listChanged: boolean;
95
+ };
96
+ prompts: {
97
+ listChanged: boolean;
98
+ };
99
+ resources: {
100
+ listChanged: boolean;
101
+ };
102
+ };
103
+ instructions: string;
104
+ };
105
+ };
58
106
  export declare function createConvertMcpServer(options: ConvertMcpRuntimeOptions): Server;
107
+ export declare function createConvertMcpHandlers(options: ConvertMcpRuntimeOptions, clientInfoFor: (extra: RuntimeRequestExtra) => Implementation | undefined): {
108
+ listTools: (extra: RuntimeRequestExtra) => Promise<{
109
+ tools: {
110
+ inputSchema: {
111
+ [x: string]: unknown;
112
+ type: "object";
113
+ properties?: {
114
+ [x: string]: object;
115
+ } | undefined;
116
+ required?: string[] | undefined;
117
+ };
118
+ name: string;
119
+ description?: string | undefined;
120
+ outputSchema?: {
121
+ [x: string]: unknown;
122
+ type: "object";
123
+ properties?: {
124
+ [x: string]: object;
125
+ } | undefined;
126
+ required?: string[] | undefined;
127
+ } | undefined;
128
+ annotations?: {
129
+ title?: string | undefined;
130
+ readOnlyHint?: boolean | undefined;
131
+ destructiveHint?: boolean | undefined;
132
+ idempotentHint?: boolean | undefined;
133
+ openWorldHint?: boolean | undefined;
134
+ } | undefined;
135
+ execution?: {
136
+ taskSupport?: "required" | "optional" | "forbidden" | undefined;
137
+ } | undefined;
138
+ _meta?: {
139
+ [x: string]: unknown;
140
+ } | undefined;
141
+ icons?: {
142
+ src: string;
143
+ mimeType?: string | undefined;
144
+ sizes?: string[] | undefined;
145
+ theme?: "light" | "dark" | undefined;
146
+ }[] | undefined;
147
+ title?: string | undefined;
148
+ }[];
149
+ }>;
150
+ listResources: (extra: RuntimeRequestExtra) => Promise<ListResourcesResult>;
151
+ listResourceTemplates: (extra: RuntimeRequestExtra) => Promise<{
152
+ resourceTemplates: never[];
153
+ }>;
154
+ readResource: (request: ReadResourceRequest, extra: RuntimeRequestExtra) => Promise<ReadResourceResult>;
155
+ listPrompts: (extra: RuntimeRequestExtra) => Promise<{
156
+ prompts: {
157
+ name: string;
158
+ description?: string | undefined;
159
+ arguments?: {
160
+ name: string;
161
+ description?: string | undefined;
162
+ required?: boolean | undefined;
163
+ }[] | undefined;
164
+ _meta?: {
165
+ [x: string]: unknown;
166
+ } | undefined;
167
+ icons?: {
168
+ src: string;
169
+ mimeType?: string | undefined;
170
+ sizes?: string[] | undefined;
171
+ theme?: "light" | "dark" | undefined;
172
+ }[] | undefined;
173
+ title?: string | undefined;
174
+ }[];
175
+ }>;
176
+ getPrompt: (request: GetPromptRequest, extra: RuntimeRequestExtra) => Promise<{
177
+ [x: string]: unknown;
178
+ messages: {
179
+ role: "user" | "assistant";
180
+ content: {
181
+ type: "text";
182
+ text: string;
183
+ annotations?: {
184
+ audience?: ("user" | "assistant")[] | undefined;
185
+ priority?: number | undefined;
186
+ lastModified?: string | undefined;
187
+ } | undefined;
188
+ _meta?: {
189
+ [x: string]: unknown;
190
+ } | undefined;
191
+ } | {
192
+ type: "image";
193
+ data: string;
194
+ mimeType: string;
195
+ annotations?: {
196
+ audience?: ("user" | "assistant")[] | undefined;
197
+ priority?: number | undefined;
198
+ lastModified?: string | undefined;
199
+ } | undefined;
200
+ _meta?: {
201
+ [x: string]: unknown;
202
+ } | undefined;
203
+ } | {
204
+ type: "audio";
205
+ data: string;
206
+ mimeType: string;
207
+ annotations?: {
208
+ audience?: ("user" | "assistant")[] | undefined;
209
+ priority?: number | undefined;
210
+ lastModified?: string | undefined;
211
+ } | undefined;
212
+ _meta?: {
213
+ [x: string]: unknown;
214
+ } | undefined;
215
+ } | {
216
+ uri: string;
217
+ name: string;
218
+ type: "resource_link";
219
+ description?: string | undefined;
220
+ mimeType?: string | undefined;
221
+ size?: number | undefined;
222
+ annotations?: {
223
+ audience?: ("user" | "assistant")[] | undefined;
224
+ priority?: number | undefined;
225
+ lastModified?: string | undefined;
226
+ } | undefined;
227
+ _meta?: {
228
+ [x: string]: unknown;
229
+ } | undefined;
230
+ icons?: {
231
+ src: string;
232
+ mimeType?: string | undefined;
233
+ sizes?: string[] | undefined;
234
+ theme?: "light" | "dark" | undefined;
235
+ }[] | undefined;
236
+ title?: string | undefined;
237
+ } | {
238
+ type: "resource";
239
+ resource: {
240
+ uri: string;
241
+ text: string;
242
+ mimeType?: string | undefined;
243
+ _meta?: {
244
+ [x: string]: unknown;
245
+ } | undefined;
246
+ } | {
247
+ uri: string;
248
+ blob: string;
249
+ mimeType?: string | undefined;
250
+ _meta?: {
251
+ [x: string]: unknown;
252
+ } | undefined;
253
+ };
254
+ annotations?: {
255
+ audience?: ("user" | "assistant")[] | undefined;
256
+ priority?: number | undefined;
257
+ lastModified?: string | undefined;
258
+ } | undefined;
259
+ _meta?: {
260
+ [x: string]: unknown;
261
+ } | undefined;
262
+ };
263
+ }[];
264
+ _meta?: {
265
+ [x: string]: unknown;
266
+ progressToken?: string | number | undefined;
267
+ "io.modelcontextprotocol/related-task"?: {
268
+ taskId: string;
269
+ } | undefined;
270
+ } | undefined;
271
+ description?: string | undefined;
272
+ }>;
273
+ callTool: (request: CallToolRequest, extra: RuntimeRequestExtra, onToolResolved?: RuntimeToolResolutionObserver) => Promise<CallToolResult>;
274
+ };
package/build/runtime.js CHANGED
@@ -9,8 +9,11 @@ import { getPrompt, listPrompts } from './prompts.js';
9
9
  import { composeResourceProviders, investigationPlaybookResourceProvider, } from './investigation-playbook.js';
10
10
  import { JSON_SCHEMA_DRAFT_2020_12, tryParseJsonLike } from './shared.js';
11
11
  import { displayKbTitle } from './kb-search.js';
12
+ import { applyFalsyRequestIdCancellationCompat } from './mcp-server-compat.js';
12
13
  export const SERVER_NAME = 'convert-api';
13
- export const SERVER_VERSION_FALLBACK = '0.0.1-beta.15';
14
+ export const SERVER_VERSION_FALLBACK = '0.0.1-beta.16';
15
+ export const MAX_SEARCH_QUERY_CHARACTERS = 500;
16
+ const MAX_RUNTIME_TOOL_CATALOG_VARIANTS = 16;
14
17
  const noopLogger = { log: () => { } };
15
18
  function failOpenLogger(logger) {
16
19
  return {
@@ -175,6 +178,7 @@ export const canonicalSearchTool = {
175
178
  properties: {
176
179
  query: {
177
180
  type: 'string',
181
+ maxLength: MAX_SEARCH_QUERY_CHARACTERS,
178
182
  description: 'Search query for Convert.com documentation.',
179
183
  },
180
184
  },
@@ -250,15 +254,47 @@ export const canonicalFetchTool = {
250
254
  export function getToolsForClient(toolSet = 'reporting', feedbackEnabled = true, executionContext) {
251
255
  return [canonicalSearchTool, canonicalFetchTool, ...getNamespacedTools(toolSet, executionContext), ...getOperatorTools(toolSet, feedbackEnabled)];
252
256
  }
253
- function getRuntimeTools(options, toolSet, executionContext) {
257
+ function getRuntimeToolCatalog(options, toolSet, executionContext) {
258
+ const feedbackEnabled = resolveBooleanOption(options.feedbackEnabled);
259
+ const notificationsEnabled = resolveBooleanOption(options.notificationsEnabled);
260
+ const decoratorKey = options.toolCatalogDecoratorCacheKey?.();
261
+ const cacheKey = options.toolCatalogCache && (!options.toolDecorator || decoratorKey !== undefined)
262
+ ? [
263
+ toolSet,
264
+ `feedback=${feedbackEnabled}`,
265
+ `notifications=${notificationsEnabled}`,
266
+ `account-wide=${executionContext?.authorizedAccounts?.some((account) => account.projects === null) === true}`,
267
+ `decorator=${decoratorKey ?? 'none'}`,
268
+ ].join(':')
269
+ : undefined;
270
+ const cached = cacheKey ? options.toolCatalogCache?.get(cacheKey) : undefined;
271
+ if (cached)
272
+ return cached;
254
273
  const tools = [
255
- ...getToolsForClient(toolSet, resolveBooleanOption(options.feedbackEnabled), {
274
+ ...getToolsForClient(toolSet, feedbackEnabled, {
256
275
  ...executionContext,
257
- notificationsEnabled: resolveBooleanOption(options.notificationsEnabled),
276
+ notificationsEnabled,
258
277
  }),
259
278
  ...(options.toolExtensions ?? []).flatMap((extension) => extension.tools(toolSet)),
260
279
  ];
261
- return options.toolDecorator ? tools.map(options.toolDecorator) : tools;
280
+ const decorated = options.toolDecorator ? tools.map(options.toolDecorator) : tools;
281
+ const snapshot = {
282
+ tools: decorated,
283
+ toolsByName: new Map(decorated.map((tool) => [tool.name, tool])),
284
+ };
285
+ if (cacheKey && options.toolCatalogCache) {
286
+ while (options.toolCatalogCache.size >= MAX_RUNTIME_TOOL_CATALOG_VARIANTS) {
287
+ const oldest = options.toolCatalogCache.keys().next().value;
288
+ if (typeof oldest !== 'string')
289
+ break;
290
+ options.toolCatalogCache.delete(oldest);
291
+ }
292
+ options.toolCatalogCache.set(cacheKey, snapshot);
293
+ }
294
+ return snapshot;
295
+ }
296
+ function getRuntimeTools(options, toolSet, executionContext) {
297
+ return getRuntimeToolCatalog(options, toolSet, executionContext).tools;
262
298
  }
263
299
  function canonicalKbId(id) {
264
300
  return id.startsWith('convert-kb:') ? id : `convert-kb:${id}`;
@@ -300,39 +336,59 @@ function resolveServerTitle(serverTitle) {
300
336
  const value = typeof serverTitle === 'function' ? serverTitle() : serverTitle;
301
337
  return value?.trim() || 'Convert MCP';
302
338
  }
303
- export function createConvertMcpServer(options) {
304
- const logger = failOpenLogger(options.logger ?? noopLogger);
305
- const resourceProvider = composeResourceProviders(investigationPlaybookResourceProvider, options.resourceProvider);
306
- const hasResources = true;
339
+ export function convertMcpServerDefinition(options) {
307
340
  const initialToolSet = resolveToolSet(options.toolsForClient);
308
341
  const instructions = typeof options.serverInstructions === 'function'
309
342
  ? options.serverInstructions(initialToolSet)
310
343
  : options.serverInstructions ?? serverInstructionsForToolSet(initialToolSet);
311
- const server = new Server({
312
- name: SERVER_NAME,
313
- version: options.serverVersion ?? SERVER_VERSION_FALLBACK,
314
- title: resolveServerTitle(options.serverTitle),
315
- description: "Convert.com MCP tools for experimentation workflows, account data, and product documentation.",
316
- websiteUrl: resolveWebsiteUrl(options.websiteUrl),
317
- icons: convertBrandIcons(resolveIconBaseUrl(options.iconBaseUrl)),
318
- }, {
319
- capabilities: {
320
- tools: { listChanged: true },
321
- prompts: { listChanged: true },
322
- ...(hasResources ? { resources: { listChanged: true } } : {}),
344
+ return {
345
+ implementation: {
346
+ name: SERVER_NAME,
347
+ version: options.serverVersion ?? SERVER_VERSION_FALLBACK,
348
+ title: resolveServerTitle(options.serverTitle),
349
+ description: "Convert.com MCP tools for experimentation workflows, account data, and product documentation.",
350
+ websiteUrl: resolveWebsiteUrl(options.websiteUrl),
351
+ icons: convertBrandIcons(resolveIconBaseUrl(options.iconBaseUrl)),
352
+ },
353
+ options: {
354
+ capabilities: {
355
+ tools: { listChanged: true },
356
+ prompts: { listChanged: true },
357
+ resources: { listChanged: true },
358
+ },
359
+ instructions,
323
360
  },
324
- instructions,
325
- });
361
+ };
362
+ }
363
+ export function createConvertMcpServer(options) {
364
+ const definition = convertMcpServerDefinition(options);
365
+ const server = new Server(definition.implementation, definition.options);
366
+ applyFalsyRequestIdCancellationCompat(server);
367
+ const handlers = createConvertMcpHandlers(options, () => server.getClientVersion());
368
+ server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => handlers.listTools(extra));
369
+ server.setRequestHandler(ListResourcesRequestSchema, (_request, extra) => handlers.listResources(extra));
370
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, (_request, extra) => handlers.listResourceTemplates(extra));
371
+ server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => handlers.readResource(request, extra));
372
+ server.setRequestHandler(ListPromptsRequestSchema, (_request, extra) => handlers.listPrompts(extra));
373
+ server.setRequestHandler(GetPromptRequestSchema, (request, extra) => handlers.getPrompt(request, extra));
374
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) => handlers.callTool(request, extra));
375
+ return server;
376
+ }
377
+ // Both protocol adapters register these same handlers. No transport may bypass
378
+ // the namespace dispatcher, grant checks, or target-bound confirmations.
379
+ export function createConvertMcpHandlers(options, clientInfoFor) {
380
+ const logger = failOpenLogger(options.logger ?? noopLogger);
381
+ const resourceProvider = composeResourceProviders(investigationPlaybookResourceProvider, options.resourceProvider);
326
382
  let staticCatalogRevisionsPromise;
327
- const toolCatalogRevisionPromises = new Map();
328
- const staticCatalogRevisions = () => {
383
+ const toolCatalogRevisionPromises = options.toolCatalogRevisionCache ?? new Map();
384
+ const staticCatalogRevisions = (extra) => {
329
385
  staticCatalogRevisionsPromise ??= Promise.all([
330
386
  catalogRevision(listPrompts()),
331
- Promise.resolve(resourceProvider.listResources(server.getClientVersion())).then(catalogRevision),
387
+ Promise.resolve(resourceProvider.listResources(clientInfoFor(extra))).then(catalogRevision),
332
388
  ]).then(([prompts, resources]) => ({ prompts, resources }));
333
389
  return staticCatalogRevisionsPromise;
334
390
  };
335
- const catalogRevisions = async (tools, cacheKey) => {
391
+ const catalogRevisions = async (tools, cacheKey, extra) => {
336
392
  let toolRevisionPromise = toolCatalogRevisionPromises.get(cacheKey);
337
393
  if (!toolRevisionPromise) {
338
394
  toolRevisionPromise = catalogRevision(tools);
@@ -340,7 +396,7 @@ export function createConvertMcpServer(options) {
340
396
  }
341
397
  const [toolRevision, staticRevisions] = await Promise.all([
342
398
  toolRevisionPromise,
343
- staticCatalogRevisions(),
399
+ staticCatalogRevisions(extra),
344
400
  ]);
345
401
  return { tools: toolRevision, ...staticRevisions };
346
402
  };
@@ -366,6 +422,7 @@ export function createConvertMcpServer(options) {
366
422
  `feedback=${resolveBooleanOption(options.feedbackEnabled)}`,
367
423
  `notifications=${resolveBooleanOption(options.notificationsEnabled)}`,
368
424
  `account-wide=${context?.authorizedAccounts?.some((account) => account.projects === null) === true}`,
425
+ `decorator=${options.toolCatalogDecoratorCacheKey?.() ?? 'none'}`,
369
426
  ].join(':');
370
427
  const notifyCatalogChanges = async (requestedCatalog, extra, tools, cacheKey) => {
371
428
  if (!options.catalogChangeNotifier || !cacheKey)
@@ -373,7 +430,7 @@ export function createConvertMcpServer(options) {
373
430
  try {
374
431
  await options.catalogChangeNotifier({
375
432
  requestedCatalog,
376
- revisions: await catalogRevisions(tools, cacheKey),
433
+ revisions: await catalogRevisions(tools, cacheKey, extra),
377
434
  sendNotification: extra.sendNotification,
378
435
  });
379
436
  }
@@ -390,7 +447,7 @@ export function createConvertMcpServer(options) {
390
447
  }
391
448
  }
392
449
  };
393
- server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
450
+ const listToolsHandler = async (extra) => {
394
451
  const toolSet = resolveToolSet(options.toolsForClient);
395
452
  const resolvedCatalog = await resolveCatalogContextSafely(extra);
396
453
  const catalogContext = resolvedCatalog.context;
@@ -401,54 +458,62 @@ export function createConvertMcpServer(options) {
401
458
  // request can emit list_changed when the full catalog recovers.
402
459
  catalogCacheKey(toolSet, catalogContext));
403
460
  return { tools };
404
- });
405
- if (hasResources) {
406
- server.setRequestHandler(ListResourcesRequestSchema, async (_request, extra) => {
407
- const toolSet = resolveToolSet(options.toolsForClient);
461
+ };
462
+ const listResourcesHandler = async (extra) => {
463
+ const toolSet = resolveToolSet(options.toolsForClient);
464
+ if (options.catalogChangeNotifier) {
408
465
  const resolvedCatalog = await resolveCatalogContextSafely(extra);
409
466
  const catalogContext = resolvedCatalog.context;
410
467
  const tools = getRuntimeTools(options, toolSet, catalogContext);
411
468
  await notifyCatalogChanges('resources', extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
412
- return { resources: await resourceProvider.listResources(server.getClientVersion()) };
413
- });
414
- server.setRequestHandler(ListResourceTemplatesRequestSchema, async (_request, extra) => {
469
+ }
470
+ return { resources: await resourceProvider.listResources(clientInfoFor(extra)) };
471
+ };
472
+ const listResourceTemplatesHandler = async (extra) => {
473
+ if (options.catalogChangeNotifier) {
415
474
  const toolSet = resolveToolSet(options.toolsForClient);
416
475
  const resolvedCatalog = await resolveCatalogContextSafely(extra);
417
476
  const catalogContext = resolvedCatalog.context;
418
477
  const tools = getRuntimeTools(options, toolSet, catalogContext);
419
478
  await notifyCatalogChanges(undefined, extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
420
- return { resourceTemplates: [] };
421
- });
422
- server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
479
+ }
480
+ return { resourceTemplates: [] };
481
+ };
482
+ const readResourceHandler = async (request, extra) => {
483
+ if (options.catalogChangeNotifier) {
423
484
  const toolSet = resolveToolSet(options.toolsForClient);
424
485
  const resolvedCatalog = await resolveCatalogContextSafely(extra);
425
486
  const catalogContext = resolvedCatalog.context;
426
487
  const tools = getRuntimeTools(options, toolSet, catalogContext);
427
488
  await notifyCatalogChanges(undefined, extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
428
- const result = await resourceProvider.readResource(request.params.uri, server.getClientVersion());
429
- if (!result) {
430
- throw new McpError(ErrorCode.InvalidParams, `Unknown resource URI: ${request.params.uri}`);
431
- }
432
- return result;
433
- });
434
- }
435
- server.setRequestHandler(ListPromptsRequestSchema, async (_request, extra) => {
436
- const toolSet = resolveToolSet(options.toolsForClient);
437
- const resolvedCatalog = await resolveCatalogContextSafely(extra);
438
- const catalogContext = resolvedCatalog.context;
439
- const tools = getRuntimeTools(options, toolSet, catalogContext);
440
- await notifyCatalogChanges('prompts', extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
489
+ }
490
+ const result = await resourceProvider.readResource(request.params.uri, clientInfoFor(extra));
491
+ if (!result) {
492
+ throw new McpError(ErrorCode.InvalidParams, `Unknown resource URI: ${request.params.uri}`);
493
+ }
494
+ return result;
495
+ };
496
+ const listPromptsHandler = async (extra) => {
497
+ if (options.catalogChangeNotifier) {
498
+ const toolSet = resolveToolSet(options.toolsForClient);
499
+ const resolvedCatalog = await resolveCatalogContextSafely(extra);
500
+ const catalogContext = resolvedCatalog.context;
501
+ const tools = getRuntimeTools(options, toolSet, catalogContext);
502
+ await notifyCatalogChanges('prompts', extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
503
+ }
441
504
  return { prompts: listPrompts() };
442
- });
443
- server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => {
505
+ };
506
+ const getPromptHandler = async (request, extra) => {
444
507
  const toolSet = resolveToolSet(options.toolsForClient);
445
- const resolvedCatalog = await resolveCatalogContextSafely(extra);
446
- const catalogContext = resolvedCatalog.context;
447
- const tools = getRuntimeTools(options, toolSet, catalogContext);
448
- await notifyCatalogChanges(undefined, extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
508
+ if (options.catalogChangeNotifier) {
509
+ const resolvedCatalog = await resolveCatalogContextSafely(extra);
510
+ const catalogContext = resolvedCatalog.context;
511
+ const tools = getRuntimeTools(options, toolSet, catalogContext);
512
+ await notifyCatalogChanges(undefined, extra, tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
513
+ }
449
514
  return getPrompt(request.params.name, request.params.arguments ?? {}, toolSet);
450
- });
451
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
515
+ };
516
+ const callToolHandler = async (request, extra, onToolResolved) => {
452
517
  const { name: toolName, arguments: toolArgs } = request.params;
453
518
  const toolSet = resolveToolSet(options.toolsForClient);
454
519
  const executionContext = await options.executionContextResolver?.(extra, request);
@@ -456,7 +521,27 @@ export function createConvertMcpServer(options) {
456
521
  ? { context: executionContext, authoritative: true }
457
522
  : await resolveCatalogContextSafely(extra);
458
523
  const catalogContext = resolvedCatalog.context;
459
- await notifyCatalogChanges(undefined, extra, getRuntimeTools(options, toolSet, catalogContext), resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
524
+ const lightweightBuiltin = toolName === 'search'
525
+ ? canonicalSearchTool
526
+ : toolName === 'fetch'
527
+ ? canonicalFetchTool
528
+ : undefined;
529
+ // Hosted modern HTTP creates one protocol Server per request. Avoid
530
+ // materializing and decorating the entire hosted catalog immediately
531
+ // before KB search/fetch solely to recover one static output schema. The
532
+ // legacy notifier path still reconciles the complete catalog, and a tool
533
+ // decorator is applied to the selected built-in so projection remains
534
+ // identical to its advertised definition.
535
+ const runtimeCatalog = options.catalogChangeNotifier || (onToolResolved && !lightweightBuiltin)
536
+ ? getRuntimeToolCatalog(options, toolSet, catalogContext)
537
+ : undefined;
538
+ const resolvedTool = lightweightBuiltin
539
+ ? (options.toolDecorator ? options.toolDecorator(lightweightBuiltin) : lightweightBuiltin)
540
+ : runtimeCatalog?.toolsByName.get(toolName);
541
+ onToolResolved?.(resolvedTool);
542
+ if (runtimeCatalog) {
543
+ await notifyCatalogChanges(undefined, extra, runtimeCatalog.tools, resolvedCatalog.authoritative ? catalogCacheKey(toolSet, catalogContext) : undefined);
544
+ }
460
545
  logger.log('callToolRequest', {
461
546
  toolName,
462
547
  hasAuthContext: Boolean(executionContext?.authContext || executionContext?.credentialsByAccount),
@@ -503,7 +588,16 @@ export function createConvertMcpServer(options) {
503
588
  structuredContent: { error: "The 'query' argument is required for search." },
504
589
  });
505
590
  }
506
- const results = await options.docs.search(query, 8);
591
+ if (query.length > MAX_SEARCH_QUERY_CHARACTERS) {
592
+ logger.log('search.invalidArguments');
593
+ const message = `The 'query' argument must be ${MAX_SEARCH_QUERY_CHARACTERS} characters or fewer.`;
594
+ return omitStructuredContentOnError('search', {
595
+ isError: true,
596
+ content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
597
+ structuredContent: { error: message },
598
+ });
599
+ }
600
+ const results = await options.docs.search(query, 8, extra.signal);
507
601
  const payload = toSearchPayload(results);
508
602
  logger.log('search.success', { matchCount: payload.results.length });
509
603
  return {
@@ -534,9 +628,9 @@ export function createConvertMcpServer(options) {
534
628
  }
535
629
  const rawId = rawKbId(id);
536
630
  const document = options.docs.getDocumentById
537
- ? await options.docs.getDocumentById(rawId)
631
+ ? await options.docs.getDocumentById(rawId, extra.signal)
538
632
  : null;
539
- const chunk = document?.requested ?? await options.docs.getById(rawId);
633
+ const chunk = document?.requested ?? await options.docs.getById(rawId, extra.signal);
540
634
  if (!chunk) {
541
635
  const message = `Knowledge base document not found for id "${id}". Run search again and fetch one of the returned IDs.`;
542
636
  logger.log('fetch.notFound');
@@ -656,8 +750,16 @@ export function createConvertMcpServer(options) {
656
750
  content: [{ type: 'text', text: `Error: Unknown tool requested: ${toolName}` }],
657
751
  structuredContent: { error: `Unknown tool: ${toolName}` },
658
752
  };
659
- });
660
- return server;
753
+ };
754
+ return {
755
+ listTools: listToolsHandler,
756
+ listResources: listResourcesHandler,
757
+ listResourceTemplates: listResourceTemplatesHandler,
758
+ readResource: readResourceHandler,
759
+ listPrompts: listPromptsHandler,
760
+ getPrompt: getPromptHandler,
761
+ callTool: callToolHandler,
762
+ };
661
763
  }
662
764
  const MAX_FETCH_TEXT_BYTES = 384 * 1024;
663
765
  function assembleKbDocument(document, sourceComplete) {