tmcp 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,21 +1,61 @@
1
1
  /**
2
2
  * @import { StandardSchemaV1 } from "@standard-schema/spec";
3
+ * @import SqidsType from "sqids";
3
4
  * @import { JSONRPCRequest, JSONRPCParams } from "json-rpc-2.0";
4
5
  * @import { ExtractURITemplateVariables } from "./internal/uri-template.js";
5
- * @import { CallToolResult, ReadResourceResult, GetPromptResult, ClientCapabilities as ClientCapabilitiesType } from "./validation/index.js";
6
- * @import { Tool, Completion, Prompt, Resource, ServerOptions, ServerInfo, SubscriptionsKeys, McpEvents } from "./internal/internal.js";
6
+ * @import { CallToolResult, ReadResourceResult, GetPromptResult, ClientCapabilities as ClientCapabilitiesType, JSONRPCRequest as JSONRPCRequestType, JSONRPCResponse, CreateMessageRequestParams, CreateMessageResult, Resource, LoggingLevel } from "./validation/index.js";
7
+ * @import { Tool, Completion, Prompt, StoredResource, ServerOptions, ServerInfo, SubscriptionsKeys, McpEvents } from "./internal/internal.js";
7
8
  */
8
- import { JSONRPCServer, JSONRPCClient } from 'json-rpc-2.0';
9
+ import { JSONRPCClient, JSONRPCServer } from 'json-rpc-2.0';
10
+ import { AsyncLocalStorage } from 'node:async_hooks';
9
11
  import { UriTemplateMatcher } from 'uri-template-matcher';
12
+ import * as v from 'valibot';
10
13
  import {
11
14
  CallToolResultSchema,
12
- ReadResourceResultSchema,
13
- GetPromptResultSchema,
14
15
  CompleteResultSchema,
15
- InitializeRequestSchema,
16
+ CreateMessageRequestParamsSchema,
17
+ CreateMessageResultSchema,
18
+ GetPromptResultSchema,
19
+ InitializeRequestParamsSchema,
20
+ JSONRPCRequestSchema,
21
+ JSONRPCResponseSchema,
22
+ McpError,
23
+ ReadResourceResultSchema,
16
24
  } from './validation/index.js';
17
- import { AsyncLocalStorage } from 'node:async_hooks';
18
- import * as v from 'valibot';
25
+ import {
26
+ get_supported_versions,
27
+ negotiate_protocol_version,
28
+ } from './validation/version.js';
29
+ import { should_version_negotiation_fail } from './validation/version.js';
30
+
31
+ /**
32
+ * @type {SqidsType | undefined}
33
+ */
34
+ let Sqids;
35
+
36
+ async function get_sqids() {
37
+ if (!Sqids) {
38
+ Sqids = new (await import('sqids')).default();
39
+ }
40
+ return Sqids;
41
+ }
42
+
43
+ /**
44
+ * Encode a cursor for pagination
45
+ * @param {number} offset
46
+ */
47
+ async function encode_cursor(offset) {
48
+ return (await get_sqids()).encode([offset]);
49
+ }
50
+
51
+ /**
52
+ * Decode a cursor from pagination
53
+ * @param {string} cursor
54
+ */
55
+ async function decode_cursor(cursor) {
56
+ const [decoded] = (await get_sqids()).decode(cursor);
57
+ return decoded;
58
+ }
19
59
 
20
60
  /**
21
61
  * @typedef {ClientCapabilitiesType} ClientCapabilities
@@ -40,10 +80,14 @@ export class McpServer {
40
80
  */
41
81
  #prompts = new Map();
42
82
  /**
43
- * @type {Map<string, Resource>}
83
+ * @type {Map<string, StoredResource>}
44
84
  */
45
85
  #resources = new Map();
46
86
  #templates = new UriTemplateMatcher();
87
+ /**
88
+ * @type {Array<{uri: string, name?: string}>}
89
+ */
90
+ roots = [];
47
91
  /**
48
92
  * @type {{ [ref: string]: Map<string, Partial<Record<string, Completion>>> }}
49
93
  */
