mcp.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. import { Server } from './index.js';
  2. import { normalizeObjectSchema, safeParseAsync, getObjectShape, objectFromShape, getParseErrorMessage, getSchemaDescription, isSchemaOptional, getLiteralValue } from './zod-compat.js';
  3. import { toJsonSchemaCompat } from './zod-json-schema-compat.js';
  4. import { McpError, ErrorCode, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, CompleteRequestSchema, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate } from '../types.js';
  5. import { isCompletable, getCompleter } from './completable.js';
  6. import { UriTemplate } from '../shared/uriTemplate.js';
  7. import { validateAndWarnToolName } from '../shared/toolNameValidation.js';
  8. import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js';
  9. import { ZodOptional } from 'zod';
  10. /**
  11. * High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
  12. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying
  13. * Server instance available via the `server` property.
  14. */
  15. export class McpServer {
  16. constructor(serverInfo, options) {
  17. this._registeredResources = {};
  18. this._registeredResourceTemplates = {};
  19. this._registeredTools = {};
  20. this._registeredPrompts = {};
  21. this._toolHandlersInitialized = false;
  22. this._completionHandlerInitialized = false;
  23. this._resourceHandlersInitialized = false;
  24. this._promptHandlersInitialized = false;
  25. this.server = new Server(serverInfo, options);
  26. }
  27. /**
  28. * Access experimental features.
  29. *
  30. * WARNING: These APIs are experimental and may change without notice.
  31. *
  32. * @experimental
  33. */
  34. get experimental() {
  35. if (!this._experimental) {
  36. this._experimental = {
  37. tasks: new ExperimentalMcpServerTasks(this)
  38. };
  39. }
  40. return this._experimental;
  41. }
  42. /**
  43. * Attaches to the given transport, starts it, and starts listening for messages.
  44. *
  45. * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
  46. */
  47. async connect(transport) {
  48. return await this.server.connect(transport);
  49. }
  50. /**
  51. * Closes the connection.
  52. */
  53. async close() {
  54. await this.server.close();
  55. }
  56. setToolRequestHandlers() {
  57. if (this._toolHandlersInitialized) {
  58. return;
  59. }
  60. this.server.assertCanSetRequestHandler(getMethodValue(ListToolsRequestSchema));
  61. this.server.assertCanSetRequestHandler(getMethodValue(CallToolRequestSchema));
  62. this.server.registerCapabilities({
  63. tools: {
  64. listChanged: true
  65. }
  66. });
  67. this.server.setRequestHandler(ListToolsRequestSchema, () => ({
  68. tools: Object.entries(this._registeredTools)
  69. .filter(([, tool]) => tool.enabled)
  70. .map(([name, tool]) => {
  71. const toolDefinition = {
  72. name,
  73. title: tool.title,
  74. description: tool.description,
  75. inputSchema: (() => {
  76. const obj = normalizeObjectSchema(tool.inputSchema);
  77. return obj
  78. ? toJsonSchemaCompat(obj, {
  79. strictUnions: true,
  80. pipeStrategy: 'input'
  81. })
  82. : EMPTY_OBJECT_JSON_SCHEMA;
  83. })(),
  84. annotations: tool.annotations,
  85. execution: tool.execution,
  86. _meta: tool._meta
  87. };
  88. if (tool.outputSchema) {
  89. const obj = normalizeObjectSchema(tool.outputSchema);
  90. if (obj) {
  91. toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
  92. strictUnions: true,
  93. pipeStrategy: 'output'
  94. });
  95. }
  96. }
  97. return toolDefinition;
  98. })
  99. }));
  100. this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  101. try {
  102. const tool = this._registeredTools[request.params.name];
  103. if (!tool) {
  104. throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
  105. }
  106. if (!tool.enabled) {
  107. throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
  108. }
  109. const isTaskRequest = !!request.params.task;
  110. const taskSupport = tool.execution?.taskSupport;
  111. const isTaskHandler = 'createTask' in tool.handler;
  112. // Validate task hint configuration
  113. if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) {
  114. throw new McpError(ErrorCode.InternalError, `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`);
  115. }
  116. // Handle taskSupport 'required' without task augmentation
  117. if (taskSupport === 'required' && !isTaskRequest) {
  118. throw new McpError(ErrorCode.MethodNotFound, `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`);
  119. }
  120. // Handle taskSupport 'optional' without task augmentation - automatic polling
  121. if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) {
  122. return await this.handleAutomaticTaskPolling(tool, request, extra);
  123. }
  124. // Normal execution path
  125. const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
  126. const result = await this.executeToolHandler(tool, args, extra);
  127. // Return CreateTaskResult immediately for task requests
  128. if (isTaskRequest) {
  129. return result;
  130. }
  131. // Validate output schema for non-task requests
  132. await this.validateToolOutput(tool, result, request.params.name);
  133. return result;
  134. }
  135. catch (error) {
  136. if (error instanceof McpError) {
  137. if (error.code === ErrorCode.UrlElicitationRequired) {
  138. throw error; // Return the error to the caller without wrapping in CallToolResult
  139. }
  140. }
  141. return this.createToolError(error instanceof Error ? error.message : String(error));
  142. }
  143. });
  144. this._toolHandlersInitialized = true;
  145. }
  146. /**
  147. * Creates a tool error result.
  148. *
  149. * @param errorMessage - The error message.
  150. * @returns The tool error result.
  151. */
  152. createToolError(errorMessage) {
  153. return {
  154. content: [
  155. {
  156. type: 'text',
  157. text: errorMessage
  158. }
  159. ],
  160. isError: true
  161. };
  162. }
  163. /**
  164. * Validates tool input arguments against the tool's input schema.
  165. */
  166. async validateToolInput(tool, args, toolName) {
  167. if (!tool.inputSchema) {
  168. return undefined;
  169. }
  170. // Try to normalize to object schema first (for raw shapes and object schemas)
  171. // If that fails, use the schema directly (for union/intersection/etc)
  172. const inputObj = normalizeObjectSchema(tool.inputSchema);
  173. const schemaToParse = inputObj ?? tool.inputSchema;
  174. const parseResult = await safeParseAsync(schemaToParse, args);
  175. if (!parseResult.success) {
  176. const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
  177. const errorMessage = getParseErrorMessage(error);
  178. throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`);
  179. }
  180. return parseResult.data;
  181. }
  182. /**
  183. * Validates tool output against the tool's output schema.
  184. */
  185. async validateToolOutput(tool, result, toolName) {
  186. if (!tool.outputSchema) {
  187. return;
  188. }
  189. // Only validate CallToolResult, not CreateTaskResult
  190. if (!('content' in result)) {
  191. return;
  192. }
  193. if (result.isError) {
  194. return;
  195. }
  196. if (!result.structuredContent) {
  197. throw new McpError(ErrorCode.InvalidParams, `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`);
  198. }
  199. // if the tool has an output schema, validate structured content
  200. const outputObj = normalizeObjectSchema(tool.outputSchema);
  201. const parseResult = await safeParseAsync(outputObj, result.structuredContent);
  202. if (!parseResult.success) {
  203. const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
  204. const errorMessage = getParseErrorMessage(error);
  205. throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`);
  206. }
  207. }
  208. /**
  209. * Executes a tool handler (either regular or task-based).
  210. */
  211. async executeToolHandler(tool, args, extra) {
  212. const handler = tool.handler;
  213. const isTaskHandler = 'createTask' in handler;
  214. if (isTaskHandler) {
  215. if (!extra.taskStore) {
  216. throw new Error('No task store provided.');
  217. }
  218. const taskExtra = { ...extra, taskStore: extra.taskStore };
  219. if (tool.inputSchema) {
  220. const typedHandler = handler;
  221. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  222. return await Promise.resolve(typedHandler.createTask(args, taskExtra));
  223. }
  224. else {
  225. const typedHandler = handler;
  226. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  227. return await Promise.resolve(typedHandler.createTask(taskExtra));
  228. }
  229. }
  230. if (tool.inputSchema) {
  231. const typedHandler = handler;
  232. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  233. return await Promise.resolve(typedHandler(args, extra));
  234. }
  235. else {
  236. const typedHandler = handler;
  237. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  238. return await Promise.resolve(typedHandler(extra));
  239. }
  240. }
  241. /**
  242. * Handles automatic task polling for tools with taskSupport 'optional'.
  243. */
  244. async handleAutomaticTaskPolling(tool, request, extra) {
  245. if (!extra.taskStore) {
  246. throw new Error('No task store provided for task-capable tool.');
  247. }
  248. // Validate input and create task
  249. const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
  250. const handler = tool.handler;
  251. const taskExtra = { ...extra, taskStore: extra.taskStore };
  252. const createTaskResult = args // undefined only if tool.inputSchema is undefined
  253. ? await Promise.resolve(handler.createTask(args, taskExtra))
  254. : // eslint-disable-next-line @typescript-eslint/no-explicit-any
  255. await Promise.resolve(handler.createTask(taskExtra));
  256. // Poll until completion
  257. const taskId = createTaskResult.task.taskId;
  258. let task = createTaskResult.task;
  259. const pollInterval = task.pollInterval ?? 5000;
  260. while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') {
  261. await new Promise(resolve => setTimeout(resolve, pollInterval));
  262. const updatedTask = await extra.taskStore.getTask(taskId);
  263. if (!updatedTask) {
  264. throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
  265. }
  266. task = updatedTask;
  267. }
  268. // Return the final result
  269. return (await extra.taskStore.getTaskResult(taskId));
  270. }
  271. setCompletionRequestHandler() {
  272. if (this._completionHandlerInitialized) {
  273. return;
  274. }
  275. this.server.assertCanSetRequestHandler(getMethodValue(CompleteRequestSchema));
  276. this.server.registerCapabilities({
  277. completions: {}
  278. });
  279. this.server.setRequestHandler(CompleteRequestSchema, async (request) => {
  280. switch (request.params.ref.type) {
  281. case 'ref/prompt':
  282. assertCompleteRequestPrompt(request);
  283. return this.handlePromptCompletion(request, request.params.ref);
  284. case 'ref/resource':
  285. assertCompleteRequestResourceTemplate(request);
  286. return this.handleResourceCompletion(request, request.params.ref);
  287. default:
  288. throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
  289. }
  290. });
  291. this._completionHandlerInitialized = true;
  292. }
  293. async handlePromptCompletion(request, ref) {
  294. const prompt = this._registeredPrompts[ref.name];
  295. if (!prompt) {
  296. throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`);
  297. }
  298. if (!prompt.enabled) {
  299. throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} disabled`);
  300. }
  301. if (!prompt.argsSchema) {
  302. return EMPTY_COMPLETION_RESULT;
  303. }
  304. const promptShape = getObjectShape(prompt.argsSchema);
  305. const field = promptShape?.[request.params.argument.name];
  306. if (!isCompletable(field)) {
  307. return EMPTY_COMPLETION_RESULT;
  308. }
  309. const completer = getCompleter(field);
  310. if (!completer) {
  311. return EMPTY_COMPLETION_RESULT;
  312. }
  313. const suggestions = await completer(request.params.argument.value, request.params.context);
  314. return createCompletionResult(suggestions);
  315. }
  316. async handleResourceCompletion(request, ref) {
  317. const template = Object.values(this._registeredResourceTemplates).find(t => t.resourceTemplate.uriTemplate.toString() === ref.uri);
  318. if (!template) {
  319. if (this._registeredResources[ref.uri]) {
  320. // Attempting to autocomplete a fixed resource URI is not an error in the spec (but probably should be).
  321. return EMPTY_COMPLETION_RESULT;
  322. }
  323. throw new McpError(ErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`);
  324. }
  325. const completer = template.resourceTemplate.completeCallback(request.params.argument.name);
  326. if (!completer) {
  327. return EMPTY_COMPLETION_RESULT;
  328. }
  329. const suggestions = await completer(request.params.argument.value, request.params.context);
  330. return createCompletionResult(suggestions);
  331. }
  332. setResourceRequestHandlers() {
  333. if (this._resourceHandlersInitialized) {
  334. return;
  335. }
  336. this.server.assertCanSetRequestHandler(getMethodValue(ListResourcesRequestSchema));
  337. this.server.assertCanSetRequestHandler(getMethodValue(ListResourceTemplatesRequestSchema));
  338. this.server.assertCanSetRequestHandler(getMethodValue(ReadResourceRequestSchema));
  339. this.server.registerCapabilities({
  340. resources: {
  341. listChanged: true
  342. }
  343. });
  344. this.server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => {
  345. const resources = Object.entries(this._registeredResources)
  346. .filter(([_, resource]) => resource.enabled)
  347. .map(([uri, resource]) => ({
  348. uri,
  349. name: resource.name,
  350. ...resource.metadata
  351. }));
  352. const templateResources = [];
  353. for (const template of Object.values(this._registeredResourceTemplates)) {
  354. if (!template.resourceTemplate.listCallback) {
  355. continue;
  356. }
  357. const result = await template.resourceTemplate.listCallback(extra);
  358. for (const resource of result.resources) {
  359. templateResources.push({
  360. ...template.metadata,
  361. // the defined resource metadata should override the template metadata if present
  362. ...resource
  363. });
  364. }
  365. }
  366. return { resources: [...resources, ...templateResources] };
  367. });
  368. this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
  369. const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({
  370. name,
  371. uriTemplate: template.resourceTemplate.uriTemplate.toString(),
  372. ...template.metadata
  373. }));
  374. return { resourceTemplates };
  375. });
  376. this.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
  377. const uri = new URL(request.params.uri);
  378. // First check for exact resource match
  379. const resource = this._registeredResources[uri.toString()];
  380. if (resource) {
  381. if (!resource.enabled) {
  382. throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} disabled`);
  383. }
  384. return resource.readCallback(uri, extra);
  385. }
  386. // Then check templates
  387. for (const template of Object.values(this._registeredResourceTemplates)) {
  388. const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
  389. if (variables) {
  390. return template.readCallback(uri, variables, extra);
  391. }
  392. }
  393. throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
  394. });
  395. this._resourceHandlersInitialized = true;
  396. }
  397. setPromptRequestHandlers() {
  398. if (this._promptHandlersInitialized) {
  399. return;
  400. }
  401. this.server.assertCanSetRequestHandler(getMethodValue(ListPromptsRequestSchema));
  402. this.server.assertCanSetRequestHandler(getMethodValue(GetPromptRequestSchema));
  403. this.server.registerCapabilities({
  404. prompts: {
  405. listChanged: true
  406. }
  407. });
  408. this.server.setRequestHandler(ListPromptsRequestSchema, () => ({
  409. prompts: Object.entries(this._registeredPrompts)
  410. .filter(([, prompt]) => prompt.enabled)
  411. .map(([name, prompt]) => {
  412. return {
  413. name,
  414. title: prompt.title,
  415. description: prompt.description,
  416. arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined
  417. };
  418. })
  419. }));
  420. this.server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => {
  421. const prompt = this._registeredPrompts[request.params.name];
  422. if (!prompt) {
  423. throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
  424. }
  425. if (!prompt.enabled) {
  426. throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`);
  427. }
  428. if (prompt.argsSchema) {
  429. const argsObj = normalizeObjectSchema(prompt.argsSchema);
  430. const parseResult = await safeParseAsync(argsObj, request.params.arguments);
  431. if (!parseResult.success) {
  432. const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
  433. const errorMessage = getParseErrorMessage(error);
  434. throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`);
  435. }
  436. const args = parseResult.data;
  437. const cb = prompt.callback;
  438. return await Promise.resolve(cb(args, extra));
  439. }
  440. else {
  441. const cb = prompt.callback;
  442. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  443. return await Promise.resolve(cb(extra));
  444. }
  445. });
  446. this._promptHandlersInitialized = true;
  447. }
  448. resource(name, uriOrTemplate, ...rest) {
  449. let metadata;
  450. if (typeof rest[0] === 'object') {
  451. metadata = rest.shift();
  452. }
  453. const readCallback = rest[0];
  454. if (typeof uriOrTemplate === 'string') {
  455. if (this._registeredResources[uriOrTemplate]) {
  456. throw new Error(`Resource ${uriOrTemplate} is already registered`);
  457. }
  458. const registeredResource = this._createRegisteredResource(name, undefined, uriOrTemplate, metadata, readCallback);
  459. this.setResourceRequestHandlers();
  460. this.sendResourceListChanged();
  461. return registeredResource;
  462. }
  463. else {
  464. if (this._registeredResourceTemplates[name]) {
  465. throw new Error(`Resource template ${name} is already registered`);
  466. }
  467. const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, undefined, uriOrTemplate, metadata, readCallback);
  468. this.setResourceRequestHandlers();
  469. this.sendResourceListChanged();
  470. return registeredResourceTemplate;
  471. }
  472. }
  473. registerResource(name, uriOrTemplate, config, readCallback) {
  474. if (typeof uriOrTemplate === 'string') {
  475. if (this._registeredResources[uriOrTemplate]) {
  476. throw new Error(`Resource ${uriOrTemplate} is already registered`);
  477. }
  478. const registeredResource = this._createRegisteredResource(name, config.title, uriOrTemplate, config, readCallback);
  479. this.setResourceRequestHandlers();
  480. this.sendResourceListChanged();
  481. return registeredResource;
  482. }
  483. else {
  484. if (this._registeredResourceTemplates[name]) {
  485. throw new Error(`Resource template ${name} is already registered`);
  486. }
  487. const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config.title, uriOrTemplate, config, readCallback);
  488. this.setResourceRequestHandlers();
  489. this.sendResourceListChanged();
  490. return registeredResourceTemplate;
  491. }
  492. }
  493. _createRegisteredResource(name, title, uri, metadata, readCallback) {
  494. const registeredResource = {
  495. name,
  496. title,
  497. metadata,
  498. readCallback,
  499. enabled: true,
  500. disable: () => registeredResource.update({ enabled: false }),
  501. enable: () => registeredResource.update({ enabled: true }),
  502. remove: () => registeredResource.update({ uri: null }),
  503. update: updates => {
  504. if (typeof updates.uri !== 'undefined' && updates.uri !== uri) {
  505. delete this._registeredResources[uri];
  506. if (updates.uri)
  507. this._registeredResources[updates.uri] = registeredResource;
  508. }
  509. if (typeof updates.name !== 'undefined')
  510. registeredResource.name = updates.name;
  511. if (typeof updates.title !== 'undefined')
  512. registeredResource.title = updates.title;
  513. if (typeof updates.metadata !== 'undefined')
  514. registeredResource.metadata = updates.metadata;
  515. if (typeof updates.callback !== 'undefined')
  516. registeredResource.readCallback = updates.callback;
  517. if (typeof updates.enabled !== 'undefined')
  518. registeredResource.enabled = updates.enabled;
  519. this.sendResourceListChanged();
  520. }
  521. };
  522. this._registeredResources[uri] = registeredResource;
  523. return registeredResource;
  524. }
  525. _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
  526. const registeredResourceTemplate = {
  527. resourceTemplate: template,
  528. title,
  529. metadata,
  530. readCallback,
  531. enabled: true,
  532. disable: () => registeredResourceTemplate.update({ enabled: false }),
  533. enable: () => registeredResourceTemplate.update({ enabled: true }),
  534. remove: () => registeredResourceTemplate.update({ name: null }),
  535. update: updates => {
  536. if (typeof updates.name !== 'undefined' && updates.name !== name) {
  537. delete this._registeredResourceTemplates[name];
  538. if (updates.name)
  539. this._registeredResourceTemplates[updates.name] = registeredResourceTemplate;
  540. }
  541. if (typeof updates.title !== 'undefined')
  542. registeredResourceTemplate.title = updates.title;
  543. if (typeof updates.template !== 'undefined')
  544. registeredResourceTemplate.resourceTemplate = updates.template;
  545. if (typeof updates.metadata !== 'undefined')
  546. registeredResourceTemplate.metadata = updates.metadata;
  547. if (typeof updates.callback !== 'undefined')
  548. registeredResourceTemplate.readCallback = updates.callback;
  549. if (typeof updates.enabled !== 'undefined')
  550. registeredResourceTemplate.enabled = updates.enabled;
  551. this.sendResourceListChanged();
  552. }
  553. };
  554. this._registeredResourceTemplates[name] = registeredResourceTemplate;
  555. // If the resource template has any completion callbacks, enable completions capability
  556. const variableNames = template.uriTemplate.variableNames;
  557. const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v));
  558. if (hasCompleter) {
  559. this.setCompletionRequestHandler();
  560. }
  561. return registeredResourceTemplate;
  562. }
  563. _createRegisteredPrompt(name, title, description, argsSchema, callback) {
  564. const registeredPrompt = {
  565. title,
  566. description,
  567. argsSchema: argsSchema === undefined ? undefined : objectFromShape(argsSchema),
  568. callback,
  569. enabled: true,
  570. disable: () => registeredPrompt.update({ enabled: false }),
  571. enable: () => registeredPrompt.update({ enabled: true }),
  572. remove: () => registeredPrompt.update({ name: null }),
  573. update: updates => {
  574. if (typeof updates.name !== 'undefined' && updates.name !== name) {
  575. delete this._registeredPrompts[name];
  576. if (updates.name)
  577. this._registeredPrompts[updates.name] = registeredPrompt;
  578. }
  579. if (typeof updates.title !== 'undefined')
  580. registeredPrompt.title = updates.title;
  581. if (typeof updates.description !== 'undefined')
  582. registeredPrompt.description = updates.description;
  583. if (typeof updates.argsSchema !== 'undefined')
  584. registeredPrompt.argsSchema = objectFromShape(updates.argsSchema);
  585. if (typeof updates.callback !== 'undefined')
  586. registeredPrompt.callback = updates.callback;
  587. if (typeof updates.enabled !== 'undefined')
  588. registeredPrompt.enabled = updates.enabled;
  589. this.sendPromptListChanged();
  590. }
  591. };
  592. this._registeredPrompts[name] = registeredPrompt;
  593. // If any argument uses a Completable schema, enable completions capability
  594. if (argsSchema) {
  595. const hasCompletable = Object.values(argsSchema).some(field => {
  596. const inner = field instanceof ZodOptional ? field._def?.innerType : field;
  597. return isCompletable(inner);
  598. });
  599. if (hasCompletable) {
  600. this.setCompletionRequestHandler();
  601. }
  602. }
  603. return registeredPrompt;
  604. }
  605. _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, execution, _meta, handler) {
  606. // Validate tool name according to SEP specification
  607. validateAndWarnToolName(name);
  608. const registeredTool = {
  609. title,
  610. description,
  611. inputSchema: getZodSchemaObject(inputSchema),
  612. outputSchema: getZodSchemaObject(outputSchema),
  613. annotations,
  614. execution,
  615. _meta,
  616. handler: handler,
  617. enabled: true,
  618. disable: () => registeredTool.update({ enabled: false }),
  619. enable: () => registeredTool.update({ enabled: true }),
  620. remove: () => registeredTool.update({ name: null }),
  621. update: updates => {
  622. if (typeof updates.name !== 'undefined' && updates.name !== name) {
  623. if (typeof updates.name === 'string') {
  624. validateAndWarnToolName(updates.name);
  625. }
  626. delete this._registeredTools[name];
  627. if (updates.name)
  628. this._registeredTools[updates.name] = registeredTool;
  629. }
  630. if (typeof updates.title !== 'undefined')
  631. registeredTool.title = updates.title;
  632. if (typeof updates.description !== 'undefined')
  633. registeredTool.description = updates.description;
  634. if (typeof updates.paramsSchema !== 'undefined')
  635. registeredTool.inputSchema = objectFromShape(updates.paramsSchema);
  636. if (typeof updates.outputSchema !== 'undefined')
  637. registeredTool.outputSchema = objectFromShape(updates.outputSchema);
  638. if (typeof updates.callback !== 'undefined')
  639. registeredTool.handler = updates.callback;
  640. if (typeof updates.annotations !== 'undefined')
  641. registeredTool.annotations = updates.annotations;
  642. if (typeof updates._meta !== 'undefined')
  643. registeredTool._meta = updates._meta;
  644. if (typeof updates.enabled !== 'undefined')
  645. registeredTool.enabled = updates.enabled;
  646. this.sendToolListChanged();
  647. }
  648. };
  649. this._registeredTools[name] = registeredTool;
  650. this.setToolRequestHandlers();
  651. this.sendToolListChanged();
  652. return registeredTool;
  653. }
  654. /**
  655. * tool() implementation. Parses arguments passed to overrides defined above.
  656. */
  657. tool(name, ...rest) {
  658. if (this._registeredTools[name]) {
  659. throw new Error(`Tool ${name} is already registered`);
  660. }
  661. let description;
  662. let inputSchema;
  663. let outputSchema;
  664. let annotations;
  665. // Tool properties are passed as separate arguments, with omissions allowed.
  666. // Support for this style is frozen as of protocol version 2025-03-26. Future additions
  667. // to tool definition should *NOT* be added.
  668. if (typeof rest[0] === 'string') {
  669. description = rest.shift();
  670. }
  671. // Handle the different overload combinations
  672. if (rest.length > 1) {
  673. // We have at least one more arg before the callback
  674. const firstArg = rest[0];
  675. if (isZodRawShapeCompat(firstArg)) {
  676. // We have a params schema as the first arg
  677. inputSchema = rest.shift();
  678. // Check if the next arg is potentially annotations
  679. if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !isZodRawShapeCompat(rest[0])) {
  680. // Case: tool(name, paramsSchema, annotations, cb)
  681. // Or: tool(name, description, paramsSchema, annotations, cb)
  682. annotations = rest.shift();
  683. }
  684. }
  685. else if (typeof firstArg === 'object' && firstArg !== null) {
  686. // ToolAnnotations values are primitives. Nested objects indicate a misplaced schema
  687. if (Object.values(firstArg).some(v => typeof v === 'object' && v !== null)) {
  688. throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`);
  689. }
  690. annotations = rest.shift();
  691. }
  692. }
  693. const callback = rest[0];
  694. return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, { taskSupport: 'forbidden' }, undefined, callback);
  695. }
  696. /**
  697. * Registers a tool with a config object and callback.
  698. */
  699. registerTool(name, config, cb) {
  700. if (this._registeredTools[name]) {
  701. throw new Error(`Tool ${name} is already registered`);
  702. }
  703. const { title, description, inputSchema, outputSchema, annotations, _meta } = config;
  704. return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: 'forbidden' }, _meta, cb);
  705. }
  706. prompt(name, ...rest) {
  707. if (this._registeredPrompts[name]) {
  708. throw new Error(`Prompt ${name} is already registered`);
  709. }
  710. let description;
  711. if (typeof rest[0] === 'string') {
  712. description = rest.shift();
  713. }
  714. let argsSchema;
  715. if (rest.length > 1) {
  716. argsSchema = rest.shift();
  717. }
  718. const cb = rest[0];
  719. const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb);
  720. this.setPromptRequestHandlers();
  721. this.sendPromptListChanged();
  722. return registeredPrompt;
  723. }
  724. /**
  725. * Registers a prompt with a config object and callback.
  726. */
  727. registerPrompt(name, config, cb) {
  728. if (this._registeredPrompts[name]) {
  729. throw new Error(`Prompt ${name} is already registered`);
  730. }
  731. const { title, description, argsSchema } = config;
  732. const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb);
  733. this.setPromptRequestHandlers();
  734. this.sendPromptListChanged();
  735. return registeredPrompt;
  736. }
  737. /**
  738. * Checks if the server is connected to a transport.
  739. * @returns True if the server is connected
  740. */
  741. isConnected() {
  742. return this.server.transport !== undefined;
  743. }
  744. /**
  745. * Sends a logging message to the client, if connected.
  746. * Note: You only need to send the parameters object, not the entire JSON RPC message
  747. * @see LoggingMessageNotification
  748. * @param params
  749. * @param sessionId optional for stateless and backward compatibility
  750. */
  751. async sendLoggingMessage(params, sessionId) {
  752. return this.server.sendLoggingMessage(params, sessionId);
  753. }
  754. /**
  755. * Sends a resource list changed event to the client, if connected.
  756. */
  757. sendResourceListChanged() {
  758. if (this.isConnected()) {
  759. this.server.sendResourceListChanged();
  760. }
  761. }
  762. /**
  763. * Sends a tool list changed event to the client, if connected.
  764. */
  765. sendToolListChanged() {
  766. if (this.isConnected()) {
  767. this.server.sendToolListChanged();
  768. }
  769. }
  770. /**
  771. * Sends a prompt list changed event to the client, if connected.
  772. */
  773. sendPromptListChanged() {
  774. if (this.isConnected()) {
  775. this.server.sendPromptListChanged();
  776. }
  777. }
  778. }
  779. /**
  780. * A resource template combines a URI pattern with optional functionality to enumerate
  781. * all resources matching that pattern.
  782. */
  783. export class ResourceTemplate {
  784. constructor(uriTemplate, _callbacks) {
  785. this._callbacks = _callbacks;
  786. this._uriTemplate = typeof uriTemplate === 'string' ? new UriTemplate(uriTemplate) : uriTemplate;
  787. }
  788. /**
  789. * Gets the URI template pattern.
  790. */
  791. get uriTemplate() {
  792. return this._uriTemplate;
  793. }
  794. /**
  795. * Gets the list callback, if one was provided.
  796. */
  797. get listCallback() {
  798. return this._callbacks.list;
  799. }
  800. /**
  801. * Gets the callback for completing a specific URI template variable, if one was provided.
  802. */
  803. completeCallback(variable) {
  804. return this._callbacks.complete?.[variable];
  805. }
  806. }
  807. const EMPTY_OBJECT_JSON_SCHEMA = {
  808. type: 'object',
  809. properties: {}
  810. };
  811. /**
  812. * Checks if a value looks like a Zod schema by checking for parse/safeParse methods.
  813. */
  814. function isZodTypeLike(value) {
  815. return (value !== null &&
  816. typeof value === 'object' &&
  817. 'parse' in value &&
  818. typeof value.parse === 'function' &&
  819. 'safeParse' in value &&
  820. typeof value.safeParse === 'function');
  821. }
  822. /**
  823. * Checks if an object is a Zod schema instance (v3 or v4).
  824. *
  825. * Zod schemas have internal markers:
  826. * - v3: `_def` property
  827. * - v4: `_zod` property
  828. *
  829. * This includes transformed schemas like z.preprocess(), z.transform(), z.pipe().
  830. */
  831. function isZodSchemaInstance(obj) {
  832. return '_def' in obj || '_zod' in obj || isZodTypeLike(obj);
  833. }
  834. /**
  835. * Checks if an object is a "raw shape" - a plain object where values are Zod schemas.
  836. *
  837. * Raw shapes are used as shorthand: `{ name: z.string() }` instead of `z.object({ name: z.string() })`.
  838. *
  839. * IMPORTANT: This must NOT match actual Zod schema instances (like z.preprocess, z.pipe),
  840. * which have internal properties that could be mistaken for schema values.
  841. */
  842. function isZodRawShapeCompat(obj) {
  843. if (typeof obj !== 'object' || obj === null) {
  844. return false;
  845. }
  846. // If it's already a Zod schema instance, it's NOT a raw shape
  847. if (isZodSchemaInstance(obj)) {
  848. return false;
  849. }
  850. // Empty objects are valid raw shapes (tools with no parameters)
  851. if (Object.keys(obj).length === 0) {
  852. return true;
  853. }
  854. // A raw shape has at least one property that is a Zod schema
  855. return Object.values(obj).some(isZodTypeLike);
  856. }
  857. /**
  858. * Converts a provided Zod schema to a Zod object if it is a ZodRawShapeCompat,
  859. * otherwise returns the schema as is. Throws if the value is not a valid Zod schema.
  860. */
  861. function getZodSchemaObject(schema) {
  862. if (!schema) {
  863. return undefined;
  864. }
  865. if (isZodRawShapeCompat(schema)) {
  866. return objectFromShape(schema);
  867. }
  868. if (!isZodSchemaInstance(schema)) {
  869. throw new Error('inputSchema must be a Zod schema or raw shape, received an unrecognized object');
  870. }
  871. return schema;
  872. }
  873. function promptArgumentsFromSchema(schema) {
  874. const shape = getObjectShape(schema);
  875. if (!shape)
  876. return [];
  877. return Object.entries(shape).map(([name, field]) => {
  878. // Get description - works for both v3 and v4
  879. const description = getSchemaDescription(field);
  880. // Check if optional - works for both v3 and v4
  881. const isOptional = isSchemaOptional(field);
  882. return {
  883. name,
  884. description,
  885. required: !isOptional
  886. };
  887. });
  888. }
  889. function getMethodValue(schema) {
  890. const shape = getObjectShape(schema);
  891. const methodSchema = shape?.method;
  892. if (!methodSchema) {
  893. throw new Error('Schema is missing a method literal');
  894. }
  895. // Extract literal value - works for both v3 and v4
  896. const value = getLiteralValue(methodSchema);
  897. if (typeof value === 'string') {
  898. return value;
  899. }
  900. throw new Error('Schema method literal must be a string');
  901. }
  902. function createCompletionResult(suggestions) {
  903. return {
  904. completion: {
  905. values: suggestions.slice(0, 100),
  906. total: suggestions.length,
  907. hasMore: suggestions.length > 100
  908. }
  909. };
  910. }
  911. const EMPTY_COMPLETION_RESULT = {
  912. completion: {
  913. values: [],
  914. hasMore: false
  915. }
  916. };
  917. //# sourceMappingURL=mcp.js.map