index.js 21 KB

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