@@ -74,6 +118,16 @@ export class McpServer {
74
118
  */
75
119
  #client_capabilities_map = new Map();
76
120
 
121
+ /**
122
+ * @type {Map<string|undefined, string>}
123
+ */
124
+ #negotiated_protocol_versions = new Map();
125
+
126
+ /**
127
+ * @type {Map<string|undefined, LoggingLevel>}
128
+ */
129
+ #session_log_levels = new Map();
130
+
77
131
  /**
78
132
  * @param {ServerInfo} server_info
79
133
  * @param {ServerOptions<StandardSchema>} options
@@ -81,24 +135,90 @@ export class McpServer {
81
135
  constructor(server_info, options) {
82
136
  this.#options = options;
83
137
  this.#server.addMethod('initialize', (initialize_request) => {
84
- const validated_initialize = v.parse(
85
- InitializeRequestSchema,
86
- initialize_request,
87
- );
88
- this.#client_capabilities_map.set(
89
- this.#session_id,
90
- validated_initialize.capabilities,
91
- );
92
- this.#event_target.dispatchEvent(
93
- new CustomEvent('initialize', {
94
- detail: validated_initialize,
95
- }),
96
- );
97
- return {
98
- protocolVersion: validated_initialize.protocolVersion,
99
- ...options,
100
- serverInfo: server_info,
101
- };
138
+ try {
139
+ // Validate basic request format
140
+ const validated_initialize = v.parse(
141
+ InitializeRequestParamsSchema,
142
+ initialize_request,
143
+ );
144
+
145
+ const session_id = this.#session_id;
146
+
147
+ // Validate protocol version format
148
+ if (
149
+ should_version_negotiation_fail(
150
+ validated_initialize.protocolVersion,
151
+ )
152
+ ) {
153
+ // Return JSON-RPC error for invalid protocol version format
154
+ const error = new McpError(
155
+ -32602,
156
+ 'Invalid protocol version format',
157
+ );
158
+ throw error;
159
+ }
160
+
161
+ // Negotiate protocol version
162
+ const negotiated_version = negotiate_protocol_version(
163
+ validated_initialize.protocolVersion,
164
+ );
165
+
166
+ // Store negotiated protocol version
167
+ this.#negotiated_protocol_versions.set(
168
+ session_id,
169
+ negotiated_version,
170
+ );
171
+
172
+ // Store client capabilities
173
+ this.#client_capabilities_map.set(
174
+ session_id,
175
+ validated_initialize.capabilities,
176
+ );
177
+
178
+ // Dispatch initialization event
179
+ this.#event_target.dispatchEvent(
180
+ new CustomEvent('initialize', {
181
+ detail: validated_initialize,
182
+ }),
183
+ );
184
+
185
+ // Trigger initial roots request if client supports it
186
+ if (validated_initialize.capabilities?.roots) {
187
+ this.#refresh_roots();
188
+ }
189
+
190
+ // Return server response with negotiated version and capabilities
191
+ return {
192
+ protocolVersion: negotiated_version,
193
+ ...options,
194
+ serverInfo: server_info,
195
+ };
196
+ } catch (error) {
197
+ // Enhanced error handling for initialization failures
198
+ if (error instanceof McpError) {
199
+ // Already has JSON-RPC error code, re-throw
200
+ throw error;
201
+ }
202
+
203
+ if (
204
+ /** @type {Error} */ (error).message?.includes(
205
+ 'Protocol version',
206
+ )
207
+ ) {
208
+ const rpc_error = new McpError(
209
+ -32602,
210
+ `Protocol version validation failed: ${/** @type {Error} */ (error).message}. Server supports: ${get_supported_versions().join(', ')}`,
211
+ );
212
+ throw rpc_error;
213
+ }
214
+
215
+ // General initialization error
216
+ const rpc_error = new McpError(
217
+ -32603,
218
+ `Initialization failed: ${/** @type {Error} */ (error).message}`,
219
+ );
220
+ throw rpc_error;
221
+ }
102
222
  });
