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 +6 -1
- package/src/index.d.ts +28 -10
- package/src/index.js +98 -29
- package/src/internal/internal.d.ts +24 -9
- package/src/internal/uri-template.d.ts +45 -0
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmcp",
|
|
3
|
-
"version": "1.0
|
|
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
|
|
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
|
|
46
|
+
}, execute: (uri: string) => Promise<unknown> | unknown): void;
|
|
40
47
|
/**
|
|
41
|
-
* @
|
|
42
|
-
* @
|
|
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:
|
|
48
|
-
complete?: Completion
|
|
49
|
-
}, execute: (uri: string, params
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
* @
|
|
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(
|
|
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
|
|
361
|
+
* @param {(uri: string) => Promise<unknown> | unknown} execute
|
|
324
362
|
*/
|
|
325
363
|
resource({ name, description, uri }, execute) {
|
|
326
|
-
this.#resource(
|
|
364
|
+
this.#resource({
|
|
365
|
+
name,
|
|
366
|
+
description,
|
|
367
|
+
uri,
|
|
368
|
+
execute,
|
|
369
|
+
template: false,
|
|
370
|
+
});
|
|
327
371
|
}
|
|
328
372
|
/**
|
|
329
|
-
* @
|
|
330
|
-
* @
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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>;
|