tmcp 1.0.2 → 1.2.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/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "tmcp",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "The main tmcp library",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "keywords": [],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/paoloricciuti/tmcp.git",
11
+ "directory": "packages/tmcp"
12
+ },
8
13
  "author": "Paolo Ricciuti",
9
14
  "license": "MIT",
10
15
  "files": [
package/src/index.d.ts CHANGED
@@ -7,6 +7,13 @@ export class McpServer<StandardSchema extends StandardSchemaV1> {
7
7
  * @param {ServerOptions<StandardSchema>} options
8
8
  */
9
9
  constructor(server_info: ServerInfo, options: ServerOptions<StandardSchema>);
10
+ /**
11
+ * @template {keyof McpEvents} TEvent
12
+ * @param {TEvent} event
13
+ * @param {McpEvents[TEvent]} callback
14
+ * @param {AddEventListenerOptions} [options]
15
+ */
16
+ on<TEvent extends keyof McpEvents>(event: TEvent, callback: McpEvents[TEvent], options?: AddEventListenerOptions): void;
10
17
  /**
11
18
  * @template {StandardSchema | undefined} [TSchema=undefined]
12
19
  * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never }} options
@@ -19,44 +26,55 @@ export class McpServer<StandardSchema extends StandardSchemaV1> {
19
26
  }, execute: TSchema extends undefined ? (() => Promise<unknown> | unknown) : ((input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<unknown> | unknown)): void;
20
27
  /**
21
28
  * @template {StandardSchema | undefined} [TSchema=undefined]
22
- * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: Completion }} options
29
+ * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: TSchema extends undefined ? never : Partial<Record<keyof (StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>), Completion>> }} options
23
30
  * @param {TSchema extends undefined ? (()=>Promise<unknown> | unknown) : (input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<unknown> | unknown} execute
24
31
  */
25
32
  prompt<TSchema extends StandardSchema | undefined = undefined>({ name, description, schema, complete }: {
26
33
  name: string;
27
34
  description: string;
28
35
  schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never;
29
- complete?: Completion;
36
+ complete?: TSchema extends undefined ? never : Partial<Record<keyof StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>, Completion>>;
30
37
  }, execute: TSchema extends undefined ? (() => Promise<unknown> | unknown) : (input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<unknown> | unknown): void;
31
38
  /**
32
39
  * @param {{ name: string; description: string; uri: string }} options
33
- * @param {(uri: string, params?: unknown) => Promise<unknown> | unknown} execute
40
+ * @param {(uri: string) => Promise<unknown> | unknown} execute
34
41
  */
35
42
  resource({ name, description, uri }: {
36
43
  name: string;
37
44
  description: string;
38
45
  uri: string;
39
- }, execute: (uri: string, params?: unknown) => Promise<unknown> | unknown): void;
46
+ }, execute: (uri: string) => Promise<unknown> | unknown): void;
40
47
  /**
41
- * @param {{ name: string; description: string; uri: string; complete?: Completion }} options
42
- * @param {(uri: string, params?: unknown) => Promise<unknown> | unknown} execute
48
+ * @template {string} TUri
49
+ * @template {ExtractURITemplateVariables<TUri>} TVariables
50
+ * @param {{ name: string; description: string; uri: TUri; complete?: TVariables extends never ? never : Partial<Record<TVariables, Completion>> }} options
51
+ * @param {(uri: string, params: Record<TVariables, string | string[]>) => Promise<unknown> | unknown} execute
43
52
  */
44
- template({ name, description, uri, complete }: {
53
+ template<TUri extends string, TVariables extends ExtractURITemplateVariables<TUri>>({ name, description, uri, complete }: {
45
54
  name: string;
46
55
  description: string;
47
- uri: string;
48
- complete?: Completion;
49
- }, execute: (uri: string, params?: unknown) => Promise<unknown> | unknown): void;
56
+ uri: TUri;
57
+ complete?: TVariables extends never ? never : Partial<Record<TVariables, Completion>>;
58
+ }, execute: (uri: string, params: Record<TVariables, string | string[]>) => Promise<unknown> | unknown): void;
50
59
  /**
51
60
  * @param {JSONRPCRequest} request
52
61
  * @returns {ReturnType<JSONRPCServer['receive']>}
53
62
  */
54
63
  receive(request: JSONRPCRequest): ReturnType<JSONRPCServer["receive"]>;
64
+ /**
65
+ * Send a notification for subscriptions
66
+ * @param {SubscriptionsKeys} what
67
+ * @param {string} id
68
+ */
69
+ changed(what: SubscriptionsKeys, id: string): void;
55
70
  #private;
56
71
  }