103
223
  this.#server.addMethod('ping', () => {
104
224
  return {};
@@ -109,7 +229,9 @@ export class McpServer {
109
229
  this.#init_tools();
110
230
  this.#init_prompts();
111
231
  this.#init_resources();
232
+ this.#init_roots();
112
233
  this.#init_completion();
234
+ this.#init_logging();
113
235
  }
114
236
 
115
237
  get #session_id() {
@@ -175,7 +297,7 @@ export class McpServer {
175
297
  [...this.#tools].map(async ([name, tool]) => {
176
298
  return {
177
299
  name,
178
- title: tool.description,
300
+ title: tool.title || tool.description,
179
301
  description: tool.description,
180
302
  inputSchema: tool.schema
181
303
  ? await this.#options.adapter.toJsonSchema(
@@ -219,8 +341,8 @@ export class McpServer {
219
341
  */
220
342
  #init_prompts() {
221
343
  if (!this.#options.capabilities?.prompts) return;
222
- this.#server.addMethod('prompts/list', async () => {
223
- const available_prompts = await Promise.all(
344
+ this.#server.addMethod('prompts/list', async ({ cursor } = {}) => {
345
+ const all_prompts = await Promise.all(
224
346
  [...this.#prompts].map(async ([name, prompt]) => {
225
347
  const arguments_schema = prompt.schema
226
348
  ? await this.#options.adapter.toJsonSchema(
@@ -236,7 +358,7 @@ export class McpServer {
236
358
  const required = arguments_schema.required ?? [];
237
359
  return {
238
360
  name,
239
- title: prompt.description,
361
+ title: prompt.title || prompt.description,
240
362
  description: prompt.description,
241
363
  arguments: keys.map((key) => {
242
364
  const property = arguments_schema.properties?.[key];
@@ -253,8 +375,26 @@ export class McpServer {
253
375
  };
254
376
  }),
255
377
  );
378
+
379
+ const pagination_options = this.#options.pagination?.prompts;
380
+ if (!pagination_options || pagination_options.size == null) {
381
+ return { prompts: all_prompts };
382
+ }
383
+
384
+ const page_length = pagination_options.size;
385
+ const offset = cursor ? await decode_cursor(cursor) : 0;
386
+ const start_index = offset;
387
+ const end_index = start_index + page_length;
388
+
389
+ const prompts = all_prompts.slice(start_index, end_index);
390
+ const has_next = end_index < all_prompts.length;
391
+ const next_cursor = has_next
392
+ ? await encode_cursor(end_index)
393
+ : null;
394
+
256
395
  return {
257
- prompts: available_prompts,
396
+ prompts,
397
+ ...(next_cursor && { nextCursor: next_cursor }),
258
398
  };
259
399
  });
260
400
  this.#server.addMethod(
@@ -303,37 +443,61 @@ export class McpServer {
303
443
  });
304
444
  }
305
445
 
306
- this.#server.addMethod('resources/list', async () => {
446
+ this.#server.addMethod('resources/list', async ({ cursor } = {}) => {
447
+ const all_resources = [];
448
+
449
+ // Add static resources
450
+ for (const [uri, { description, name, title, ...resource }] of this
451
+ .#resources) {
452
+ if (!resource.template) {
453
+ all_resources.push({
454
+ name,
455
+ title: title || description,
456
+ description,
457
+ uri,
458
+ });
459
+ } else if (resource.list_resources) {
460
+ const template_resources = await resource.list_resources();
461
+ all_resources.push(...template_resources);
462
+ }
463
+ }
464
+
465
+ const pagination_options = this.#options.pagination?.resources;
466
+ if (!pagination_options || pagination_options.size == null) {
467
+ return { resources: all_resources };
468
+ }
469
+
470
+ const page_length = pagination_options.size;
471
+ const offset = cursor ? await decode_cursor(cursor) : 0;
472
+ const start_index = offset;
473
+ const end_index = start_index + page_length;
474
+
475
+ const resources = all_resources.slice(start_index, end_index);
476
+ const has_next = end_index < all_resources.length;
477
+ const next_cursor = has_next
478
+ ? await encode_cursor(end_index)
479
+ : null;
480
+
307
481
  return {
308
- resources: [...this.#resources].reduce(
309
- (arr, [uri, { description, name, template }]) => {
310
- if (!template) {
311
- arr.push({
312
- name,
313
- description,
314
- uri,
315
- });
316
- }
317
- return arr;
318
- },
319
- /** @type {Array<{name: string, description: string, uri: string}>} */ ([]),
320
- ),
482
+ resources,
483
+ ...(next_cursor && { nextCursor: next_cursor }),
321
484
  };
322
485
  });
323
486
  this.#server.addMethod('resources/templates/list', async () => {
324
487
  return {
325
488
  resourceTemplates: [...this.#resources].reduce(
326
- (arr, [uri, { description, name, template }]) => {
489
+ (arr, [uri, { description, name, title, template }]) => {
327
490
  if (template) {
328
491
  arr.push({
329
492
  name,
493
+ title: title || description,
330
494
  description,
331
495
  uriTemplate: uri,
332
496
  });
333
497
  }
334
498
  return arr;
335
499
  },
336
- /** @type {Array<{name: string, description: string, uriTemplate: string}>} */ ([]),
500
+ /** @type {Array<{name: string, title: string, description: string, uriTemplate: string}>} */ ([]),
337
501
  ),
338
502
  };
339
503
  });
@@ -364,6 +528,37 @@ export class McpServer {
364
528
  );
365
529
  });
