index.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.Client = void 0;
  4. exports.getSupportedElicitationModes = getSupportedElicitationModes;
  5. const protocol_js_1 = require("../shared/protocol.js");
  6. const types_js_1 = require("../types.js");
  7. const ajv_provider_js_1 = require("../validation/ajv-provider.js");
  8. const zod_compat_js_1 = require("../server/zod-compat.js");
  9. const client_js_1 = require("../experimental/tasks/client.js");
  10. const helpers_js_1 = require("../experimental/tasks/helpers.js");
  11. /**
  12. * Elicitation default application helper. Applies defaults to the data based on the schema.
  13. *
  14. * @param schema - The schema to apply defaults to.
  15. * @param data - The data to apply defaults to.
  16. */
  17. function applyElicitationDefaults(schema, data) {
  18. if (!schema || data === null || typeof data !== 'object')
  19. return;
  20. // Handle object properties
  21. if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
  22. const obj = data;
  23. const props = schema.properties;
  24. for (const key of Object.keys(props)) {
  25. const propSchema = props[key];
  26. // If missing or explicitly undefined, apply default if present
  27. if (obj[key] === undefined && Object.prototype.hasOwnProperty.call(propSchema, 'default')) {
  28. obj[key] = propSchema.default;
  29. }
  30. // Recurse into existing nested objects/arrays
  31. if (obj[key] !== undefined) {
  32. applyElicitationDefaults(propSchema, obj[key]);
  33. }
  34. }
  35. }
  36. if (Array.isArray(schema.anyOf)) {
  37. for (const sub of schema.anyOf) {
  38. // Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
  39. if (typeof sub !== 'boolean') {
  40. applyElicitationDefaults(sub, data);
  41. }
  42. }
  43. }
  44. // Combine schemas
  45. if (Array.isArray(schema.oneOf)) {
  46. for (const sub of schema.oneOf) {
  47. // Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
  48. if (typeof sub !== 'boolean') {
  49. applyElicitationDefaults(sub, data);
  50. }
  51. }
  52. }
  53. }
  54. /**
  55. * Determines which elicitation modes are supported based on declared client capabilities.
  56. *
  57. * According to the spec:
  58. * - An empty elicitation capability object defaults to form mode support (backwards compatibility)
  59. * - URL mode is only supported if explicitly declared
  60. *
  61. * @param capabilities - The client's elicitation capabilities
  62. * @returns An object indicating which modes are supported
  63. */
  64. function getSupportedElicitationModes(capabilities) {
  65. if (!capabilities) {
  66. return { supportsFormMode: false, supportsUrlMode: false };
  67. }
  68. const hasFormCapability = capabilities.form !== undefined;
  69. const hasUrlCapability = capabilities.url !== undefined;
  70. // If neither form nor url are explicitly declared, form mode is supported (backwards compatibility)
  71. const supportsFormMode = hasFormCapability || (!hasFormCapability && !hasUrlCapability);
  72. const supportsUrlMode = hasUrlCapability;
  73. return { supportsFormMode, supportsUrlMode };
  74. }
  75. /**
  76. * An MCP client on top of a pluggable transport.
  77. *
  78. * The client will automatically begin the initialization flow with the server when connect() is called.
  79. *
  80. * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters:
  81. *
  82. * ```typescript
  83. * // Custom schemas
  84. * const CustomRequestSchema = RequestSchema.extend({...})
  85. * const CustomNotificationSchema = NotificationSchema.extend({...})
  86. * const CustomResultSchema = ResultSchema.extend({...})
  87. *
  88. * // Type aliases
  89. * type CustomRequest = z.infer<typeof CustomRequestSchema>
  90. * type CustomNotification = z.infer<typeof CustomNotificationSchema>
  91. * type CustomResult = z.infer<typeof CustomResultSchema>
  92. *
  93. * // Create typed client
  94. * const client = new Client<CustomRequest, CustomNotification, CustomResult>({
  95. * name: "CustomClient",
  96. * version: "1.0.0"
  97. * })
  98. * ```
  99. */
  100. class Client extends protocol_js_1.Protocol {
  101. /**
  102. * Initializes this client with the given name and version information.
  103. */
  104. constructor(_clientInfo, options) {
  105. super(options);
  106. this._clientInfo = _clientInfo;
  107. this._cachedToolOutputValidators = new Map();
  108. this._cachedKnownTaskTools = new Set();
  109. this._cachedRequiredTaskTools = new Set();
  110. this._listChangedDebounceTimers = new Map();
  111. this._capabilities = options?.capabilities ?? {};
  112. this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new ajv_provider_js_1.AjvJsonSchemaValidator();
  113. // Store list changed config for setup after connection (when we know server capabilities)
  114. if (options?.listChanged) {
  115. this._pendingListChangedConfig = options.listChanged;
  116. }
  117. }
  118. /**
  119. * Set up handlers for list changed notifications based on config and server capabilities.
  120. * This should only be called after initialization when server capabilities are known.
  121. * Handlers are silently skipped if the server doesn't advertise the corresponding listChanged capability.
  122. * @internal
  123. */
  124. _setupListChangedHandlers(config) {
  125. if (config.tools && this._serverCapabilities?.tools?.listChanged) {
  126. this._setupListChangedHandler('tools', types_js_1.ToolListChangedNotificationSchema, config.tools, async () => {
  127. const result = await this.listTools();
  128. return result.tools;
  129. });
  130. }
  131. if (config.prompts && this._serverCapabilities?.prompts?.listChanged) {
  132. this._setupListChangedHandler('prompts', types_js_1.PromptListChangedNotificationSchema, config.prompts, async () => {
  133. const result = await this.listPrompts();
  134. return result.prompts;
  135. });
  136. }
  137. if (config.resources && this._serverCapabilities?.resources?.listChanged) {
  138. this._setupListChangedHandler('resources', types_js_1.ResourceListChangedNotificationSchema, config.resources, async () => {
  139. const result = await this.listResources();
  140. return result.resources;
  141. });
  142. }
  143. }
  144. /**
  145. * Access experimental features.
  146. *
  147. * WARNING: These APIs are experimental and may change without notice.
  148. *
  149. * @experimental
  150. */
  151. get experimental() {
  152. if (!this._experimental) {
  153. this._experimental = {
  154. tasks: new client_js_1.ExperimentalClientTasks(this)
  155. };
  156. }
  157. return this._experimental;
  158. }
  159. /**
  160. * Registers new capabilities. This can only be called before connecting to a transport.
  161. *
  162. * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
  163. */
  164. registerCapabilities(capabilities) {
  165. if (this.transport) {
  166. throw new Error('Cannot register capabilities after connecting to transport');
  167. }
  168. this._capabilities = (0, protocol_js_1.mergeCapabilities)(this._capabilities, capabilities);
  169. }
  170. /**
  171. * Override request handler registration to enforce client-side validation for elicitation.
  172. */
  173. setRequestHandler(requestSchema, handler) {
  174. const shape = (0, zod_compat_js_1.getObjectShape)(requestSchema);
  175. const methodSchema = shape?.method;
  176. if (!methodSchema) {
  177. throw new Error('Schema is missing a method literal');
  178. }
  179. // Extract literal value using type-safe property access
  180. let methodValue;
  181. if ((0, zod_compat_js_1.isZ4Schema)(methodSchema)) {
  182. const v4Schema = methodSchema;
  183. const v4Def = v4Schema._zod?.def;
  184. methodValue = v4Def?.value ?? v4Schema.value;
  185. }
  186. else {
  187. const v3Schema = methodSchema;
  188. const legacyDef = v3Schema._def;
  189. methodValue = legacyDef?.value ?? v3Schema.value;
  190. }
  191. if (typeof methodValue !== 'string') {
  192. throw new Error('Schema method literal must be a string');
  193. }
  194. const method = methodValue;
  195. if (method === 'elicitation/create') {
  196. const wrappedHandler = async (request, extra) => {
  197. const validatedRequest = (0, zod_compat_js_1.safeParse)(types_js_1.ElicitRequestSchema, request);
  198. if (!validatedRequest.success) {
  199. // Type guard: if success is false, error is guaranteed to exist
  200. const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
  201. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
  202. }
  203. const { params } = validatedRequest.data;
  204. params.mode = params.mode ?? 'form';
  205. const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation);
  206. if (params.mode === 'form' && !supportsFormMode) {
  207. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Client does not support form-mode elicitation requests');
  208. }
  209. if (params.mode === 'url' && !supportsUrlMode) {
  210. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
  211. }
  212. const result = await Promise.resolve(handler(request, extra));
  213. // When task creation is requested, validate and return CreateTaskResult
  214. if (params.task) {
  215. const taskValidationResult = (0, zod_compat_js_1.safeParse)(types_js_1.CreateTaskResultSchema, result);
  216. if (!taskValidationResult.success) {
  217. const errorMessage = taskValidationResult.error instanceof Error
  218. ? taskValidationResult.error.message
  219. : String(taskValidationResult.error);
  220. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
  221. }
  222. return taskValidationResult.data;
  223. }
  224. // For non-task requests, validate against ElicitResultSchema
  225. const validationResult = (0, zod_compat_js_1.safeParse)(types_js_1.ElicitResultSchema, result);
  226. if (!validationResult.success) {
  227. // Type guard: if success is false, error is guaranteed to exist
  228. const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
  229. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
  230. }
  231. const validatedResult = validationResult.data;
  232. const requestedSchema = params.mode === 'form' ? params.requestedSchema : undefined;
  233. if (params.mode === 'form' && validatedResult.action === 'accept' && validatedResult.content && requestedSchema) {
  234. if (this._capabilities.elicitation?.form?.applyDefaults) {
  235. try {
  236. applyElicitationDefaults(requestedSchema, validatedResult.content);
  237. }
  238. catch {
  239. // gracefully ignore errors in default application
  240. }
  241. }
  242. }
  243. return validatedResult;
  244. };
  245. // Install the wrapped handler
  246. return super.setRequestHandler(requestSchema, wrappedHandler);
  247. }
  248. if (method === 'sampling/createMessage') {
  249. const wrappedHandler = async (request, extra) => {
  250. const validatedRequest = (0, zod_compat_js_1.safeParse)(types_js_1.CreateMessageRequestSchema, request);
  251. if (!validatedRequest.success) {
  252. const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
  253. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
  254. }
  255. const { params } = validatedRequest.data;
  256. const result = await Promise.resolve(handler(request, extra));
  257. // When task creation is requested, validate and return CreateTaskResult
  258. if (params.task) {
  259. const taskValidationResult = (0, zod_compat_js_1.safeParse)(types_js_1.CreateTaskResultSchema, result);
  260. if (!taskValidationResult.success) {
  261. const errorMessage = taskValidationResult.error instanceof Error
  262. ? taskValidationResult.error.message
  263. : String(taskValidationResult.error);
  264. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
  265. }
  266. return taskValidationResult.data;
  267. }
  268. // For non-task requests, validate against appropriate schema based on tools presence
  269. const hasTools = params.tools || params.toolChoice;
  270. const resultSchema = hasTools ? types_js_1.CreateMessageResultWithToolsSchema : types_js_1.CreateMessageResultSchema;
  271. const validationResult = (0, zod_compat_js_1.safeParse)(resultSchema, result);
  272. if (!validationResult.success) {
  273. const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
  274. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
  275. }
  276. return validationResult.data;
  277. };
  278. // Install the wrapped handler
  279. return super.setRequestHandler(requestSchema, wrappedHandler);
  280. }
  281. // Other handlers use default behavior
  282. return super.setRequestHandler(requestSchema, handler);
  283. }
  284. assertCapability(capability, method) {
  285. if (!this._serverCapabilities?.[capability]) {
  286. throw new Error(`Server does not support ${capability} (required for ${method})`);
  287. }
  288. }
  289. async connect(transport, options) {
  290. await super.connect(transport);
  291. // When transport sessionId is already set this means we are trying to reconnect.
  292. // In this case we don't need to initialize again.
  293. if (transport.sessionId !== undefined) {
  294. return;
  295. }
  296. try {
  297. const result = await this.request({
  298. method: 'initialize',
  299. params: {
  300. protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
  301. capabilities: this._capabilities,
  302. clientInfo: this._clientInfo
  303. }
  304. }, types_js_1.InitializeResultSchema, options);
  305. if (result === undefined) {
  306. throw new Error(`Server sent invalid initialize result: ${result}`);
  307. }
  308. if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
  309. throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
  310. }
  311. this._serverCapabilities = result.capabilities;
  312. this._serverVersion = result.serverInfo;
  313. // HTTP transports must set the protocol version in each header after initialization.
  314. if (transport.setProtocolVersion) {
  315. transport.setProtocolVersion(result.protocolVersion);
  316. }
  317. this._instructions = result.instructions;
  318. await this.notification({
  319. method: 'notifications/initialized'
  320. });
  321. // Set up list changed handlers now that we know server capabilities
  322. if (this._pendingListChangedConfig) {
  323. this._setupListChangedHandlers(this._pendingListChangedConfig);
  324. this._pendingListChangedConfig = undefined;
  325. }
  326. }
  327. catch (error) {
  328. // Disconnect if initialization fails.
  329. void this.close();
  330. throw error;
  331. }
  332. }
  333. /**
  334. * After initialization has completed, this will be populated with the server's reported capabilities.
  335. */
  336. getServerCapabilities() {
  337. return this._serverCapabilities;
  338. }
  339. /**
  340. * After initialization has completed, this will be populated with information about the server's name and version.
  341. */
  342. getServerVersion() {
  343. return this._serverVersion;
  344. }
  345. /**
  346. * After initialization has completed, this may be populated with information about the server's instructions.
  347. */
  348. getInstructions() {
  349. return this._instructions;
  350. }
  351. assertCapabilityForMethod(method) {
  352. switch (method) {
  353. case 'logging/setLevel':
  354. if (!this._serverCapabilities?.logging) {
  355. throw new Error(`Server does not support logging (required for ${method})`);
  356. }
  357. break;
  358. case 'prompts/get':
  359. case 'prompts/list':
  360. if (!this._serverCapabilities?.prompts) {
  361. throw new Error(`Server does not support prompts (required for ${method})`);
  362. }
  363. break;
  364. case 'resources/list':
  365. case 'resources/templates/list':
  366. case 'resources/read':
  367. case 'resources/subscribe':
  368. case 'resources/unsubscribe':
  369. if (!this._serverCapabilities?.resources) {
  370. throw new Error(`Server does not support resources (required for ${method})`);
  371. }
  372. if (method === 'resources/subscribe' && !this._serverCapabilities.resources.subscribe) {
  373. throw new Error(`Server does not support resource subscriptions (required for ${method})`);
  374. }
  375. break;
  376. case 'tools/call':
  377. case 'tools/list':
  378. if (!this._serverCapabilities?.tools) {
  379. throw new Error(`Server does not support tools (required for ${method})`);
  380. }
  381. break;
  382. case 'completion/complete':
  383. if (!this._serverCapabilities?.completions) {
  384. throw new Error(`Server does not support completions (required for ${method})`);
  385. }
  386. break;
  387. case 'initialize':
  388. // No specific capability required for initialize
  389. break;
  390. case 'ping':
  391. // No specific capability required for ping
  392. break;
  393. }
  394. }
  395. assertNotificationCapability(method) {
  396. switch (method) {
  397. case 'notifications/roots/list_changed':
  398. if (!this._capabilities.roots?.listChanged) {
  399. throw new Error(`Client does not support roots list changed notifications (required for ${method})`);
  400. }
  401. break;
  402. case 'notifications/initialized':
  403. // No specific capability required for initialized
  404. break;
  405. case 'notifications/cancelled':
  406. // Cancellation notifications are always allowed
  407. break;
  408. case 'notifications/progress':
  409. // Progress notifications are always allowed
  410. break;
  411. }
  412. }
  413. assertRequestHandlerCapability(method) {
  414. // Task handlers are registered in Protocol constructor before _capabilities is initialized
  415. // Skip capability check for task methods during initialization
  416. if (!this._capabilities) {
  417. return;
  418. }
  419. switch (method) {
  420. case 'sampling/createMessage':
  421. if (!this._capabilities.sampling) {
  422. throw new Error(`Client does not support sampling capability (required for ${method})`);
  423. }
  424. break;
  425. case 'elicitation/create':
  426. if (!this._capabilities.elicitation) {
  427. throw new Error(`Client does not support elicitation capability (required for ${method})`);
  428. }
  429. break;
  430. case 'roots/list':
  431. if (!this._capabilities.roots) {
  432. throw new Error(`Client does not support roots capability (required for ${method})`);
  433. }
  434. break;
  435. case 'tasks/get':
  436. case 'tasks/list':
  437. case 'tasks/result':
  438. case 'tasks/cancel':
  439. if (!this._capabilities.tasks) {
  440. throw new Error(`Client does not support tasks capability (required for ${method})`);
  441. }
  442. break;
  443. case 'ping':
  444. // No specific capability required for ping
  445. break;
  446. }
  447. }
  448. assertTaskCapability(method) {
  449. (0, helpers_js_1.assertToolsCallTaskCapability)(this._serverCapabilities?.tasks?.requests, method, 'Server');
  450. }
  451. assertTaskHandlerCapability(method) {
  452. // Task handlers are registered in Protocol constructor before _capabilities is initialized
  453. // Skip capability check for task methods during initialization
  454. if (!this._capabilities) {
  455. return;
  456. }
  457. (0, helpers_js_1.assertClientRequestTaskCapability)(this._capabilities.tasks?.requests, method, 'Client');
  458. }
  459. async ping(options) {
  460. return this.request({ method: 'ping' }, types_js_1.EmptyResultSchema, options);
  461. }
  462. async complete(params, options) {
  463. return this.request({ method: 'completion/complete', params }, types_js_1.CompleteResultSchema, options);
  464. }
  465. async setLoggingLevel(level, options) {
  466. return this.request({ method: 'logging/setLevel', params: { level } }, types_js_1.EmptyResultSchema, options);
  467. }
  468. async getPrompt(params, options) {
  469. return this.request({ method: 'prompts/get', params }, types_js_1.GetPromptResultSchema, options);
  470. }
  471. async listPrompts(params, options) {
  472. return this.request({ method: 'prompts/list', params }, types_js_1.ListPromptsResultSchema, options);
  473. }
  474. async listResources(params, options) {
  475. return this.request({ method: 'resources/list', params }, types_js_1.ListResourcesResultSchema, options);
  476. }
  477. async listResourceTemplates(params, options) {
  478. return this.request({ method: 'resources/templates/list', params }, types_js_1.ListResourceTemplatesResultSchema, options);
  479. }
  480. async readResource(params, options) {
  481. return this.request({ method: 'resources/read', params }, types_js_1.ReadResourceResultSchema, options);
  482. }
  483. async subscribeResource(params, options) {
  484. return this.request({ method: 'resources/subscribe', params }, types_js_1.EmptyResultSchema, options);
  485. }
  486. async unsubscribeResource(params, options) {
  487. return this.request({ method: 'resources/unsubscribe', params }, types_js_1.EmptyResultSchema, options);
  488. }
  489. /**
  490. * Calls a tool and waits for the result. Automatically validates structured output if the tool has an outputSchema.
  491. *
  492. * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
  493. */
  494. async callTool(params, resultSchema = types_js_1.CallToolResultSchema, options) {
  495. // Guard: required-task tools need experimental API
  496. if (this.isToolTaskRequired(params.name)) {
  497. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`);
  498. }
  499. const result = await this.request({ method: 'tools/call', params }, resultSchema, options);
  500. // Check if the tool has an outputSchema
  501. const validator = this.getToolOutputValidator(params.name);
  502. if (validator) {
  503. // If tool has outputSchema, it MUST return structuredContent (unless it's an error)
  504. if (!result.structuredContent && !result.isError) {
  505. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Tool ${params.name} has an output schema but did not return structured content`);
  506. }
  507. // Only validate structured content if present (not when there's an error)
  508. if (result.structuredContent) {
  509. try {
  510. // Validate the structured content against the schema
  511. const validationResult = validator(result.structuredContent);
  512. if (!validationResult.valid) {
  513. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validationResult.errorMessage}`);
  514. }
  515. }
  516. catch (error) {
  517. if (error instanceof types_js_1.McpError) {
  518. throw error;
  519. }
  520. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`);
  521. }
  522. }
  523. }
  524. return result;
  525. }
  526. isToolTask(toolName) {
  527. if (!this._serverCapabilities?.tasks?.requests?.tools?.call) {
  528. return false;
  529. }
  530. return this._cachedKnownTaskTools.has(toolName);
  531. }
  532. /**
  533. * Check if a tool requires task-based execution.
  534. * Unlike isToolTask which includes 'optional' tools, this only checks for 'required'.
  535. */
  536. isToolTaskRequired(toolName) {
  537. return this._cachedRequiredTaskTools.has(toolName);
  538. }
  539. /**
  540. * Cache validators for tool output schemas.
  541. * Called after listTools() to pre-compile validators for better performance.
  542. */
  543. cacheToolMetadata(tools) {
  544. this._cachedToolOutputValidators.clear();
  545. this._cachedKnownTaskTools.clear();
  546. this._cachedRequiredTaskTools.clear();
  547. for (const tool of tools) {
  548. // If the tool has an outputSchema, create and cache the validator
  549. if (tool.outputSchema) {
  550. const toolValidator = this._jsonSchemaValidator.getValidator(tool.outputSchema);
  551. this._cachedToolOutputValidators.set(tool.name, toolValidator);
  552. }
  553. // If the tool supports task-based execution, cache that information
  554. const taskSupport = tool.execution?.taskSupport;
  555. if (taskSupport === 'required' || taskSupport === 'optional') {
  556. this._cachedKnownTaskTools.add(tool.name);
  557. }
  558. if (taskSupport === 'required') {
  559. this._cachedRequiredTaskTools.add(tool.name);
  560. }
  561. }
  562. }
  563. /**
  564. * Get cached validator for a tool
  565. */
  566. getToolOutputValidator(toolName) {
  567. return this._cachedToolOutputValidators.get(toolName);
  568. }
  569. async listTools(params, options) {
  570. const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
  571. // Cache the tools and their output schemas for future validation
  572. this.cacheToolMetadata(result.tools);
  573. return result;
  574. }
  575. /**
  576. * Set up a single list changed handler.
  577. * @internal
  578. */
  579. _setupListChangedHandler(listType, notificationSchema, options, fetcher) {
  580. // Validate options using Zod schema (validates autoRefresh and debounceMs)
  581. const parseResult = types_js_1.ListChangedOptionsBaseSchema.safeParse(options);
  582. if (!parseResult.success) {
  583. throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`);
  584. }
  585. // Validate callback
  586. if (typeof options.onChanged !== 'function') {
  587. throw new Error(`Invalid ${listType} listChanged options: onChanged must be a function`);
  588. }
  589. const { autoRefresh, debounceMs } = parseResult.data;
  590. const { onChanged } = options;
  591. const refresh = async () => {
  592. if (!autoRefresh) {
  593. onChanged(null, null);
  594. return;
  595. }
  596. try {
  597. const items = await fetcher();
  598. onChanged(null, items);
  599. }
  600. catch (e) {
  601. const error = e instanceof Error ? e : new Error(String(e));
  602. onChanged(error, null);
  603. }
  604. };
  605. const handler = () => {
  606. if (debounceMs) {
  607. // Clear any pending debounce timer for this list type
  608. const existingTimer = this._listChangedDebounceTimers.get(listType);
  609. if (existingTimer) {
  610. clearTimeout(existingTimer);
  611. }
  612. // Set up debounced refresh
  613. const timer = setTimeout(refresh, debounceMs);
  614. this._listChangedDebounceTimers.set(listType, timer);
  615. }
  616. else {
  617. // No debounce, refresh immediately
  618. refresh();
  619. }
  620. };
  621. // Register notification handler
  622. this.setNotificationHandler(notificationSchema, handler);
  623. }
  624. async sendRootsListChanged() {
  625. return this.notification({ method: 'notifications/roots/list_changed' });
  626. }
  627. }
  628. exports.Client = Client;
  629. //# sourceMappingURL=index.js.map