57
72
  import type { StandardSchemaV1 } from "@standard-schema/spec";
73
+ import type { McpEvents } from "./internal/internal.js";
58
74
  import type { Completion } from "./internal/internal.js";
75
+ import type { ExtractURITemplateVariables } from "./internal/uri-template.js";
59
76
  import type { JSONRPCRequest } from "json-rpc-2.0";
60
77
  import { JSONRPCServer } from 'json-rpc-2.0';
78
+ import type { SubscriptionsKeys } from "./internal/internal.js";
61
79
  import type { ServerInfo } from "./internal/internal.js";
62
80
  import type { ServerOptions } from "./internal/internal.js";
package/src/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @import { StandardSchemaV1 } from "@standard-schema/spec";
3
3
  * @import { JSONRPCRequest, JSONRPCParams } from "json-rpc-2.0";
4
- * @import { Tool, Completion, Prompt, Resource, ServerOptions, ServerInfo } from "./internal/internal.js";
4
+ * @import { ExtractURITemplateVariables } from "./internal/uri-template.js";
5
+ * @import { Tool, Completion, Prompt, Resource, ServerOptions, ServerInfo, SubscriptionsKeys, McpEvents } from "./internal/internal.js";
5
6
  */
6
7
  import { JSONRPCServer, JSONRPCClient } from 'json-rpc-2.0';
7
8
  import { UriTemplateMatcher } from 'uri-template-matcher';
@@ -30,23 +31,31 @@ export class McpServer {
30
31
  #resources = new Map();
31
32
  #templates = new UriTemplateMatcher();
32
33
  /**
33
- * @type {{ [ref: string]: Map<string, Completion> }}
34
+ * @type {{ [ref: string]: Map<string, Partial<Record<string, Completion>>> }}
34
35
  */
35
36
  #completions = {
36
37
  'ref/prompt': new Map(),
37
38
  'ref/resource': new Map(),
38
39
  };
40
+
41
+ #event_target = new EventTarget();
42
+
43
+ /**
44
+ * @type {Record<SubscriptionsKeys, Set<string>>}
45
+ */
46
+ #subscriptions = {
47
+ /**
48
+ * @type {Set<string>}
49
+ */
50
+ resource: new Set(),
51
+ };
52
+
39
53
  /**
40
54
  * @param {ServerInfo} server_info
41
55
  * @param {ServerOptions<StandardSchema>} options
42
56
  */
43
57
  constructor(server_info, options) {
44
58
  this.#options = options;
45
- if (options.send) {
46
- this.#client = new JSONRPCClient((payload) => {
47
- options.send?.(payload);
48
- });
49
- }
50
59
  this.#server.addMethod('initialize', ({ protocolVersion }) => {
51
60
  return {
52
61
  protocolVersion,
@@ -66,6 +75,30 @@ export class McpServer {
66
75
  this.#init_completion();
67
76
  }
68
77
 
78
+ /**
79
+ * @template {keyof McpEvents} TEvent
80
+ * @param {TEvent} event
81
+ * @param {McpEvents[TEvent]} callback
82
+ * @param {AddEventListenerOptions} [options]
83
+ */
84
+ on(event, callback, options) {
85
+ if (event === 'send' && !this.#client) {
86
+ this.#client = new JSONRPCClient((payload) => {
87
+ this.#event_target.dispatchEvent(
88
+ new CustomEvent('send', { detail: payload }),
89
+ );
90
+ });
91
+ }
92
+
93
+ this.#event_target.addEventListener(
94
+ event,
95
+ (e) => {
96
+ callback(/** @type {CustomEvent} */ (e).detail);
97
+ },
98
+ options,
99
+ );
100
+ }
101
+
69
102
  /**
70
103
  * @param {string} method
71
104
  * @param {JSONRPCParams} [params]
@@ -190,6 +223,14 @@ export class McpServer {
190
223
  */
191
224
  #init_resources() {
192
225
  if (!this.#options.capabilities?.resources) return;
226
+
227
+ if (this.#options.capabilities?.resources?.subscribe) {
228
+ this.#server.addMethod('resources/subscribe', async ({ uri }) => {
229
+ this.#subscriptions.resource.add(uri);
230
+ return {};
231
+ });
232
+ }
233
+
193
234
  this.#server.addMethod('resources/list', async () => {
194
235
  return {
195
236
  resources: [...this.#resources].reduce(
@@ -237,7 +278,12 @@ export class McpServer {
237
278
  throw new Error(`Resource ${uri} not found`);
238
279
  }
239
280
  }
240
- return resource.execute(uri, params);
281
+ if (resource.template) {
282
+ if (!params)
283
+ throw new Error('Missing parameters for template resource');
284
+ return resource.execute(uri, params);
285
+ }
286
+ return resource.execute(uri);
241
287
  });