366
530
  }
531
+ /**
532
+ *
533
+ */
534
+ #init_roots() {
535
+ this.#server.addMethod('notifications/roots/list_changed', () => {
536
+ this.#refresh_roots();
537
+ return null;
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Request roots list from client
543
+ */
544
+ async #refresh_roots() {
545
+ if (!this.#client_capabilities?.roots) return;
546
+
547
+ this.#lazyily_create_client();
548
+ try {
549
+ const response = await this.#client?.request(
550
+ 'roots/list',
551
+ undefined,
552
+ {
553
+ sessions: [this.#session_id],
554
+ },
555
+ );
556
+ this.roots = response?.roots || [];
557
+ } catch {
558
+ // Client doesn't support roots or request failed
559
+ this.roots = [];
560
+ }
561
+ }
367
562
  /**
368
563
  *
369
564
  */
@@ -384,27 +579,39 @@ export class McpServer {
384
579
  },
385
580
  );
386
581
  }
582
+ /**
583
+ *
584
+ */
585
+ #init_logging() {
586
+ if (!this.#options.capabilities?.logging) return;
587
+
588
+ this.#server.addMethod('logging/setLevel', ({ level }) => {
589
+ this.#session_log_levels.set(this.#session_id, level);
590
+ return {};
591
+ });
592
+ }
387
593
  /**
388
594
  * @template {StandardSchema | undefined} [TSchema=undefined]
389
- * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never }} options
595
+ * @param {{ name: string; description: string; title?: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never }} options
390
596
  * @param {TSchema extends undefined ? (()=>Promise<CallToolResult> | CallToolResult) : ((input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<CallToolResult> | CallToolResult)} execute
391
597
  */
392
- tool({ name, description, schema }, execute) {
598
+ tool({ name, description, title, schema }, execute) {
393
599
  if (this.#options.capabilities?.tools?.listChanged) {
394
600
  this.#notify('notifications/tools/list_changed', {});
395
601
  }
396
602
  this.#tools.set(name, {
397
603
  description,
604
+ title,
398
605
  schema,
399
606
  execute,
400
607
  });
401
608
  }
402
609
  /**
403
610
  * @template {StandardSchema | undefined} [TSchema=undefined]
404
- * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: NoInfer<TSchema extends undefined ? never : Partial<Record<keyof (StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>), Completion>>> }} options
611
+ * @param {{ name: string; description: string; title?: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: NoInfer<TSchema extends undefined ? never : Partial<Record<keyof (StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>), Completion>>> }} options
405
612
  * @param {TSchema extends undefined ? (()=>Promise<GetPromptResult> | GetPromptResult) : (input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<GetPromptResult> | GetPromptResult} execute
406
613
  */
