index.js 29 KB

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