242
288
  }
243
289
  /**
@@ -251,9 +297,11 @@ export class McpServer {
251
297
  if (!completions) return null;
252
298
  const complete = completions.get(ref.uri ?? ref.name);
253
299
  if (!complete) return null;
300
+ const actual_complete = complete[argument.name];
301
+ if (!actual_complete) return null;
254
302
  return {
255
303
  completion: {
256
- values: complete(argument, context),
304
+ values: actual_complete(argument.value, context),
257
305
  hasMore: false,
258
306
  },
259
307
  };
@@ -277,7 +325,7 @@ export class McpServer {
277
325
  }
278
326
  /**
279
327
  * @template {StandardSchema | undefined} [TSchema=undefined]
280
- * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: Completion }} options
328
+ * @param {{ name: string; description: string; schema?: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema> extends Record<string, unknown> ? TSchema : never; complete?: TSchema extends undefined ? never : Partial<Record<keyof (StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>), Completion>> }} options
281
329
  * @param {TSchema extends undefined ? (()=>Promise<unknown> | unknown) : (input: StandardSchemaV1.InferInput<TSchema extends undefined ? never : TSchema>) => Promise<unknown> | unknown} execute
282
330
  */
283
331
  prompt({ name, description, schema, complete }, execute) {
@@ -294,43 +342,48 @@ export class McpServer {
294
342
  });
295
343
  }
296
344
  /**
297
- * @param {string} name
298
- * @param {string} description
299
- * @param {string} uri
300
- * @param {(uri: string, params?: unknown) => Promise<unknown> | unknown} execute
301
- * @param {Completion} [complete]
302
- * @param {boolean} [template=false]
345
+ * @type {(resource: Resource & {complete?: Partial<Record<string,Completion>>, uri: string})=> void}
303
346
  */
304
- #resource(name, description, uri, execute, complete, template = false) {
347
+ #resource({ uri, complete, ...resource }) {
305
348
  if (complete) {
306
349
  this.#completions['ref/resource'].set(uri, complete);
307
350
  }
308
- if (template) {
351
+ if (resource.template) {
309
352
  this.#templates.add(uri);
310
353
  }
311
354
  if (this.#options.capabilities?.resources?.listChanged) {
312
355
  this.#notify('notifications/resources/list_changed', {});
313
356
  }
314
- this.#resources.set(uri, {
315
- description,
316
- name,
317
- execute,
318
- template,
319
- });
357
+ this.#resources.set(uri, resource);
320
358
  }
321
359
  /**
322
360
  * @param {{ name: string; description: string; uri: string }} options
323
- * @param {(uri: string, params?: unknown) => Promise<unknown> | unknown} execute
361
+ * @param {(uri: string) => Promise<unknown> | unknown} execute
324
362
  */
325
363
  resource({ name, description, uri }, execute) {
326
- this.#resource(name, description, uri, execute, undefined, false);
364
+ this.#resource({
365
+ name,
366
+ description,
367
+ uri,
368
+ execute,
369
+ template: false,
370
+ });
327
371
  }
328
372
  /**
329
- * @param {{ name: string; description: string; uri: string; complete?: Completion }} options
330
- * @param {(uri: string, params?: unknown) => Promise<unknown> | unknown} execute
373
+ * @template {string} TUri
374
+ * @template {ExtractURITemplateVariables<TUri>} TVariables
375
+ * @param {{ name: string; description: string; uri: TUri; complete?: TVariables extends never ? never : Partial<Record<TVariables, Completion>> }} options
376
+ * @param {(uri: string, params: Record<TVariables, string | string[]>) => Promise<unknown> | unknown} execute
331
377
  */
332
378
  template({ name, description, uri, complete }, execute) {
333
- this.#resource(name, description, uri, execute, complete, true);
379
+ this.#resource({
380
+ name,
381
+ description,
382
+ uri,
383
+ execute,
384
+ complete,
385
+ template: true,
386
+ });
334
387
  }
335
388
  /**
336
389
  * @param {JSONRPCRequest} request
@@ -339,4 +392,20 @@ export class McpServer {
339
392
  receive(request) {
340
393
  return this.#server.receive(request);
341
394
  }
395
+
396
+ /**
397
+ * Send a notification for subscriptions
398
+ * @param {SubscriptionsKeys} what
399
+ * @param {string} id
400
+ */
401
+ changed(what, id) {
402
+ if (this.#subscriptions[what].has(id)) {
403
+ const resource = this.#resources.get(id);
404
+ if (!resource) return;
405
+ this.#notify(`notifications/resources/updated`, {
406
+ uri: id,
407
+ title: resource.name,
408
+ });
409
+ }
410
+ }
342
411
  }