407
- prompt({ name, description, schema, complete }, execute) {
614
+ prompt({ name, description, title, schema, complete }, execute) {
408
615
  if (complete) {
409
616
  this.#completions['ref/prompt'].set(name, complete);
410
617
  }
@@ -413,12 +620,13 @@ export class McpServer {
413
620
  }
414
621
  this.#prompts.set(name, {
415
622
  description,
623
+ title,
416
624
  schema,
417
625
  execute,
418
626
  });
419
627
  }
420
628
  /**
421
- * @type {(resource: Resource & {complete?: Partial<Record<string,Completion>>, uri: string})=> void}
629
+ * @type {(resource: StoredResource & {complete?: Partial<Record<string,Completion>>, uri: string})=> void}
422
630
  */
423
631
  #resource({ uri, complete, ...resource }) {
424
632
  if (complete) {
@@ -433,13 +641,14 @@ export class McpServer {
433
641
  this.#resources.set(uri, resource);
434
642
  }
435
643
  /**
436
- * @param {{ name: string; description: string; uri: string }} options
644
+ * @param {{ name: string; description: string; title?: string; uri: string }} options
437
645
  * @param {(uri: string) => Promise<ReadResourceResult> | ReadResourceResult} execute
438
646
  */
439
- resource({ name, description, uri }, execute) {
647
+ resource({ name, description, title, uri }, execute) {
440
648
  this.#resource({
441
649
  name,
442
650
  description,
651
+ title,
443
652
  uri,
444
653
  execute,
445
654
  template: false,
@@ -448,28 +657,58 @@ export class McpServer {
448
657
  /**
449
658
  * @template {string} TUri
450
659
  * @template {ExtractURITemplateVariables<TUri>} TVariables
451
- * @param {{ name: string; description: string; uri: TUri; complete?: NoInfer<TVariables extends never ? never : Partial<Record<TVariables, Completion>>> }} options
660
+ * @param {{ name: string; description: string; title?: string; uri: TUri; complete?: NoInfer<TVariables extends never ? never : Partial<Record<TVariables, Completion>>>; list?: () => Promise<Array<Resource>> | Array<Resource> }} options
452
661
  * @param {(uri: string, params: Record<TVariables, string | string[]>) => Promise<ReadResourceResult> | ReadResourceResult} execute
453
662
  */
454
- template({ name, description, uri, complete }, execute) {
663
+ template(
664
+ { name, description, title, uri, complete, list: list_resources },
665
+ execute,
666
+ ) {
455
667
  this.#resource({
456
668
  name,
457
669
  description,
670
+ title,
458
671
  uri,
459
672
  execute,
460
673
  complete,
674
+ list_resources,
461
675
  template: true,
462
676
  });
463
677
  }
464
678
  /**
465
- * @param {JSONRPCRequest} request
679
+ * @param {JSONRPCResponse | JSONRPCRequest} message
466
680
  * @param {string} [session_id]
467
- * @returns {ReturnType<JSONRPCServer['receive']>}
681
+ * @returns {ReturnType<JSONRPCServer['receive']> | ReturnType<JSONRPCClient['receive'] | undefined>}
468
682
  */
469
- receive(request, session_id) {
470
- return this.#session_storage.run(
471
- session_id,
472
- async () => await this.#server.receive(request),
683
+ receive(message, session_id) {
684
+ // Validate the message first
685
+ const validated_message = v.safeParse(JSONRPCRequestSchema, message);
686
+
687
+ // Check if it's a request or response
688
+ if (validated_message.success) {
689
+ // we want to set a default log level for each session
690
+ // so that if the user calls `log` it can send a notification
691
+ // to each client that didn't explicitly set a log level too
692
+ if (
693
+ !!this.#options.capabilities?.logging &&
694
+ !this.#session_log_levels.has(session_id)
695
+ ) {
696
+ this.#session_log_levels.set(
697
+ session_id,
698
+ this.#options.logging?.default ?? 'info',
699
+ );
700
+ }
701
+ return this.#session_storage.run(
702
+ session_id,
703
+ async () =>
704
+ await this.#server.receive(validated_message.output),
705
+ );
706
+ }
707
+ // It's a response - handle with client
708
+ const validated_response = v.parse(JSONRPCResponseSchema, message);
709
+ this.#lazyily_create_client();
710
+ return this.#session_storage.run(session_id, async () =>
711
+ this.#client?.receive(validated_response),
473
712
  );
474
713
  }
475
714
 
@@ -496,6 +735,13 @@ export class McpServer {
496
735
  }
497
736
  }
