index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.Server = void 0;
  4. const protocol_js_1 = require("../shared/protocol.js");
  5. const types_js_1 = require("../types.js");
  6. const ajv_provider_js_1 = require("../validation/ajv-provider.js");
  7. const zod_compat_js_1 = require("./zod-compat.js");
  8. const server_js_1 = require("../experimental/tasks/server.js");
  9. const helpers_js_1 = require("../experimental/tasks/helpers.js");
  10. /**
  11. * An MCP server on top of a pluggable transport.
  12. *
  13. * This server will automatically respond to the initialization flow as initiated from the client.
  14. *
  15. * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters:
  16. *
  17. * ```typescript
  18. * // Custom schemas
  19. * const CustomRequestSchema = RequestSchema.extend({...})
  20. * const CustomNotificationSchema = NotificationSchema.extend({...})
  21. * const CustomResultSchema = ResultSchema.extend({...})
  22. *
  23. * // Type aliases
  24. * type CustomRequest = z.infer<typeof CustomRequestSchema>
  25. * type CustomNotification = z.infer<typeof CustomNotificationSchema>
  26. * type CustomResult = z.infer<typeof CustomResultSchema>
  27. *
  28. * // Create typed server
  29. * const server = new Server<CustomRequest, CustomNotification, CustomResult>({
  30. * name: "CustomServer",
  31. * version: "1.0.0"
  32. * })
  33. * ```
  34. * @deprecated Use `McpServer` instead for the high-level API. Only use `Server` for advanced use cases.
  35. */
  36. class Server extends protocol_js_1.Protocol {
  37. /**
  38. * Initializes this server with the given name and version information.
  39. */
  40. constructor(_serverInfo, options) {
  41. super(options);
  42. this._serverInfo = _serverInfo;
  43. // Map log levels by session id
  44. this._loggingLevels = new Map();
  45. // Map LogLevelSchema to severity index
  46. this.LOG_LEVEL_SEVERITY = new Map(types_js_1.LoggingLevelSchema.options.map((level, index) => [level, index]));
  47. // Is a message with the given level ignored in the log level set for the given session id?
  48. this.isMessageIgnored = (level, sessionId) => {
  49. const currentLevel = this._loggingLevels.get(sessionId);
  50. return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false;
  51. };
  52. this._capabilities = options?.capabilities ?? {};
  53. this._instructions = options?.instructions;
  54. this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new ajv_provider_js_1.AjvJsonSchemaValidator();
  55. this.setRequestHandler(types_js_1.InitializeRequestSchema, request => this._oninitialize(request));
  56. this.setNotificationHandler(types_js_1.InitializedNotificationSchema, () => this.oninitialized?.());
  57. if (this._capabilities.logging) {
  58. this.setRequestHandler(types_js_1.SetLevelRequestSchema, async (request, extra) => {
  59. const transportSessionId = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] || undefined;
  60. const { level } = request.params;
  61. const parseResult = types_js_1.LoggingLevelSchema.safeParse(level);
  62. if (parseResult.success) {
  63. this._loggingLevels.set(transportSessionId, parseResult.data);
  64. }
  65. return {};
  66. });
  67. }
  68. }
  69. /**
  70. * Access experimental features.
  71. *
  72. * WARNING: These APIs are experimental and may change without notice.
  73. *
  74. * @experimental
  75. */
  76. get experimental() {
  77. if (!this._experimental) {
  78. this._experimental = {
  79. tasks: new server_js_1.ExperimentalServerTasks(this)
  80. };
  81. }
  82. return this._experimental;
  83. }
  84. /**
  85. * Registers new capabilities. This can only be called before connecting to a transport.
  86. *
  87. * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
  88. */
  89. registerCapabilities(capabilities) {
  90. if (this.transport) {
  91. throw new Error('Cannot register capabilities after connecting to transport');
  92. }
  93. this._capabilities = (0, protocol_js_1.mergeCapabilities)(this._capabilities, capabilities);
  94. }
  95. /**
  96. * Override request handler registration to enforce server-side validation for tools/call.
  97. */
  98. setRequestHandler(requestSchema, handler) {
  99. const shape = (0, zod_compat_js_1.getObjectShape)(requestSchema);
  100. const methodSchema = shape?.method;
  101. if (!methodSchema) {
  102. throw new Error('Schema is missing a method literal');
  103. }
  104. // Extract literal value using type-safe property access
  105. let methodValue;
  106. if ((0, zod_compat_js_1.isZ4Schema)(methodSchema)) {
  107. const v4Schema = methodSchema;
  108. const v4Def = v4Schema._zod?.def;
  109. methodValue = v4Def?.value ?? v4Schema.value;
  110. }
  111. else {
  112. const v3Schema = methodSchema;
  113. const legacyDef = v3Schema._def;
  114. methodValue = legacyDef?.value ?? v3Schema.value;
  115. }
  116. if (typeof methodValue !== 'string') {
  117. throw new Error('Schema method literal must be a string');
  118. }
  119. const method = methodValue;
  120. if (method === 'tools/call') {
  121. const wrappedHandler = async (request, extra) => {
  122. const validatedRequest = (0, zod_compat_js_1.safeParse)(types_js_1.CallToolRequestSchema, request);
  123. if (!validatedRequest.success) {
  124. const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
  125. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
  126. }
  127. const { params } = validatedRequest.data;
  128. const result = await Promise.resolve(handler(request, extra));
  129. // When task creation is requested, validate and return CreateTaskResult
  130. if (params.task) {
  131. const taskValidationResult = (0, zod_compat_js_1.safeParse)(types_js_1.CreateTaskResultSchema, result);
  132. if (!taskValidationResult.success) {
  133. const errorMessage = taskValidationResult.error instanceof Error
  134. ? taskValidationResult.error.message
  135. : String(taskValidationResult.error);
  136. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
  137. }
  138. return taskValidationResult.data;
  139. }
  140. // For non-task requests, validate against CallToolResultSchema
  141. const validationResult = (0, zod_compat_js_1.safeParse)(types_js_1.CallToolResultSchema, result);
  142. if (!validationResult.success) {
  143. const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
  144. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
  145. }
  146. return validationResult.data;
  147. };
  148. // Install the wrapped handler
  149. return super.setRequestHandler(requestSchema, wrappedHandler);
  150. }
  151. // Other handlers use default behavior
  152. return super.setRequestHandler(requestSchema, handler);
  153. }
  154. assertCapabilityForMethod(method) {
  155. switch (method) {
  156. case 'sampling/createMessage':
  157. if (!this._clientCapabilities?.sampling) {
  158. throw new Error(`Client does not support sampling (required for ${method})`);
  159. }
  160. break;
  161. case 'elicitation/create':
  162. if (!this._clientCapabilities?.elicitation) {
  163. throw new Error(`Client does not support elicitation (required for ${method})`);
  164. }
  165. break;
  166. case 'roots/list':
  167. if (!this._clientCapabilities?.roots) {
  168. throw new Error(`Client does not support listing roots (required for ${method})`);
  169. }
  170. break;
  171. case 'ping':
  172. // No specific capability required for ping
  173. break;
  174. }
  175. }
  176. assertNotificationCapability(method) {
  177. switch (method) {
  178. case 'notifications/message':
  179. if (!this._capabilities.logging) {
  180. throw new Error(`Server does not support logging (required for ${method})`);
  181. }
  182. break;
  183. case 'notifications/resources/updated':
  184. case 'notifications/resources/list_changed':
  185. if (!this._capabilities.resources) {
  186. throw new Error(`Server does not support notifying about resources (required for ${method})`);
  187. }
  188. break;
  189. case 'notifications/tools/list_changed':
  190. if (!this._capabilities.tools) {
  191. throw new Error(`Server does not support notifying of tool list changes (required for ${method})`);
  192. }
  193. break;
  194. case 'notifications/prompts/list_changed':
  195. if (!this._capabilities.prompts) {
  196. throw new Error(`Server does not support notifying of prompt list changes (required for ${method})`);
  197. }
  198. break;
  199. case 'notifications/elicitation/complete':
  200. if (!this._clientCapabilities?.elicitation?.url) {
  201. throw new Error(`Client does not support URL elicitation (required for ${method})`);
  202. }
  203. break;
  204. case 'notifications/cancelled':
  205. // Cancellation notifications are always allowed
  206. break;
  207. case 'notifications/progress':
  208. // Progress notifications are always allowed
  209. break;
  210. }
  211. }
  212. assertRequestHandlerCapability(method) {
  213. // Task handlers are registered in Protocol constructor before _capabilities is initialized
  214. // Skip capability check for task methods during initialization
  215. if (!this._capabilities) {
  216. return;
  217. }
  218. switch (method) {
  219. case 'completion/complete':
  220. if (!this._capabilities.completions) {
  221. throw new Error(`Server does not support completions (required for ${method})`);
  222. }
  223. break;
  224. case 'logging/setLevel':
  225. if (!this._capabilities.logging) {
  226. throw new Error(`Server does not support logging (required for ${method})`);
  227. }
  228. break;
  229. case 'prompts/get':
  230. case 'prompts/list':
  231. if (!this._capabilities.prompts) {
  232. throw new Error(`Server does not support prompts (required for ${method})`);
  233. }
  234. break;
  235. case 'resources/list':
  236. case 'resources/templates/list':
  237. case 'resources/read':
  238. if (!this._capabilities.resources) {
  239. throw new Error(`Server does not support resources (required for ${method})`);
  240. }
  241. break;
  242. case 'tools/call':
  243. case 'tools/list':
  244. if (!this._capabilities.tools) {
  245. throw new Error(`Server does not support tools (required for ${method})`);
  246. }
  247. break;
  248. case 'tasks/get':
  249. case 'tasks/list':
  250. case 'tasks/result':
  251. case 'tasks/cancel':
  252. if (!this._capabilities.tasks) {
  253. throw new Error(`Server does not support tasks capability (required for ${method})`);
  254. }
  255. break;
  256. case 'ping':
  257. case 'initialize':
  258. // No specific capability required for these methods
  259. break;
  260. }
  261. }
  262. assertTaskCapability(method) {
  263. (0, helpers_js_1.assertClientRequestTaskCapability)(this._clientCapabilities?.tasks?.requests, method, 'Client');
  264. }
  265. assertTaskHandlerCapability(method) {
  266. // Task handlers are registered in Protocol constructor before _capabilities is initialized
  267. // Skip capability check for task methods during initialization
  268. if (!this._capabilities) {
  269. return;
  270. }
  271. (0, helpers_js_1.assertToolsCallTaskCapability)(this._capabilities.tasks?.requests, method, 'Server');
  272. }
  273. async _oninitialize(request) {
  274. const requestedVersion = request.params.protocolVersion;
  275. this._clientCapabilities = request.params.capabilities;
  276. this._clientVersion = request.params.clientInfo;
  277. const protocolVersion = types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : types_js_1.LATEST_PROTOCOL_VERSION;
  278. return {
  279. protocolVersion,
  280. capabilities: this.getCapabilities(),
  281. serverInfo: this._serverInfo,
  282. ...(this._instructions && { instructions: this._instructions })
  283. };
  284. }
  285. /**
  286. * After initialization has completed, this will be populated with the client's reported capabilities.
  287. */
  288. getClientCapabilities() {
  289. return this._clientCapabilities;
  290. }
  291. /**
  292. * After initialization has completed, this will be populated with information about the client's name and version.
  293. */
  294. getClientVersion() {
  295. return this._clientVersion;
  296. }
  297. getCapabilities() {
  298. return this._capabilities;
  299. }
  300. async ping() {
  301. return this.request({ method: 'ping' }, types_js_1.EmptyResultSchema);
  302. }
  303. // Implementation
  304. async createMessage(params, options) {
  305. // Capability check - only required when tools/toolChoice are provided
  306. if (params.tools || params.toolChoice) {
  307. if (!this._clientCapabilities?.sampling?.tools) {
  308. throw new Error('Client does not support sampling tools capability.');
  309. }
  310. }
  311. // Message structure validation - always validate tool_use/tool_result pairs.
  312. // These may appear even without tools/toolChoice in the current request when
  313. // a previous sampling request returned tool_use and this is a follow-up with results.
  314. if (params.messages.length > 0) {
  315. const lastMessage = params.messages[params.messages.length - 1];
  316. const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
  317. const hasToolResults = lastContent.some(c => c.type === 'tool_result');
  318. const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
  319. const previousContent = previousMessage
  320. ? Array.isArray(previousMessage.content)
  321. ? previousMessage.content
  322. : [previousMessage.content]
  323. : [];
  324. const hasPreviousToolUse = previousContent.some(c => c.type === 'tool_use');
  325. if (hasToolResults) {
  326. if (lastContent.some(c => c.type !== 'tool_result')) {
  327. throw new Error('The last message must contain only tool_result content if any is present');
  328. }
  329. if (!hasPreviousToolUse) {
  330. throw new Error('tool_result blocks are not matching any tool_use from the previous message');
  331. }
  332. }
  333. if (hasPreviousToolUse) {
  334. const toolUseIds = new Set(previousContent.filter(c => c.type === 'tool_use').map(c => c.id));
  335. const toolResultIds = new Set(lastContent.filter(c => c.type === 'tool_result').map(c => c.toolUseId));
  336. if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every(id => toolResultIds.has(id))) {
  337. throw new Error('ids of tool_result blocks and tool_use blocks from previous message do not match');
  338. }
  339. }
  340. }
  341. // Use different schemas based on whether tools are provided
  342. if (params.tools) {
  343. return this.request({ method: 'sampling/createMessage', params }, types_js_1.CreateMessageResultWithToolsSchema, options);
  344. }
  345. return this.request({ method: 'sampling/createMessage', params }, types_js_1.CreateMessageResultSchema, options);
  346. }
  347. /**
  348. * Creates an elicitation request for the given parameters.
  349. * For backwards compatibility, `mode` may be omitted for form requests and will default to `'form'`.
  350. * @param params The parameters for the elicitation request.
  351. * @param options Optional request options.
  352. * @returns The result of the elicitation request.
  353. */
  354. async elicitInput(params, options) {
  355. const mode = (params.mode ?? 'form');
  356. switch (mode) {
  357. case 'url': {
  358. if (!this._clientCapabilities?.elicitation?.url) {
  359. throw new Error('Client does not support url elicitation.');
  360. }
  361. const urlParams = params;
  362. return this.request({ method: 'elicitation/create', params: urlParams }, types_js_1.ElicitResultSchema, options);
  363. }
  364. case 'form': {
  365. if (!this._clientCapabilities?.elicitation?.form) {
  366. throw new Error('Client does not support form elicitation.');
  367. }
  368. const formParams = params.mode === 'form' ? params : { ...params, mode: 'form' };
  369. const result = await this.request({ method: 'elicitation/create', params: formParams }, types_js_1.ElicitResultSchema, options);
  370. if (result.action === 'accept' && result.content && formParams.requestedSchema) {
  371. try {
  372. const validator = this._jsonSchemaValidator.getValidator(formParams.requestedSchema);
  373. const validationResult = validator(result.content);
  374. if (!validationResult.valid) {
  375. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${validationResult.errorMessage}`);
  376. }
  377. }
  378. catch (error) {
  379. if (error instanceof types_js_1.McpError) {
  380. throw error;
  381. }
  382. throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Error validating elicitation response: ${error instanceof Error ? error.message : String(error)}`);
  383. }
  384. }
  385. return result;
  386. }
  387. }
  388. }
  389. /**
  390. * Creates a reusable callback that, when invoked, will send a `notifications/elicitation/complete`
  391. * notification for the specified elicitation ID.
  392. *
  393. * @param elicitationId The ID of the elicitation to mark as complete.
  394. * @param options Optional notification options. Useful when the completion notification should be related to a prior request.
  395. * @returns A function that emits the completion notification when awaited.
  396. */
  397. createElicitationCompletionNotifier(elicitationId, options) {
  398. if (!this._clientCapabilities?.elicitation?.url) {
  399. throw new Error('Client does not support URL elicitation (required for notifications/elicitation/complete)');
  400. }
  401. return () => this.notification({
  402. method: 'notifications/elicitation/complete',
  403. params: {
  404. elicitationId
  405. }
  406. }, options);
  407. }
  408. async listRoots(params, options) {
  409. return this.request({ method: 'roots/list', params }, types_js_1.ListRootsResultSchema, options);
  410. }
  411. /**
  412. * Sends a logging message to the client, if connected.
  413. * Note: You only need to send the parameters object, not the entire JSON RPC message
  414. * @see LoggingMessageNotification
  415. * @param params
  416. * @param sessionId optional for stateless and backward compatibility
  417. */
  418. async sendLoggingMessage(params, sessionId) {
  419. if (this._capabilities.logging) {
  420. if (!this.isMessageIgnored(params.level, sessionId)) {
  421. return this.notification({ method: 'notifications/message', params });
  422. }
  423. }
  424. }
  425. async sendResourceUpdated(params) {
  426. return this.notification({
  427. method: 'notifications/resources/updated',
  428. params
  429. });
  430. }
  431. async sendResourceListChanged() {
  432. return this.notification({
  433. method: 'notifications/resources/list_changed'
  434. });
  435. }
  436. async sendToolListChanged() {
  437. return this.notification({ method: 'notifications/tools/list_changed' });
  438. }
  439. async sendPromptListChanged() {
  440. return this.notification({ method: 'notifications/prompts/list_changed' });
  441. }
  442. }
  443. exports.Server = Server;
  444. //# sourceMappingURL=index.js.map