@@ -11,7 +11,7 @@ export type Tool<TSchema extends StandardSchemaV1 = StandardSchemaV1<any>> = {
11
11
  };
12
12
 
13
13
  export type Completion = (
14
- argument: { name: string; value: string },
14
+ query: string,
15
15
  context: { arguments: Record<string, string> },
16
16
  ) => string[];
17
17
 
@@ -23,19 +23,28 @@ export type Prompt<TSchema extends StandardSchemaV1 = StandardSchemaV1<any>> = {
23
23
  ) => Promise<unknown> | unknown;
24
24
  };
25
25
 
26
- export type Resource = {
27
- description: string;
28
- name: string;
29
- template: boolean;
30
- execute: (uri: string, params?: unknown) => Promise<unknown> | unknown;
31
- };
26
+ export type Resource =
27
+ | {
28
+ description: string;
29
+ name: string;
30
+ template: true;
31
+ execute: (
32
+ uri: string,
33
+ params: Record<string, string | string[]>,
34
+ ) => Promise<unknown> | unknown;
35
+ }
36
+ | {
37
+ description: string;
38
+ name: string;
39
+ template: false;
40
+ execute: (uri: string) => Promise<unknown> | unknown;
41
+ };
32
42
 
33
43
  export type ServerOptions<TSchema extends StandardSchemaV1> = {
34
- send?: (payload: JSONRPCRequest) => void;
35
44
  capabilities?: {
36
45
  tools?: { listChanged?: boolean };
37
46
  resources?: { listChanged?: boolean; subscribe?: boolean };
38
- prompts?: { listChanged?: boolean; subscribe?: boolean };
47
+ prompts?: { listChanged?: boolean };
39
48
  };
40
49
  instructions?: string;
41
50
  adapter: JsonSchemaAdapter<TSchema>;
@@ -46,3 +55,9 @@ export type ServerInfo = {
46
55
  version: string;
47
56
  description: string;
48
57
  };
58
+
59
+ export type SubscriptionsKeys = 'resource';
60
+
61
+ export type McpEvents = {
62
+ send: (message: JSONRPCRequest) => void;
63
+ };
@@ -0,0 +1,45 @@
1
+ // Helper type to remove whitespace
2
+ type Trim<S extends string> = S extends ` ${infer R}`
3
+ ? Trim<R>
4
+ : S extends `${infer L} `
5
+ ? Trim<L>
6
+ : S;
7
+
8
+ // Helper type to extract variable name, removing modifiers
9
+ type ExtractVarName<S extends string> = S extends `${infer Name}:${string}`
10
+ ? Trim<Name> // Remove prefix modifier
11
+ : S extends `${infer Name}*`
12
+ ? Trim<Name> // Remove explode modifier
13
+ : Trim<S>;
14
+
15
+ // Helper type to split comma-separated variables
16
+ type SplitVariables<S extends string> = S extends `${infer First},${infer Rest}`
17
+ ? ExtractVarName<First> | SplitVariables<Rest>
18
+ : ExtractVarName<S>;
19
+
20
+ // Helper type to extract content from braces and handle operators
21
+ type ExtractFromExpression<S extends string> = S extends `+${infer Vars}`
22
+ ? SplitVariables<Vars> // Reserved {+var}
23
+ : S extends `#${infer Vars}`
24
+ ? SplitVariables<Vars> // Fragment {#var}
25
+ : S extends `.${infer Vars}`
26
+ ? SplitVariables<Vars> // Label {.var}
27
+ : S extends `/${infer Vars}`
28
+ ? SplitVariables<Vars> // Path {/var}
29
+ : S extends `;${infer Vars}`
30
+ ? SplitVariables<Vars> // Path-style {;var}
31
+ : S extends `?${infer Vars}`
32
+ ? SplitVariables<Vars> // Query {?var}
33
+ : S extends `&${infer Vars}`
34
+ ? SplitVariables<Vars> // Query continuation {&var}
35
+ : SplitVariables<S>; // Simple {var}
36
+
37
+ // Main recursive type to extract all variables from URI template
38
+ type ExtractVariablesFromTemplate<S extends string> =
39
+ S extends `${string}{${infer Expression}}${infer Rest}`
40
+ ? ExtractFromExpression<Expression> | ExtractVariablesFromTemplate<Rest>
41
+ : never;
42
+
43
+ // Main exported type
44
+ export type ExtractURITemplateVariables<T extends string> =
45
+ ExtractVariablesFromTemplate<T>;