498
737
 
738
+ /**
739
+ * Refresh roots list from client
740
+ */
741
+ async refreshRoots() {
742
+ await this.#refresh_roots();
743
+ }
744
+
499
745
  /**
500
746
  * @template {StandardSchema} TSchema
501
747
  * @param {TSchema} schema
@@ -526,4 +772,96 @@ export class McpServer {
526
772
  }
527
773
  return validated_result.value;
528
774
  }
775
+
776
+ /**
777
+ * Request language model sampling from the client
778
+ * @param {CreateMessageRequestParams} request
779
+ * @returns {Promise<CreateMessageResult>}
780
+ */
781
+ async message(request) {
782
+ if (!this.#client_capabilities?.sampling)
783
+ throw new Error("Client doesn't support sampling");
784
+
785
+ this.#lazyily_create_client();
786
+
787
+ // Validate the request
788
+ const validated_request = v.parse(
789
+ CreateMessageRequestParamsSchema,
790
+ request,
791
+ );
792
+
793
+ // Make the request to the client
794
+ const response = await this.#client?.request(
795
+ 'sampling/createMessage',
796
+ validated_request,
797
+ {
798
+ sessions: [this.#session_id],
799
+ },
800
+ );
801
+
802
+ // Validate and return the response
803
+ return v.parse(CreateMessageResultSchema, response);
804
+ }
805
+
806
+ /**
807
+ * Log a message to the client if logging is enabled and the level is appropriate
808
+ * @param {LoggingLevel} level
809
+ * @param {unknown} data
810
+ * @param {string} [logger]
811
+ */
812
+ log(level, data, logger) {
813
+ if (!this.#options.capabilities?.logging) {
814
+ throw new Error(
815
+ "The server doesn't support logging, please enable it in capabilities",
816
+ );
817
+ }
818
+
819
+ const sessions = [];
820
+
821
+ for (let [session_id, session_log_level] of this.#session_log_levels) {
822
+ // Check if the current log level should be sent based on severity
823
+ if (this.#should_log(level, session_log_level)) {
824
+ sessions.push(session_id);
825
+ }
826
+ }
827
+
828
+ if (sessions.length === 0) return;
829
+
830
+ // Send the log notification to the client
831
+ this.#notify(
832
+ 'notifications/message',
833
+ {
834
+ level,
835
+ data,
836
+ logger,
837
+ },
838
+ {
839
+ sessions,
840
+ },
841
+ );
842
+ }
843
+
844
+ /**
845
+ * Check if a log message should be sent based on severity levels
846
+ * @param {LoggingLevel} message_level
847
+ * @param {LoggingLevel} session_level
848
+ * @returns {boolean}
849
+ */
850
+ #should_log(message_level, session_level) {
851
+ const levels = [
852
+ 'debug',
853
+ 'info',
854
+ 'notice',
855
+ 'warning',
856
+ 'error',
857
+ 'critical',
858
+ 'alert',
859
+ 'emergency',
860
+ ];
861
+ const message_severity = levels.indexOf(message_level);
862
+ const session_severity = levels.indexOf(session_level);
863
+
864
+ // Send if message severity is equal to or higher than session level
865
+ return message_severity >= session_severity;
866
+ }
529
867
  }