tmcp 1.19.4 → 1.20.0-next.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/README.md +71 -7
- package/package.json +5 -1
- package/src/index.js +1367 -52
- package/src/internal/internal.d.ts +225 -50
- package/src/method-policy.js +1 -0
- package/src/types/index.d.ts +742 -120
- package/src/types/index.d.ts.map +26 -2
- package/src/validation/index.js +412 -88
- package/src/validation/method-policy.js +78 -0
- package/src/validation/version.js +27 -2
package/src/index.js
CHANGED
|
@@ -4,36 +4,123 @@
|
|
|
4
4
|
* @import SqidsType from "sqids";
|
|
5
5
|
* @import { JSONRPCRequest, JSONRPCParams } from "json-rpc-2.0";
|
|
6
6
|
* @import { ExtractURITemplateVariables } from "./internal/uri-template.js";
|
|
7
|
-
* @import { CallToolResult as CallToolResultType, ReadResourceResult as ReadResourceResultType, GetPromptResult as GetPromptResultType, ServerInfo as ServerInfoType, ClientCapabilities as ClientCapabilitiesType, JSONRPCRequest as JSONRPCRequestType, JSONRPCResponse, CreateMessageRequestParams as CreateMessageRequestParamsType, CreateMessageResult as CreateMessageResultType, Resource as ResourceType, LoggingLevel as LoggingLevelType, ToolAnnotations, ClientInfo as ClientInfoType, ElicitResult as ElicitResultType, Icons as IconsType, JSONRPCMessage, InitializeResult as InitializeResultType, ListToolsResult as ListToolsResultType, ListPromptsResult as ListPromptsResultType, ListResourceTemplatesResult as ListResourceTemplatesResultType, ListResourcesResult as ListResourcesResultType, CompleteResult as CompleteResultType } from "./validation/index.js";
|
|
8
|
-
* @import { Tool, Completion, Prompt, StoredResource, ServerOptions, SubscriptionsKeys, ChangedArgs, McpEvents, AllSame, TemplateOptions } from "./internal/internal.js";
|
|
7
|
+
* @import { CallToolResult as CallToolResultType, ReadResourceResult as ReadResourceResultType, GetPromptResult as GetPromptResultType, ServerInfo as ServerInfoType, ClientCapabilities as ClientCapabilitiesType, JSONRPCRequest as JSONRPCRequestType, JSONRPCResponse, CreateMessageRequestParams as CreateMessageRequestParamsType, CreateMessageResult as CreateMessageResultType, Resource as ResourceType, LoggingLevel as LoggingLevelType, ToolAnnotations, ClientInfo as ClientInfoType, ElicitResult as ElicitResultType, Icons as IconsType, JSONRPCMessage, InitializeResult as InitializeResultType, ListToolsResult as ListToolsResultType, ListPromptsResult as ListPromptsResultType, ListResourceTemplatesResult as ListResourceTemplatesResultType, ListResourcesResult as ListResourcesResultType, CompleteResult as CompleteResultType, SubscriptionFilter as SubscriptionFilterType, SubscriptionsListenRequest as SubscriptionsListenRequestType, SubscriptionsListenResult as SubscriptionsListenResultType, SubscriptionsAcknowledgedNotification as SubscriptionsAcknowledgedNotificationType } from "./validation/index.js";
|
|
8
|
+
* @import { Tool, Completion, Prompt, StoredResource, ServerOptions, SubscriptionsKeys, ChangedArgs, McpEvents, AllSame, TemplateOptions, MrtrState, Subscription as SubscriptionType, SubscriptionCallbacks as SubscriptionCallbacksType, SubscriptionManager as SubscriptionManagerType, SubscriptionOrigin as SubscriptionOriginType } from "./internal/internal.js";
|
|
9
9
|
* @import { CreatedTool, ToolOptions, CreatedPrompt, PromptOptions, CreatedResource, CreatedTemplate, ResourceOptions } from "./internal/internal.js";
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createJSONRPCErrorResponse,
|
|
13
|
+
JSONRPCClient,
|
|
14
|
+
JSONRPCErrorException,
|
|
15
|
+
JSONRPCServer,
|
|
16
|
+
} from 'json-rpc-2.0';
|
|
12
17
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
18
|
import { UriTemplateMatcher } from 'uri-template-matcher';
|
|
14
19
|
import * as v from 'valibot';
|
|
15
20
|
import {
|
|
16
21
|
CallToolResultSchema,
|
|
22
|
+
CancelledNotificationSchema,
|
|
23
|
+
ClientCapabilitiesSchema,
|
|
17
24
|
CompleteResultSchema,
|
|
18
25
|
CreateMessageRequestParamsSchema,
|
|
19
26
|
CreateMessageResultSchema,
|
|
20
27
|
GetPromptResultSchema,
|
|
28
|
+
ImplementationSchema,
|
|
21
29
|
InitializeRequestParamsSchema,
|
|
22
30
|
JSONRPCNotificationSchema,
|
|
23
31
|
JSONRPCRequestSchema,
|
|
24
32
|
JSONRPCResponseSchema,
|
|
33
|
+
LoggingLevelSchema,
|
|
25
34
|
McpError,
|
|
26
35
|
ReadResourceResultSchema,
|
|
36
|
+
ElicitRequestSchema,
|
|
27
37
|
ElicitResultSchema,
|
|
28
38
|
JSONRPCErrorSchema,
|
|
39
|
+
InputResponsesSchema,
|
|
40
|
+
missing_required_client_capability_error,
|
|
41
|
+
SUBSCRIPTION_ID_META_KEY,
|
|
42
|
+
SubscriptionsListenRequestParamsSchema,
|
|
43
|
+
unsupported_protocol_version_error,
|
|
29
44
|
} from './validation/index.js';
|
|
30
45
|
import {
|
|
31
46
|
get_supported_versions,
|
|
32
47
|
negotiate_protocol_version,
|
|
48
|
+
KNOWN_PER_REQUEST_PROTOCOL_VERSIONS,
|
|
33
49
|
} from './validation/version.js';
|
|
34
50
|
import { should_version_negotiation_fail } from './validation/version.js';
|
|
51
|
+
import {
|
|
52
|
+
is_method_allowed,
|
|
53
|
+
CACHEABLE_METHODS,
|
|
54
|
+
MRTR_METHODS,
|
|
55
|
+
} from './validation/method-policy.js';
|
|
35
56
|
import { event } from './internal/utils.js';
|
|
36
57
|
|
|
58
|
+
export {
|
|
59
|
+
McpError,
|
|
60
|
+
HEADER_MISMATCH,
|
|
61
|
+
MISSING_REQUIRED_CLIENT_CAPABILITY,
|
|
62
|
+
UNSUPPORTED_PROTOCOL_VERSION,
|
|
63
|
+
} from './validation/index.js';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Maximum length of the `requestState` text accepted from or returned to a
|
|
67
|
+
* client. This prevents a client from making the server process an
|
|
68
|
+
* excessively large value.
|
|
69
|
+
*/
|
|
70
|
+
const MAX_ENCODED_REQUEST_STATE_LENGTH = 262144;
|
|
71
|
+
const RequestStateEnvelopeSchema = v.object({
|
|
72
|
+
version: v.literal(1),
|
|
73
|
+
inputResponses: InputResponsesSchema,
|
|
74
|
+
state: v.optional(v.unknown()),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Private error used to stop a handler when it needs input from the client.
|
|
79
|
+
* The server catches it and returns an `InputRequiredResult` instead of an
|
|
80
|
+
* error response. It is not exported, so application code cannot create a
|
|
81
|
+
* fake one.
|
|
82
|
+
*/
|
|
83
|
+
class InputRequiredSignal extends Error {
|
|
84
|
+
constructor() {
|
|
85
|
+
super(
|
|
86
|
+
'Input required: this request must end with an InputRequiredResult. If you see this message in a response, a catch block in a handler swallowed the internal input-required signal or the input call was not awaited — always await input calls and rethrow the signal (see isInputRequired()).',
|
|
87
|
+
);
|
|
88
|
+
this.name = 'InputRequiredSignal';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check whether an error means that tmcp is waiting for client input.
|
|
94
|
+
*
|
|
95
|
+
* When there is no open connection to the client, `elicitation()` and
|
|
96
|
+
* `message()` ask the client to retry the original request with the answer.
|
|
97
|
+
* They stop the current handler by throwing a private error. A broad `catch`
|
|
98
|
+
* in the handler must rethrow that error, or tmcp cannot ask for the retry.
|
|
99
|
+
* Use this helper to distinguish it from errors your handler should process:
|
|
100
|
+
*
|
|
101
|
+
* ```js
|
|
102
|
+
* try {
|
|
103
|
+
* const answer = await server.elicitation(msg, schema);
|
|
104
|
+
* } catch (error) {
|
|
105
|
+
* if (isInputRequired(error)) throw error;
|
|
106
|
+
* // handle the real error
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
* @param {unknown} error
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
export function isInputRequired(error) {
|
|
113
|
+
return error instanceof InputRequiredSignal;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return the per-request protocol versions supported by tmcp.
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
export function getPerRequestProtocolVersions() {
|
|
121
|
+
return [...KNOWN_PER_REQUEST_PROTOCOL_VERSIONS];
|
|
122
|
+
}
|
|
123
|
+
|
|
37
124
|
/**
|
|
38
125
|
* Information about a validated access token, provided to request handlers.
|
|
39
126
|
* @typedef {Object} AuthInfo
|
|
@@ -52,10 +139,29 @@ import { event } from './internal/utils.js';
|
|
|
52
139
|
* @typedef {Object} Context
|
|
53
140
|
* @property {string} [sessionId]
|
|
54
141
|
* @property {{ clientCapabilities?: ClientCapabilitiesType, clientInfo?: ClientInfoType, logLevel?: LoggingLevel }} [sessionInfo]
|
|
142
|
+
* @property {string} [protocolVersion] The exact per-request protocol version when the current request carries per-request protocol metadata; `undefined` for session-negotiated requests.
|
|
143
|
+
* @property {unknown} [requestState] Data saved by the handler before asking the client for input, then returned by the client on the retry. It is `undefined` when no data was saved or when the request uses an open session. The default JSON converter lets the client read and change this value. Do not store secrets in it or use it to make authorization decisions. Configure `requestStateCodec` if the server must detect changes.
|
|
144
|
+
* @property {AbortSignal} [signal] Aborted when the transport observes that the current request was cancelled.
|
|
55
145
|
* @property {AuthInfo} [auth]
|
|
56
146
|
* @property {TCustom} [custom]
|
|
57
147
|
*/
|
|
58
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Context accepted by `receive()`. Subscription routing fields are
|
|
151
|
+
* transport-only and are deliberately omitted from `server.ctx`.
|
|
152
|
+
* @template {Record<string, unknown> | undefined} [TCustom=undefined]
|
|
153
|
+
* @typedef {Context<TCustom> & { subscriptionOrigin?: SubscriptionOriginType, subscriptionManager?: SubscriptionManagerType }} ReceiveContext
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/** @typedef {SubscriptionFilterType} SubscriptionFilter */
|
|
157
|
+
/** @typedef {SubscriptionOriginType} SubscriptionOrigin */
|
|
158
|
+
/** @typedef {SubscriptionType} Subscription */
|
|
159
|
+
/** @typedef {SubscriptionCallbacksType} SubscriptionCallbacks */
|
|
160
|
+
/** @typedef {SubscriptionManagerType} SubscriptionManager */
|
|
161
|
+
/** @typedef {SubscriptionsListenRequestType} SubscriptionsListenRequest */
|
|
162
|
+
/** @typedef {SubscriptionsListenResultType} SubscriptionsListenResult */
|
|
163
|
+
/** @typedef {SubscriptionsAcknowledgedNotificationType} SubscriptionsAcknowledgedNotification */
|
|
164
|
+
|
|
59
165
|
/**
|
|
60
166
|
* @typedef {IconsType} Icons
|
|
61
167
|
*/
|
|
@@ -65,7 +171,7 @@ import { event } from './internal/utils.js';
|
|
|
65
171
|
*/
|
|
66
172
|
|
|
67
173
|
/**
|
|
68
|
-
* @template
|
|
174
|
+
* @template TStructuredContent
|
|
69
175
|
* @typedef {CallToolResultType<TStructuredContent>} CallToolResult
|
|
70
176
|
*/
|
|
71
177
|
|
|
@@ -177,7 +283,13 @@ async function safe_enabled(enabled) {
|
|
|
177
283
|
* @template {Record<string, unknown> | undefined} [CustomContext=undefined]
|
|
178
284
|
*/
|
|
179
285
|
export class McpServer {
|
|
180
|
-
#server = new JSONRPCServer(
|
|
286
|
+
#server = new JSONRPCServer({
|
|
287
|
+
errorListener: (...args) => {
|
|
288
|
+
const mrtr = this.#ctx_storage.getStore()?.mrtr;
|
|
289
|
+
if (mrtr !== undefined && args[1] === mrtr.signal) return;
|
|
290
|
+
console.warn(...args);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
181
293
|
/**
|
|
182
294
|
* @type {JSONRPCClient<"broadcast" | "standalone"> | undefined}
|
|
183
295
|
*/
|
|
@@ -211,7 +323,12 @@ export class McpServer {
|
|
|
211
323
|
#event_target = new EventTarget();
|
|
212
324
|
|
|
213
325
|
/**
|
|
214
|
-
* @type {
|
|
326
|
+
* @type {ServerInfo}
|
|
327
|
+
*/
|
|
328
|
+
#server_info;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @type {AsyncLocalStorage<ReceiveContext<CustomContext> & { progress_token?: string, request_id?: string | number, stateless?: boolean, mrtr?: MrtrState }>}
|
|
215
332
|
*/
|
|
216
333
|
#ctx_storage = new AsyncLocalStorage();
|
|
217
334
|
|
|
@@ -221,6 +338,35 @@ export class McpServer {
|
|
|
221
338
|
*/
|
|
222
339
|
constructor(server_info, options) {
|
|
223
340
|
this.#options = options;
|
|
341
|
+
this.#server_info = server_info;
|
|
342
|
+
// Remember when a handler stopped because it needs client input. After
|
|
343
|
+
// the JSON-RPC library finishes, `receive()` replaces that temporary
|
|
344
|
+
// error with an `InputRequiredResult`. All other errors keep their
|
|
345
|
+
// normal code and message.
|
|
346
|
+
this.#server.mapErrorToJSONRPCErrorResponse = (id, error) => {
|
|
347
|
+
const mrtr = this.#ctx_storage.getStore()?.mrtr;
|
|
348
|
+
if (
|
|
349
|
+
mrtr !== undefined &&
|
|
350
|
+
error === mrtr.signal &&
|
|
351
|
+
mrtr.pending.size > 0
|
|
352
|
+
) {
|
|
353
|
+
mrtr.signal_at_boundary = true;
|
|
354
|
+
}
|
|
355
|
+
if (error instanceof JSONRPCErrorException) {
|
|
356
|
+
return createJSONRPCErrorResponse(
|
|
357
|
+
id,
|
|
358
|
+
error.code,
|
|
359
|
+
error.message,
|
|
360
|
+
error.data,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return createJSONRPCErrorResponse(
|
|
364
|
+
id,
|
|
365
|
+
-32603,
|
|
366
|
+
/** @type {Error | undefined} */ (error)?.message ??
|
|
367
|
+
'An unexpected error occurred',
|
|
368
|
+
);
|
|
369
|
+
};
|
|
224
370
|
this.#server.addMethod('initialize', (initialize_request) => {
|
|
225
371
|
try {
|
|
226
372
|
// Validate basic request format
|
|
@@ -254,9 +400,12 @@ export class McpServer {
|
|
|
254
400
|
);
|
|
255
401
|
|
|
256
402
|
// Return server response with negotiated version and capabilities
|
|
403
|
+
// (per-request protocol options are not part of the legacy initialize result)
|
|
404
|
+
// eslint-disable-next-line no-unused-vars
|
|
405
|
+
const { cache, ...legacy_options } = options;
|
|
257
406
|
return {
|
|
258
407
|
protocolVersion: negotiated_version,
|
|
259
|
-
...
|
|
408
|
+
...legacy_options,
|
|
260
409
|
serverInfo: server_info,
|
|
261
410
|
};
|
|
262
411
|
} catch (error) {
|
|
@@ -292,12 +441,24 @@ export class McpServer {
|
|
|
292
441
|
this.#server.addMethod('notifications/initialized', () => {
|
|
293
442
|
return null;
|
|
294
443
|
});
|
|
444
|
+
this.#server.addMethod('server/discover', () => {
|
|
445
|
+
// only reachable for per-request (stateless) requests thanks to
|
|
446
|
+
// the method policy guard in `receive`
|
|
447
|
+
return {
|
|
448
|
+
supportedVersions: this.#enabled_protocol_versions(),
|
|
449
|
+
capabilities: this.#discover_capabilities(),
|
|
450
|
+
...(this.#options.instructions != null
|
|
451
|
+
? { instructions: this.#options.instructions }
|
|
452
|
+
: {}),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
295
455
|
this.#init_tools();
|
|
296
456
|
this.#init_prompts();
|
|
297
457
|
this.#init_resources();
|
|
298
458
|
this.#init_roots();
|
|
299
459
|
this.#init_completion();
|
|
300
460
|
this.#init_logging();
|
|
461
|
+
this.#init_subscriptions();
|
|
301
462
|
}
|
|
302
463
|
|
|
303
464
|
/**
|
|
@@ -322,8 +483,20 @@ export class McpServer {
|
|
|
322
483
|
* @type {Context<CustomContext>}
|
|
323
484
|
*/
|
|
324
485
|
get ctx() {
|
|
325
|
-
|
|
326
|
-
const {
|
|
486
|
+
/* eslint-disable no-unused-vars */
|
|
487
|
+
const {
|
|
488
|
+
progress_token,
|
|
489
|
+
request_id,
|
|
490
|
+
stateless,
|
|
491
|
+
mrtr,
|
|
492
|
+
subscriptionOrigin,
|
|
493
|
+
subscriptionManager,
|
|
494
|
+
...rest
|
|
495
|
+
} = this.#ctx_storage.getStore() ?? {};
|
|
496
|
+
/* eslint-enable no-unused-vars */
|
|
497
|
+
if (mrtr !== undefined) {
|
|
498
|
+
rest.requestState = mrtr.incoming_state;
|
|
499
|
+
}
|
|
327
500
|
return rest;
|
|
328
501
|
}
|
|
329
502
|
|
|
@@ -480,6 +653,7 @@ export class McpServer {
|
|
|
480
653
|
|
|
481
654
|
// Validate input arguments if schema is provided
|
|
482
655
|
let validated_args = args;
|
|
656
|
+
this.#mark_mrtr_registration('tool', name, tool.replayable);
|
|
483
657
|
if (tool.schema) {
|
|
484
658
|
let validation_result =
|
|
485
659
|
tool.schema['~standard'].validate(args);
|
|
@@ -617,8 +791,9 @@ export class McpServer {
|
|
|
617
791
|
async ({ name, arguments: args }) => {
|
|
618
792
|
const prompt = this.#prompts.get(name);
|
|
619
793
|
if (!prompt) {
|
|
620
|
-
throw new McpError(-
|
|
794
|
+
throw new McpError(-32602, `Prompt ${name} not found`);
|
|
621
795
|
}
|
|
796
|
+
this.#mark_mrtr_registration('prompt', name, prompt.replayable);
|
|
622
797
|
if (!prompt.schema) {
|
|
623
798
|
return v.parse(
|
|
624
799
|
GetPromptResultSchema,
|
|
@@ -747,9 +922,14 @@ export class McpServer {
|
|
|
747
922
|
params = match.params;
|
|
748
923
|
}
|
|
749
924
|
if (!resource) {
|
|
750
|
-
throw new McpError(-
|
|
925
|
+
throw new McpError(-32602, `Resource ${uri} not found`);
|
|
751
926
|
}
|
|
752
927
|
}
|
|
928
|
+
this.#mark_mrtr_registration(
|
|
929
|
+
resource.template ? 'template' : 'resource',
|
|
930
|
+
resource.name,
|
|
931
|
+
resource.replayable,
|
|
932
|
+
);
|
|
753
933
|
if (resource.template) {
|
|
754
934
|
if (!params)
|
|
755
935
|
throw new McpError(
|
|
@@ -781,6 +961,12 @@ export class McpServer {
|
|
|
781
961
|
* Request roots list from client
|
|
782
962
|
*/
|
|
783
963
|
async #refresh_roots() {
|
|
964
|
+
if (this.#is_stateless) {
|
|
965
|
+
throw new McpError(
|
|
966
|
+
-32603,
|
|
967
|
+
'Client roots are not available during per-request (stateless) execution: roots are deprecated in protocol version 2026-07-28 and tmcp does not emit roots/list as a multi round-trip input request',
|
|
968
|
+
);
|
|
969
|
+
}
|
|
784
970
|
if (!this.#client_capabilities?.roots) return;
|
|
785
971
|
|
|
786
972
|
this.#lazyily_create_client();
|
|
@@ -826,28 +1012,231 @@ export class McpServer {
|
|
|
826
1012
|
});
|
|
827
1013
|
}
|
|
828
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Reduce a requested filter to notification types enabled by this server.
|
|
1017
|
+
* @param {SubscriptionFilter} requested
|
|
1018
|
+
* @returns {SubscriptionFilter}
|
|
1019
|
+
*/
|
|
1020
|
+
#subscription_filters(requested) {
|
|
1021
|
+
const capabilities = this.#options.capabilities;
|
|
1022
|
+
/** @type {SubscriptionFilter} */
|
|
1023
|
+
const filters = {};
|
|
1024
|
+
if (
|
|
1025
|
+
requested.toolsListChanged === true &&
|
|
1026
|
+
capabilities?.tools?.listChanged === true
|
|
1027
|
+
) {
|
|
1028
|
+
filters.toolsListChanged = true;
|
|
1029
|
+
}
|
|
1030
|
+
if (
|
|
1031
|
+
requested.promptsListChanged === true &&
|
|
1032
|
+
capabilities?.prompts?.listChanged === true
|
|
1033
|
+
) {
|
|
1034
|
+
filters.promptsListChanged = true;
|
|
1035
|
+
}
|
|
1036
|
+
if (
|
|
1037
|
+
requested.resourcesListChanged === true &&
|
|
1038
|
+
capabilities?.resources?.listChanged === true
|
|
1039
|
+
) {
|
|
1040
|
+
filters.resourcesListChanged = true;
|
|
1041
|
+
}
|
|
1042
|
+
if (
|
|
1043
|
+
requested.resourceSubscriptions?.length &&
|
|
1044
|
+
capabilities?.resources?.subscribe === true
|
|
1045
|
+
) {
|
|
1046
|
+
const resources = [
|
|
1047
|
+
...new Set(requested.resourceSubscriptions),
|
|
1048
|
+
].filter(
|
|
1049
|
+
(uri) =>
|
|
1050
|
+
this.#subscription_resource_registration(uri) !== undefined,
|
|
1051
|
+
);
|
|
1052
|
+
if (resources.length > 0) filters.resourceSubscriptions = resources;
|
|
1053
|
+
}
|
|
1054
|
+
return filters;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Resolve a static resource or a concrete URI handled by a template. The
|
|
1059
|
+
* template pattern itself is not a subscribable resource URI.
|
|
1060
|
+
* @param {string} uri
|
|
1061
|
+
*/
|
|
1062
|
+
#subscription_resource_registration(uri) {
|
|
1063
|
+
const resource = this.#resources.get(uri);
|
|
1064
|
+
if (resource) return resource.template ? undefined : resource;
|
|
1065
|
+
const match = this.#templates.match(uri);
|
|
1066
|
+
return match ? this.#resources.get(match.template) : undefined;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Register the long-lived per-request subscription method and cancellation
|
|
1071
|
+
* notification. Transports keep the response sink open while the returned
|
|
1072
|
+
* promise is pending.
|
|
1073
|
+
*/
|
|
1074
|
+
#init_subscriptions() {
|
|
1075
|
+
this.#server.addMethod('subscriptions/listen', async (params) => {
|
|
1076
|
+
const parsed = v.safeParse(
|
|
1077
|
+
SubscriptionsListenRequestParamsSchema,
|
|
1078
|
+
params,
|
|
1079
|
+
);
|
|
1080
|
+
if (!parsed.success) {
|
|
1081
|
+
throw new McpError(
|
|
1082
|
+
-32602,
|
|
1083
|
+
'Invalid subscriptions/listen params: expected a notifications filter',
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
const id = this.#ctx_storage.getStore()?.request_id;
|
|
1087
|
+
if (id === undefined) {
|
|
1088
|
+
throw new McpError(
|
|
1089
|
+
-32600,
|
|
1090
|
+
'subscriptions/listen must be a JSON-RPC request with an id',
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const store = this.#ctx_storage.getStore();
|
|
1094
|
+
const origin = store?.subscriptionOrigin;
|
|
1095
|
+
const manager = store?.subscriptionManager;
|
|
1096
|
+
if (typeof origin !== 'string' || manager === undefined) {
|
|
1097
|
+
throw new McpError(
|
|
1098
|
+
-32603,
|
|
1099
|
+
'subscriptions/listen requires the transport to provide a subscriptionManager and stable string subscriptionOrigin for this connection',
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const filters = this.#subscription_filters(
|
|
1103
|
+
parsed.output.notifications,
|
|
1104
|
+
);
|
|
1105
|
+
/** @type {(result: Record<string, unknown>)=>void} */
|
|
1106
|
+
let settle = () => {};
|
|
1107
|
+
const pending = new Promise((resolve) => {
|
|
1108
|
+
settle = resolve;
|
|
1109
|
+
});
|
|
1110
|
+
const created = await manager.create(
|
|
1111
|
+
{ id, origin, filters },
|
|
1112
|
+
{
|
|
1113
|
+
acknowledge: () =>
|
|
1114
|
+
this.#subscription_send(id, origin, {
|
|
1115
|
+
jsonrpc: '2.0',
|
|
1116
|
+
method: 'notifications/subscriptions/acknowledged',
|
|
1117
|
+
params: {
|
|
1118
|
+
notifications: filters,
|
|
1119
|
+
_meta: {
|
|
1120
|
+
[SUBSCRIPTION_ID_META_KEY]: id,
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
}),
|
|
1124
|
+
send: (notification) =>
|
|
1125
|
+
this.#subscription_send(
|
|
1126
|
+
id,
|
|
1127
|
+
origin,
|
|
1128
|
+
this.#tag_subscription(notification, id),
|
|
1129
|
+
),
|
|
1130
|
+
close: () => {
|
|
1131
|
+
settle({
|
|
1132
|
+
_meta: { [SUBSCRIPTION_ID_META_KEY]: id },
|
|
1133
|
+
});
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
);
|
|
1137
|
+
if (!created) {
|
|
1138
|
+
throw new McpError(
|
|
1139
|
+
-32602,
|
|
1140
|
+
`A subscription with id ${JSON.stringify(id)} is already active for this connection`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
return pending;
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
this.#server.addMethod('notifications/cancelled', async (params) => {
|
|
1147
|
+
const cancellation = v.safeParse(
|
|
1148
|
+
CancelledNotificationSchema.entries.params,
|
|
1149
|
+
params,
|
|
1150
|
+
);
|
|
1151
|
+
const store = this.#ctx_storage.getStore();
|
|
1152
|
+
const origin = store?.subscriptionOrigin;
|
|
1153
|
+
const manager = store?.subscriptionManager;
|
|
1154
|
+
if (
|
|
1155
|
+
cancellation.success &&
|
|
1156
|
+
origin !== undefined &&
|
|
1157
|
+
manager !== undefined
|
|
1158
|
+
) {
|
|
1159
|
+
await manager.close(
|
|
1160
|
+
cancellation.output.requestId,
|
|
1161
|
+
origin,
|
|
1162
|
+
'cancelled',
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return null;
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* @param {JSONRPCRequest} notification
|
|
1171
|
+
* @param {string | number} subscription_id
|
|
1172
|
+
*/
|
|
1173
|
+
#tag_subscription(notification, subscription_id) {
|
|
1174
|
+
const params = /** @type {Record<string, unknown>} */ (
|
|
1175
|
+
notification.params ?? {}
|
|
1176
|
+
);
|
|
1177
|
+
return {
|
|
1178
|
+
...notification,
|
|
1179
|
+
params: {
|
|
1180
|
+
...params,
|
|
1181
|
+
_meta: {
|
|
1182
|
+
.../** @type {Record<string, unknown> | undefined} */ (
|
|
1183
|
+
params._meta
|
|
1184
|
+
),
|
|
1185
|
+
[SUBSCRIPTION_ID_META_KEY]: subscription_id,
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* @param {string | number} subscription_id
|
|
1193
|
+
* @param {string} origin
|
|
1194
|
+
* @param {JSONRPCRequest} request
|
|
1195
|
+
*/
|
|
1196
|
+
#subscription_send(subscription_id, origin, request) {
|
|
1197
|
+
this.#event_target.dispatchEvent(
|
|
1198
|
+
event('send', {
|
|
1199
|
+
subscriptionId: subscription_id,
|
|
1200
|
+
subscriptionOrigin: origin,
|
|
1201
|
+
request,
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
829
1206
|
#notify_tools_list_changed() {
|
|
830
1207
|
if (this.#options.capabilities?.tools?.listChanged) {
|
|
831
|
-
this.#
|
|
1208
|
+
this.#broadcast_change('notifications/tools/list_changed', {});
|
|
832
1209
|
}
|
|
833
1210
|
}
|
|
834
1211
|
|
|
835
1212
|
#notify_prompts_list_changed() {
|
|
836
1213
|
if (this.#options.capabilities?.prompts?.listChanged) {
|
|
837
|
-
this.#
|
|
1214
|
+
this.#broadcast_change('notifications/prompts/list_changed', {});
|
|
838
1215
|
}
|
|
839
1216
|
}
|
|
840
1217
|
|
|
841
1218
|
#notify_resources_list_changed() {
|
|
842
1219
|
if (this.#options.capabilities?.resources?.listChanged) {
|
|
843
|
-
this.#
|
|
844
|
-
'notifications/resources/list_changed',
|
|
845
|
-
{},
|
|
846
|
-
'broadcast',
|
|
847
|
-
);
|
|
1220
|
+
this.#broadcast_change('notifications/resources/list_changed', {});
|
|
848
1221
|
}
|
|
849
1222
|
}
|
|
850
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* Publish a change through the existing broadcast lifecycle. Transports use
|
|
1226
|
+
* `subscriptionOnly` to keep concrete template updates off legacy streams.
|
|
1227
|
+
* @param {string} method
|
|
1228
|
+
* @param {Record<string, unknown>} params
|
|
1229
|
+
* @param {boolean} [subscription_only]
|
|
1230
|
+
*/
|
|
1231
|
+
#broadcast_change(method, params, subscription_only = false) {
|
|
1232
|
+
this.#event_target.dispatchEvent(
|
|
1233
|
+
event('broadcast', {
|
|
1234
|
+
request: { jsonrpc: '2.0', method, params },
|
|
1235
|
+
...(subscription_only ? { subscriptionOnly: true } : {}),
|
|
1236
|
+
}),
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
851
1240
|
/**
|
|
852
1241
|
* Use the `defineTool` utility to create a reusable tool and pass it to this method to add it to the server.
|
|
853
1242
|
* @template {Array<CreatedTool<any, any>>} T
|
|
@@ -941,6 +1330,28 @@ export class McpServer {
|
|
|
941
1330
|
this.#tools.set(tool_or_options.name, stored_tool);
|
|
942
1331
|
}
|
|
943
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Run transport-specific validation against a registered tool's JSON
|
|
1335
|
+
* Schema without enabling, validating, or executing the tool.
|
|
1336
|
+
* @param {string} name
|
|
1337
|
+
* @param {Record<string, unknown>} args
|
|
1338
|
+
* @param {(input_schema: Record<string, unknown>, args: Record<string, unknown>)=>void | Promise<void>} validator
|
|
1339
|
+
* @returns {Promise<boolean>}
|
|
1340
|
+
*/
|
|
1341
|
+
async validateToolCall(name, args, validator) {
|
|
1342
|
+
const tool = this.#tools.get(name);
|
|
1343
|
+
if (!tool) return false;
|
|
1344
|
+
const input_schema =
|
|
1345
|
+
tool.schema && this.#options.adapter
|
|
1346
|
+
? await this.#options.adapter.toJsonSchema(tool.schema)
|
|
1347
|
+
: { type: 'object', properties: {} };
|
|
1348
|
+
await validator(
|
|
1349
|
+
/** @type {Record<string, unknown>} */ (input_schema),
|
|
1350
|
+
args,
|
|
1351
|
+
);
|
|
1352
|
+
return true;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
944
1355
|
/**
|
|
945
1356
|
* Add a prompt to the server. Prompts are used to provide the user with pre-defined messages that adds context to the LLM.
|
|
946
1357
|
* Use the description and title to help the user to understand what the prompt does and when to use it.
|
|
@@ -1101,11 +1512,127 @@ export class McpServer {
|
|
|
1101
1512
|
stored_template.template = true;
|
|
1102
1513
|
this.#resource(stored_template);
|
|
1103
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* The per-request protocol versions supported by tmcp.
|
|
1517
|
+
* @returns {string[]}
|
|
1518
|
+
*/
|
|
1519
|
+
#enabled_protocol_versions() {
|
|
1520
|
+
return getPerRequestProtocolVersions();
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Check whether a JSON-RPC method is registered without invoking it.
|
|
1525
|
+
* @param {string} method
|
|
1526
|
+
*/
|
|
1527
|
+
hasMethod(method) {
|
|
1528
|
+
return this.#server.hasMethod(method);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Derive capabilities advertised through `server/discover`. Subscription
|
|
1533
|
+
* flags stay hidden until the active transport can route long-lived listen
|
|
1534
|
+
* streams; core support alone cannot guarantee delivery.
|
|
1535
|
+
* @returns {Record<string, unknown>}
|
|
1536
|
+
*/
|
|
1537
|
+
#discover_capabilities() {
|
|
1538
|
+
const store = this.#ctx_storage.getStore();
|
|
1539
|
+
if (
|
|
1540
|
+
store?.subscriptionManager !== undefined &&
|
|
1541
|
+
typeof store.subscriptionOrigin === 'string'
|
|
1542
|
+
) {
|
|
1543
|
+
return { ...(this.#options.capabilities ?? {}) };
|
|
1544
|
+
}
|
|
1545
|
+
const { resources, tools, prompts, ...rest } =
|
|
1546
|
+
this.#options.capabilities ?? {};
|
|
1547
|
+
/** @type {Record<string, unknown>} */
|
|
1548
|
+
const capabilities = { ...rest };
|
|
1549
|
+
if (resources) {
|
|
1550
|
+
// eslint-disable-next-line no-unused-vars
|
|
1551
|
+
const { subscribe, listChanged, ...resources_rest } = resources;
|
|
1552
|
+
capabilities.resources = resources_rest;
|
|
1553
|
+
}
|
|
1554
|
+
if (tools) {
|
|
1555
|
+
// eslint-disable-next-line no-unused-vars
|
|
1556
|
+
const { listChanged, ...tools_rest } = tools;
|
|
1557
|
+
capabilities.tools = tools_rest;
|
|
1558
|
+
}
|
|
1559
|
+
if (prompts) {
|
|
1560
|
+
// eslint-disable-next-line no-unused-vars
|
|
1561
|
+
const { listChanged, ...prompts_rest } = prompts;
|
|
1562
|
+
capabilities.prompts = prompts_rest;
|
|
1563
|
+
}
|
|
1564
|
+
return capabilities;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Resolve the effective cache policy for a cacheable method.
|
|
1569
|
+
* @param {string} method
|
|
1570
|
+
* @returns {{ ttlMs: number, cacheScope: 'public' | 'private' }}
|
|
1571
|
+
*/
|
|
1572
|
+
#cache_policy(method) {
|
|
1573
|
+
const cache = this.#options.cache;
|
|
1574
|
+
const override = cache?.methods?.[method];
|
|
1575
|
+
return {
|
|
1576
|
+
ttlMs: override?.ttlMs ?? cache?.ttlMs ?? 0,
|
|
1577
|
+
cacheScope: override?.cacheScope ?? cache?.cacheScope ?? 'private',
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Decorate a successful per-request (stateless) wire result:
|
|
1583
|
+
* `resultType` (preserving handler-provided string extension values,
|
|
1584
|
+
* anything else becomes `'complete'`), serverInfo `_meta` merge, and
|
|
1585
|
+
* `ttlMs`/`cacheScope` for cacheable methods. The cache fields are
|
|
1586
|
+
* ALWAYS taken from the configured policy, overwriting any
|
|
1587
|
+
* handler-provided values: handlers are profile-unaware and must not be
|
|
1588
|
+
* able to opt results into public caching.
|
|
1589
|
+
* @param {string} method
|
|
1590
|
+
* @param {Record<string, unknown>} result
|
|
1591
|
+
* @param {boolean} [private_result]
|
|
1592
|
+
*/
|
|
1593
|
+
#decorate_result(method, result, private_result = false) {
|
|
1594
|
+
if (typeof result.resultType !== 'string') {
|
|
1595
|
+
result.resultType = 'complete';
|
|
1596
|
+
}
|
|
1597
|
+
result._meta = {
|
|
1598
|
+
.../** @type {Record<string, unknown> | undefined} */ (
|
|
1599
|
+
result._meta
|
|
1600
|
+
),
|
|
1601
|
+
'io.modelcontextprotocol/serverInfo': this.#server_info,
|
|
1602
|
+
};
|
|
1603
|
+
if (CACHEABLE_METHODS.has(method)) {
|
|
1604
|
+
const policy = private_result
|
|
1605
|
+
? { ttlMs: 0, cacheScope: /** @type {const} */ ('private') }
|
|
1606
|
+
: this.#cache_policy(method);
|
|
1607
|
+
result.ttlMs = policy.ttlMs;
|
|
1608
|
+
result.cacheScope = policy.cacheScope;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Build a JSON-RPC error response for a request, or `null` for a
|
|
1614
|
+
* notification (which cannot receive a response).
|
|
1615
|
+
* @param {string | number | undefined} id
|
|
1616
|
+
* @param {McpError} error
|
|
1617
|
+
*/
|
|
1618
|
+
#error_response(id, error) {
|
|
1619
|
+
if (id === undefined) return Promise.resolve(null);
|
|
1620
|
+
return Promise.resolve({
|
|
1621
|
+
jsonrpc: /** @type {const} */ ('2.0'),
|
|
1622
|
+
id,
|
|
1623
|
+
error: {
|
|
1624
|
+
code: error.code,
|
|
1625
|
+
message: error.message,
|
|
1626
|
+
...(error.data !== undefined ? { data: error.data } : {}),
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1104
1631
|
/**
|
|
1105
1632
|
* The main function that receive a JSONRpc message and either dispatch a `send` event or process the request.
|
|
1106
1633
|
*
|
|
1107
1634
|
* @param {JSONRPCMessage} message
|
|
1108
|
-
* @param {
|
|
1635
|
+
* @param {ReceiveContext<CustomContext>} [ctx]
|
|
1109
1636
|
* @returns {ReturnType<JSONRPCServer['receive']> | ReturnType<JSONRPCClient['receive'] | undefined>}
|
|
1110
1637
|
*/
|
|
1111
1638
|
receive(message, ctx) {
|
|
@@ -1117,14 +1644,422 @@ export class McpServer {
|
|
|
1117
1644
|
|
|
1118
1645
|
// Check if it's a request or response
|
|
1119
1646
|
if (validated_message.success) {
|
|
1647
|
+
const request_message = validated_message.output;
|
|
1648
|
+
const id = 'id' in request_message ? request_message.id : undefined;
|
|
1649
|
+
const meta = request_message.params?._meta;
|
|
1120
1650
|
const progress_token = /** @type {string | undefined} */ (
|
|
1121
|
-
|
|
1651
|
+
meta?.progressToken
|
|
1122
1652
|
);
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1653
|
+
const requested_version =
|
|
1654
|
+
meta?.['io.modelcontextprotocol/protocolVersion'];
|
|
1655
|
+
const request_capabilities =
|
|
1656
|
+
meta?.['io.modelcontextprotocol/clientCapabilities'];
|
|
1657
|
+
const request_client_info =
|
|
1658
|
+
meta?.['io.modelcontextprotocol/clientInfo'];
|
|
1659
|
+
const request_log_level =
|
|
1660
|
+
meta?.['io.modelcontextprotocol/logLevel'];
|
|
1661
|
+
|
|
1662
|
+
// `protocolVersion` and the internal `stateless` flag are set
|
|
1663
|
+
// exclusively by the classification below: transport-provided
|
|
1664
|
+
// values must never make a session-negotiated request look like
|
|
1665
|
+
// a per-request one
|
|
1666
|
+
const {
|
|
1667
|
+
// eslint-disable-next-line no-unused-vars
|
|
1668
|
+
protocolVersion,
|
|
1669
|
+
// eslint-disable-next-line no-unused-vars
|
|
1670
|
+
stateless: _,
|
|
1671
|
+
...transport_ctx
|
|
1672
|
+
} = /** @type {ReceiveContext<CustomContext> & { stateless?: boolean }} */ (
|
|
1673
|
+
ctx ?? {}
|
|
1127
1674
|
);
|
|
1675
|
+
/** @type {ReceiveContext<CustomContext> & { progress_token?: string, request_id?: string | number, stateless?: boolean, mrtr?: MrtrState }} */
|
|
1676
|
+
let store = { ...transport_ctx, progress_token, request_id: id };
|
|
1677
|
+
// classify the request: the presence of ANY reserved per-request
|
|
1678
|
+
// _meta key enters the per-request path — incomplete or invalid
|
|
1679
|
+
// metadata is rejected, never silently merged or downgraded to
|
|
1680
|
+
// the session path
|
|
1681
|
+
if (
|
|
1682
|
+
requested_version !== undefined ||
|
|
1683
|
+
request_capabilities !== undefined ||
|
|
1684
|
+
request_client_info !== undefined ||
|
|
1685
|
+
request_log_level !== undefined
|
|
1686
|
+
) {
|
|
1687
|
+
if (
|
|
1688
|
+
requested_version === undefined ||
|
|
1689
|
+
request_capabilities === undefined
|
|
1690
|
+
) {
|
|
1691
|
+
return this.#error_response(
|
|
1692
|
+
id,
|
|
1693
|
+
new McpError(
|
|
1694
|
+
-32602,
|
|
1695
|
+
'Requests with per-request protocol metadata must include both "io.modelcontextprotocol/protocolVersion" and "io.modelcontextprotocol/clientCapabilities" in _meta',
|
|
1696
|
+
),
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof requested_version !== 'string') {
|
|
1700
|
+
return this.#error_response(
|
|
1701
|
+
id,
|
|
1702
|
+
new McpError(
|
|
1703
|
+
-32602,
|
|
1704
|
+
'Invalid "io.modelcontextprotocol/protocolVersion" in _meta: expected a string',
|
|
1705
|
+
),
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
// `looseObject` alone would accept arrays, so check the
|
|
1709
|
+
// basic shape explicitly first
|
|
1710
|
+
const parsed_capabilities =
|
|
1711
|
+
typeof request_capabilities !== 'object' ||
|
|
1712
|
+
request_capabilities === null ||
|
|
1713
|
+
Array.isArray(request_capabilities)
|
|
1714
|
+
? undefined
|
|
1715
|
+
: v.safeParse(
|
|
1716
|
+
ClientCapabilitiesSchema,
|
|
1717
|
+
request_capabilities,
|
|
1718
|
+
);
|
|
1719
|
+
if (!parsed_capabilities?.success) {
|
|
1720
|
+
return this.#error_response(
|
|
1721
|
+
id,
|
|
1722
|
+
new McpError(
|
|
1723
|
+
-32602,
|
|
1724
|
+
'Invalid "io.modelcontextprotocol/clientCapabilities" in _meta: expected a client capabilities object',
|
|
1725
|
+
),
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const parsed_client_info =
|
|
1729
|
+
request_client_info === undefined
|
|
1730
|
+
? undefined
|
|
1731
|
+
: v.safeParse(
|
|
1732
|
+
ImplementationSchema,
|
|
1733
|
+
request_client_info,
|
|
1734
|
+
);
|
|
1735
|
+
if (parsed_client_info && !parsed_client_info.success) {
|
|
1736
|
+
return this.#error_response(
|
|
1737
|
+
id,
|
|
1738
|
+
new McpError(
|
|
1739
|
+
-32602,
|
|
1740
|
+
'Invalid "io.modelcontextprotocol/clientInfo" in _meta: expected an implementation object with "name" and "version"',
|
|
1741
|
+
),
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
const parsed_log_level =
|
|
1745
|
+
request_log_level === undefined
|
|
1746
|
+
? undefined
|
|
1747
|
+
: v.safeParse(LoggingLevelSchema, request_log_level);
|
|
1748
|
+
if (parsed_log_level && !parsed_log_level.success) {
|
|
1749
|
+
return this.#error_response(
|
|
1750
|
+
id,
|
|
1751
|
+
new McpError(
|
|
1752
|
+
-32602,
|
|
1753
|
+
'Invalid "io.modelcontextprotocol/logLevel" in _meta: expected a logging level',
|
|
1754
|
+
),
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
const enabled = this.#enabled_protocol_versions();
|
|
1758
|
+
if (!enabled.includes(requested_version)) {
|
|
1759
|
+
return this.#error_response(
|
|
1760
|
+
id,
|
|
1761
|
+
unsupported_protocol_version_error(
|
|
1762
|
+
enabled,
|
|
1763
|
+
requested_version,
|
|
1764
|
+
),
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
// per-request info fully replaces any transport-provided
|
|
1768
|
+
// session info: stateless requests never inherit session state
|
|
1769
|
+
store = {
|
|
1770
|
+
...store,
|
|
1771
|
+
stateless: true,
|
|
1772
|
+
protocolVersion: requested_version,
|
|
1773
|
+
sessionInfo: {
|
|
1774
|
+
clientCapabilities: parsed_capabilities.output,
|
|
1775
|
+
clientInfo: parsed_client_info?.output,
|
|
1776
|
+
logLevel: parsed_log_level?.output,
|
|
1777
|
+
},
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const stateless = store.stateless === true;
|
|
1782
|
+
if (
|
|
1783
|
+
!is_method_allowed(request_message.method, stateless) ||
|
|
1784
|
+
!this.hasMethod(request_message.method)
|
|
1785
|
+
) {
|
|
1786
|
+
return this.#error_response(
|
|
1787
|
+
id,
|
|
1788
|
+
new McpError(
|
|
1789
|
+
-32601,
|
|
1790
|
+
`Method ${request_message.method} not found`,
|
|
1791
|
+
),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Read retry answers and saved state before running the handler, then
|
|
1796
|
+
// remove them from its normal arguments. Only tool calls, prompt reads,
|
|
1797
|
+
// and resource reads can receive these fields.
|
|
1798
|
+
/** @type {string | undefined} */
|
|
1799
|
+
let encoded_incoming_state;
|
|
1800
|
+
const params = request_message.params;
|
|
1801
|
+
const raw_input_responses = params?.inputResponses;
|
|
1802
|
+
const raw_request_state = params?.requestState;
|
|
1803
|
+
if (
|
|
1804
|
+
!stateless &&
|
|
1805
|
+
(raw_input_responses !== undefined ||
|
|
1806
|
+
raw_request_state !== undefined)
|
|
1807
|
+
) {
|
|
1808
|
+
return this.#error_response(
|
|
1809
|
+
id,
|
|
1810
|
+
new McpError(
|
|
1811
|
+
-32602,
|
|
1812
|
+
'"inputResponses" and "requestState" params are only supported by per-request (stateless) multi round-trip requests',
|
|
1813
|
+
),
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
if (stateless) {
|
|
1817
|
+
/** @type {Record<string, unknown> | undefined} */
|
|
1818
|
+
let input_responses;
|
|
1819
|
+
if (
|
|
1820
|
+
raw_input_responses !== undefined ||
|
|
1821
|
+
raw_request_state !== undefined
|
|
1822
|
+
) {
|
|
1823
|
+
if (!MRTR_METHODS.has(request_message.method)) {
|
|
1824
|
+
return this.#error_response(
|
|
1825
|
+
id,
|
|
1826
|
+
new McpError(
|
|
1827
|
+
-32602,
|
|
1828
|
+
`"inputResponses" and "requestState" params are only accepted on multi round-trip methods (${[...MRTR_METHODS].join(', ')})`,
|
|
1829
|
+
),
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
if (raw_input_responses !== undefined) {
|
|
1833
|
+
const parsed = v.safeParse(
|
|
1834
|
+
InputResponsesSchema,
|
|
1835
|
+
raw_input_responses,
|
|
1836
|
+
);
|
|
1837
|
+
if (!parsed.success) {
|
|
1838
|
+
return this.#error_response(
|
|
1839
|
+
id,
|
|
1840
|
+
new McpError(
|
|
1841
|
+
-32602,
|
|
1842
|
+
'Invalid "inputResponses": expected a map of input response objects keyed by input request key',
|
|
1843
|
+
),
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
input_responses = parsed.output;
|
|
1847
|
+
}
|
|
1848
|
+
if (raw_request_state !== undefined) {
|
|
1849
|
+
if (typeof raw_request_state !== 'string') {
|
|
1850
|
+
return this.#error_response(
|
|
1851
|
+
id,
|
|
1852
|
+
new McpError(
|
|
1853
|
+
-32602,
|
|
1854
|
+
'Invalid "requestState": expected the opaque string previously returned by the server',
|
|
1855
|
+
),
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
if (
|
|
1859
|
+
raw_request_state.length >
|
|
1860
|
+
MAX_ENCODED_REQUEST_STATE_LENGTH
|
|
1861
|
+
) {
|
|
1862
|
+
return this.#error_response(
|
|
1863
|
+
id,
|
|
1864
|
+
new McpError(
|
|
1865
|
+
-32602,
|
|
1866
|
+
`Invalid "requestState": encoded state exceeds the maximum accepted length of ${MAX_ENCODED_REQUEST_STATE_LENGTH} characters`,
|
|
1867
|
+
),
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
encoded_incoming_state = raw_request_state;
|
|
1871
|
+
}
|
|
1872
|
+
if (params) {
|
|
1873
|
+
delete params.inputResponses;
|
|
1874
|
+
delete params.requestState;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (MRTR_METHODS.has(request_message.method)) {
|
|
1878
|
+
store.mrtr = {
|
|
1879
|
+
input_responses,
|
|
1880
|
+
incoming_state: undefined,
|
|
1881
|
+
ordinal: 0,
|
|
1882
|
+
used_keys: new Set(),
|
|
1883
|
+
pending: new Map(),
|
|
1884
|
+
consumed_responses: new Map(),
|
|
1885
|
+
outgoing_state: undefined,
|
|
1886
|
+
registration: undefined,
|
|
1887
|
+
input_error: undefined,
|
|
1888
|
+
signal: new InputRequiredSignal(),
|
|
1889
|
+
signal_at_boundary: false,
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return this.#ctx_storage.run(store, async () => {
|
|
1895
|
+
const mrtr = store.mrtr;
|
|
1896
|
+
if (
|
|
1897
|
+
mrtr !== undefined &&
|
|
1898
|
+
encoded_incoming_state !== undefined
|
|
1899
|
+
) {
|
|
1900
|
+
let decoded_state;
|
|
1901
|
+
try {
|
|
1902
|
+
decoded_state = await this.#request_state_codec.decode(
|
|
1903
|
+
encoded_incoming_state,
|
|
1904
|
+
);
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
return this.#error_response(
|
|
1907
|
+
id,
|
|
1908
|
+
new McpError(
|
|
1909
|
+
-32602,
|
|
1910
|
+
`Invalid "requestState": the configured codec failed to decode it (${/** @type {Error} */ (error)?.message})`,
|
|
1911
|
+
),
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
const parsed_state = v.safeParse(
|
|
1915
|
+
RequestStateEnvelopeSchema,
|
|
1916
|
+
decoded_state,
|
|
1917
|
+
);
|
|
1918
|
+
if (!parsed_state.success) {
|
|
1919
|
+
return this.#error_response(
|
|
1920
|
+
id,
|
|
1921
|
+
new McpError(
|
|
1922
|
+
-32602,
|
|
1923
|
+
'Invalid "requestState": expected state previously returned by tmcp',
|
|
1924
|
+
),
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
mrtr.incoming_state = parsed_state.output.state;
|
|
1928
|
+
mrtr.outgoing_state = parsed_state.output.state;
|
|
1929
|
+
mrtr.input_responses = Object.assign(
|
|
1930
|
+
Object.create(null),
|
|
1931
|
+
parsed_state.output.inputResponses,
|
|
1932
|
+
mrtr.input_responses,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
const response = await this.#server.receive(request_message);
|
|
1936
|
+
if (mrtr?.input_error !== undefined) {
|
|
1937
|
+
return this.#error_response(id, mrtr.input_error);
|
|
1938
|
+
}
|
|
1939
|
+
if (
|
|
1940
|
+
mrtr !== undefined &&
|
|
1941
|
+
mrtr.pending.size > 0 &&
|
|
1942
|
+
response != null
|
|
1943
|
+
) {
|
|
1944
|
+
if ('result' in response) {
|
|
1945
|
+
// The handler returned even though it still needs client input.
|
|
1946
|
+
// It probably caught tmcp's private error or forgot to await the
|
|
1947
|
+
// input call.
|
|
1948
|
+
return this.#error_response(
|
|
1949
|
+
id,
|
|
1950
|
+
new McpError(
|
|
1951
|
+
-32603,
|
|
1952
|
+
`Handler${mrtr.registration ? ` for ${mrtr.registration.kind} "${mrtr.registration.name}"` : ''} returned a result while input requests are pending: a catch block swallowed the internal input-required signal thrown by elicitation()/message(), or an input call was not awaited. Always await input calls. Catch blocks must rethrow the signal — use isInputRequired(error) to detect it and rethrow selectively.`,
|
|
1953
|
+
),
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (mrtr.signal_at_boundary) {
|
|
1957
|
+
/** @type {Record<string, { method: string, params: Record<string, unknown> }>} */
|
|
1958
|
+
let input_requests;
|
|
1959
|
+
try {
|
|
1960
|
+
const entries = await Promise.all(
|
|
1961
|
+
[...mrtr.pending].map(
|
|
1962
|
+
async ([key, request]) =>
|
|
1963
|
+
/** @type {[string, { method: string, params: Record<string, unknown> }]} */ ([
|
|
1964
|
+
key,
|
|
1965
|
+
await request,
|
|
1966
|
+
]),
|
|
1967
|
+
),
|
|
1968
|
+
);
|
|
1969
|
+
input_requests = Object.fromEntries(entries);
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
return this.#error_response(
|
|
1972
|
+
id,
|
|
1973
|
+
new McpError(
|
|
1974
|
+
-32603,
|
|
1975
|
+
`Failed to prepare an input request: ${/** @type {Error} */ (error)?.message ?? 'unknown error'}`,
|
|
1976
|
+
),
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
// Tell the client what input is needed. Do not add cache fields,
|
|
1980
|
+
// because this is not the final result and must not be cached.
|
|
1981
|
+
/** @type {Record<string, unknown>} */
|
|
1982
|
+
const result = {
|
|
1983
|
+
resultType: 'input_required',
|
|
1984
|
+
inputRequests: input_requests,
|
|
1985
|
+
_meta: {
|
|
1986
|
+
'io.modelcontextprotocol/serverInfo':
|
|
1987
|
+
this.#server_info,
|
|
1988
|
+
},
|
|
1989
|
+
};
|
|
1990
|
+
if (
|
|
1991
|
+
mrtr.outgoing_state !== undefined ||
|
|
1992
|
+
mrtr.consumed_responses.size > 0
|
|
1993
|
+
) {
|
|
1994
|
+
/** @type {string} */
|
|
1995
|
+
let encoded;
|
|
1996
|
+
try {
|
|
1997
|
+
// Clients only have to answer the latest inputRequests. Keep
|
|
1998
|
+
// answers already used by the handler so the next retry can
|
|
1999
|
+
// start from the top without asking the same questions again.
|
|
2000
|
+
encoded =
|
|
2001
|
+
await this.#request_state_codec.encode({
|
|
2002
|
+
version: 1,
|
|
2003
|
+
inputResponses: Object.fromEntries(
|
|
2004
|
+
mrtr.consumed_responses,
|
|
2005
|
+
),
|
|
2006
|
+
...(mrtr.outgoing_state !== undefined
|
|
2007
|
+
? { state: mrtr.outgoing_state }
|
|
2008
|
+
: {}),
|
|
2009
|
+
});
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
return this.#error_response(
|
|
2012
|
+
id,
|
|
2013
|
+
new McpError(
|
|
2014
|
+
-32603,
|
|
2015
|
+
`Failed to encode requestState with the configured codec: ${/** @type {Error} */ (error)?.message}`,
|
|
2016
|
+
),
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
if (
|
|
2020
|
+
typeof encoded !== 'string' ||
|
|
2021
|
+
encoded.length >
|
|
2022
|
+
MAX_ENCODED_REQUEST_STATE_LENGTH
|
|
2023
|
+
) {
|
|
2024
|
+
return this.#error_response(
|
|
2025
|
+
id,
|
|
2026
|
+
new McpError(
|
|
2027
|
+
-32603,
|
|
2028
|
+
`The requestState codec must produce a string of at most ${MAX_ENCODED_REQUEST_STATE_LENGTH} characters`,
|
|
2029
|
+
),
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
result.requestState = encoded;
|
|
2033
|
+
}
|
|
2034
|
+
return {
|
|
2035
|
+
jsonrpc: /** @type {const} */ ('2.0'),
|
|
2036
|
+
id: /** @type {string | number} */ (id),
|
|
2037
|
+
result,
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
// an unrelated error won the race against (or replaced)
|
|
2041
|
+
// the signal: let it through unchanged
|
|
2042
|
+
}
|
|
2043
|
+
if (stateless && response != null && 'result' in response) {
|
|
2044
|
+
// a successful stateless response must always be a
|
|
2045
|
+
// decorated object result, even when the handler
|
|
2046
|
+
// resolved `null` (e.g. completion of an unknown ref)
|
|
2047
|
+
if (
|
|
2048
|
+
response.result == null ||
|
|
2049
|
+
typeof response.result !== 'object'
|
|
2050
|
+
) {
|
|
2051
|
+
response.result = {};
|
|
2052
|
+
}
|
|
2053
|
+
this.#decorate_result(
|
|
2054
|
+
request_message.method,
|
|
2055
|
+
/** @type {Record<string, unknown>} */ (
|
|
2056
|
+
response.result
|
|
2057
|
+
),
|
|
2058
|
+
(mrtr?.consumed_responses.size ?? 0) > 0,
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
return response;
|
|
2062
|
+
});
|
|
1128
2063
|
}
|
|
1129
2064
|
// It's a response - handle with client
|
|
1130
2065
|
const validated_response = v.parse(
|
|
@@ -1144,6 +2079,12 @@ export class McpServer {
|
|
|
1144
2079
|
* @returns {Promise<unknown>}
|
|
1145
2080
|
*/
|
|
1146
2081
|
async request({ method, params }) {
|
|
2082
|
+
if (this.#is_stateless) {
|
|
2083
|
+
throw new McpError(
|
|
2084
|
+
-32603,
|
|
2085
|
+
'Low-level server-to-client requests are not supported for per-request protocol requests: there is no server-to-client JSON-RPC channel, and arbitrary requests cannot be translated into multi round-trip input requests',
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
1147
2088
|
this.#lazyily_create_client();
|
|
1148
2089
|
return this.#client?.request(method, params, 'standalone');
|
|
1149
2090
|
}
|
|
@@ -1163,25 +2104,249 @@ export class McpServer {
|
|
|
1163
2104
|
this.#notify_resources_list_changed();
|
|
1164
2105
|
} else {
|
|
1165
2106
|
const resource = this.#resources.get(id);
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
2107
|
+
const subscription_resource =
|
|
2108
|
+
this.#subscription_resource_registration(id);
|
|
2109
|
+
if (!resource && !subscription_resource) return;
|
|
2110
|
+
if (resource) {
|
|
2111
|
+
this.#broadcast_change(`notifications/resources/updated`, {
|
|
1170
2112
|
uri: id,
|
|
1171
2113
|
title: resource.name,
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
)
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
if (!resource && subscription_resource) {
|
|
2117
|
+
this.#broadcast_change(
|
|
2118
|
+
'notifications/resources/updated',
|
|
2119
|
+
{ uri: id },
|
|
2120
|
+
true,
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
1175
2123
|
}
|
|
1176
2124
|
}
|
|
1177
2125
|
|
|
1178
2126
|
/**
|
|
1179
|
-
* Refresh roots list
|
|
2127
|
+
* Refresh the roots list when the server has an open client session.
|
|
2128
|
+
*
|
|
2129
|
+
* This throws when handling a standalone request because roots are no
|
|
2130
|
+
* longer supported by protocol version `2026-07-28`.
|
|
1180
2131
|
*/
|
|
1181
2132
|
async refreshRoots() {
|
|
1182
2133
|
await this.#refresh_roots();
|
|
1183
2134
|
}
|
|
1184
2135
|
|
|
2136
|
+
/**
|
|
2137
|
+
* Whether the client included its version and capabilities in this request
|
|
2138
|
+
* instead of opening a session first.
|
|
2139
|
+
*/
|
|
2140
|
+
get #is_stateless() {
|
|
2141
|
+
return this.#ctx_storage.getStore()?.stateless === true;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Check that the client supports the requested input and that the current
|
|
2146
|
+
* method is allowed to ask for it.
|
|
2147
|
+
* @param {'elicitation' | 'sampling'} capability
|
|
2148
|
+
* @param {string} description Human-readable description for the session-negotiated error message
|
|
2149
|
+
* @param {'form' | 'url'} [elicitation_mode]
|
|
2150
|
+
*/
|
|
2151
|
+
#assert_client_request_allowed(capability, description, elicitation_mode) {
|
|
2152
|
+
const declared_capability = this.#client_capabilities?.[capability];
|
|
2153
|
+
const capability_is_object =
|
|
2154
|
+
typeof declared_capability === 'object' &&
|
|
2155
|
+
declared_capability !== null &&
|
|
2156
|
+
!Array.isArray(declared_capability);
|
|
2157
|
+
const supports_elicitation_mode =
|
|
2158
|
+
capability_is_object &&
|
|
2159
|
+
(capability !== 'elicitation' ||
|
|
2160
|
+
(elicitation_mode === 'url'
|
|
2161
|
+
? declared_capability.url !== undefined
|
|
2162
|
+
: Object.keys(declared_capability).length === 0 ||
|
|
2163
|
+
declared_capability.form !== undefined));
|
|
2164
|
+
const supported = supports_elicitation_mode;
|
|
2165
|
+
if (!supported) {
|
|
2166
|
+
if (this.#is_stateless) {
|
|
2167
|
+
throw missing_required_client_capability_error({
|
|
2168
|
+
[capability]:
|
|
2169
|
+
capability === 'elicitation'
|
|
2170
|
+
? { [elicitation_mode ?? 'form']: {} }
|
|
2171
|
+
: {},
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
throw new McpError(-32601, `Client doesn't support ${description}`);
|
|
2175
|
+
}
|
|
2176
|
+
if (this.#is_stateless && this.#mrtr === undefined) {
|
|
2177
|
+
throw new McpError(
|
|
2178
|
+
-32603,
|
|
2179
|
+
`${description} input requests are only available inside tools/call, prompts/get, and resources/read during per-request (stateless) execution`,
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* Work data used while the current request waits for client input.
|
|
2186
|
+
* It is absent when the current method cannot ask for input.
|
|
2187
|
+
*/
|
|
2188
|
+
get #mrtr() {
|
|
2189
|
+
return this.#ctx_storage.getStore()?.mrtr;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Remember which handler is running so input calls can check its
|
|
2194
|
+
* `replayable` setting and include its name in errors.
|
|
2195
|
+
* @param {string} kind
|
|
2196
|
+
* @param {string} name
|
|
2197
|
+
* @param {boolean | undefined} replayable
|
|
2198
|
+
*/
|
|
2199
|
+
#mark_mrtr_registration(kind, name, replayable) {
|
|
2200
|
+
const mrtr = this.#mrtr;
|
|
2201
|
+
if (mrtr === undefined) return;
|
|
2202
|
+
mrtr.registration = { kind, name, replayable: replayable === true };
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/**
|
|
2206
|
+
* Convert tmcp's retry data to and from text. Plain JSON is used by default,
|
|
2207
|
+
* which means the client can read and change the value.
|
|
2208
|
+
*/
|
|
2209
|
+
get #request_state_codec() {
|
|
2210
|
+
return (
|
|
2211
|
+
this.#options.requestStateCodec ?? {
|
|
2212
|
+
/** @type {(state: unknown)=>string} */
|
|
2213
|
+
encode: (state) => JSON.stringify(state),
|
|
2214
|
+
/** @type {(encoded: string)=>unknown} */
|
|
2215
|
+
decode: (encoded) => JSON.parse(encoded),
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
/**
|
|
2221
|
+
* Check that the handler allows tmcp to run it again after client input.
|
|
2222
|
+
* @param {MrtrState} mrtr
|
|
2223
|
+
* @param {string} description
|
|
2224
|
+
*/
|
|
2225
|
+
#assert_replayable(mrtr, description) {
|
|
2226
|
+
if (mrtr.registration?.replayable) return;
|
|
2227
|
+
const registration = mrtr.registration
|
|
2228
|
+
? `${mrtr.registration.kind} "${mrtr.registration.name}"`
|
|
2229
|
+
: 'registration';
|
|
2230
|
+
throw new McpError(
|
|
2231
|
+
-32603,
|
|
2232
|
+
`${registration} asked the client for ${description}, but it is not marked as replayable. The client must retry the ORIGINAL request with its answer, which starts the handler again FROM THE TOP. Work done before the input call, such as database writes, emails, or payments, may therefore happen more than once. Set \`replayable: true\` on the ${mrtr.registration?.kind ?? 'tool/prompt/resource'} definition only when that work is safe to repeat or is delayed until after all input is available. This is a tmcp safety check, not an MCP requirement.`,
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Use the provided input name, or assign the next number (`"1"`, `"2"`,
|
|
2238
|
+
* and so on). Numbering starts again each time the client retries.
|
|
2239
|
+
* @param {MrtrState} mrtr
|
|
2240
|
+
* @param {string | undefined} explicit_key
|
|
2241
|
+
*/
|
|
2242
|
+
#next_input_key(mrtr, explicit_key) {
|
|
2243
|
+
if (explicit_key != null) {
|
|
2244
|
+
if (mrtr.used_keys.has(explicit_key)) {
|
|
2245
|
+
const error = new McpError(
|
|
2246
|
+
-32603,
|
|
2247
|
+
`Duplicate input request key "${explicit_key}": each elicitation()/message() call within one request attempt must use a distinct key`,
|
|
2248
|
+
);
|
|
2249
|
+
mrtr.input_error = error;
|
|
2250
|
+
throw error;
|
|
2251
|
+
}
|
|
2252
|
+
mrtr.used_keys.add(explicit_key);
|
|
2253
|
+
return explicit_key;
|
|
2254
|
+
}
|
|
2255
|
+
let key;
|
|
2256
|
+
do {
|
|
2257
|
+
mrtr.ordinal += 1;
|
|
2258
|
+
key = String(mrtr.ordinal);
|
|
2259
|
+
} while (mrtr.used_keys.has(key));
|
|
2260
|
+
mrtr.used_keys.add(key);
|
|
2261
|
+
return key;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Let a handler recover from an invalid client answer by asking again with
|
|
2266
|
+
* the same key instead of consuming the same invalid value repeatedly.
|
|
2267
|
+
* @param {MrtrState} mrtr
|
|
2268
|
+
* @param {string} key
|
|
2269
|
+
*/
|
|
2270
|
+
#release_input_response(mrtr, key) {
|
|
2271
|
+
mrtr.used_keys.delete(key);
|
|
2272
|
+
if (mrtr.input_responses !== undefined) {
|
|
2273
|
+
delete mrtr.input_responses[key];
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Save data that the handler will need when the client retries this
|
|
2279
|
+
* request. tmcp turns it into text, sends it to the client, and restores it
|
|
2280
|
+
* as `server.ctx.requestState` on the retry.
|
|
2281
|
+
* Passing `undefined` clears previously set state.
|
|
2282
|
+
*
|
|
2283
|
+
* This does nothing when the server has an open client session or when the
|
|
2284
|
+
* request finishes without asking for input.
|
|
2285
|
+
*
|
|
2286
|
+
* SECURITY: the default JSON converter lets the client read and change this
|
|
2287
|
+
* data. Do not put secrets in it or use it for authorization. Configure a
|
|
2288
|
+
* protected `requestStateCodec` if the server must detect changes.
|
|
2289
|
+
* @param {unknown} state
|
|
2290
|
+
*/
|
|
2291
|
+
setRequestState(state) {
|
|
2292
|
+
const mrtr = this.#mrtr;
|
|
2293
|
+
if (mrtr === undefined) return;
|
|
2294
|
+
mrtr.outgoing_state = state;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* Build and validate an elicitation request sent to the client.
|
|
2299
|
+
* @template {StandardSchema extends undefined ? never : StandardSchema} TSchema
|
|
2300
|
+
* @param {string} message
|
|
2301
|
+
* @param {TSchema | string} schema_or_url
|
|
2302
|
+
*/
|
|
2303
|
+
async #create_elicitation_request(message, schema_or_url) {
|
|
2304
|
+
const request =
|
|
2305
|
+
typeof schema_or_url === 'string'
|
|
2306
|
+
? {
|
|
2307
|
+
method: /** @type {const} */ ('elicitation/create'),
|
|
2308
|
+
params: {
|
|
2309
|
+
mode: /** @type {const} */ ('url'),
|
|
2310
|
+
message,
|
|
2311
|
+
url: schema_or_url,
|
|
2312
|
+
},
|
|
2313
|
+
}
|
|
2314
|
+
: {
|
|
2315
|
+
method: /** @type {const} */ ('elicitation/create'),
|
|
2316
|
+
params: {
|
|
2317
|
+
message,
|
|
2318
|
+
requestedSchema:
|
|
2319
|
+
await this.#options.adapter?.toJsonSchema(
|
|
2320
|
+
schema_or_url,
|
|
2321
|
+
),
|
|
2322
|
+
},
|
|
2323
|
+
};
|
|
2324
|
+
const parsed = v.safeParse(ElicitRequestSchema, request);
|
|
2325
|
+
if (!parsed.success) {
|
|
2326
|
+
if (typeof schema_or_url === 'string') {
|
|
2327
|
+
throw new McpError(
|
|
2328
|
+
-32602,
|
|
2329
|
+
`URL ${schema_or_url} is not a valid URL`,
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
throw new McpError(
|
|
2333
|
+
-32603,
|
|
2334
|
+
`Invalid elicitation schema: form elicitation requires a flat object containing only supported primitive fields (${JSON.stringify(parsed.issues)})`,
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
return parsed.output;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
/**
|
|
2341
|
+
* Ask the user to complete an interaction at a URL. The client will open
|
|
2342
|
+
* the URL out of band and return the user's action without form content.
|
|
2343
|
+
*
|
|
2344
|
+
* @overload
|
|
2345
|
+
* @param {string} message
|
|
2346
|
+
* @param {string} url
|
|
2347
|
+
* @param {{ key?: string }} [options] `key` names this question so tmcp can match its answer on a retry.
|
|
2348
|
+
* @returns {Promise<Omit<ElicitResult, 'content'>>}
|
|
2349
|
+
*/
|
|
1185
2350
|
/**
|
|
1186
2351
|
* Emit an elicitation request to the client. Elicitations are used to ask the user for input in a structured way, the client will show a UI to the user to fill the input.
|
|
1187
2352
|
* The schema should be a valid Standard Schema V1 schema and should be an Object with the properties you need.
|
|
@@ -1189,26 +2354,131 @@ export class McpServer {
|
|
|
1189
2354
|
*
|
|
1190
2355
|
* If the client doesn't support elicitation, it will throw an error.
|
|
1191
2356
|
*
|
|
2357
|
+
* When there is no open client session, tmcp returns the question to the
|
|
2358
|
+
* client and asks it to retry the original request with the answer. The
|
|
2359
|
+
* handler then starts again from the beginning, so its definition must set
|
|
2360
|
+
* `replayable: true`. Always await this call. If a surrounding `catch`
|
|
2361
|
+
* handles errors, use `isInputRequired()` and rethrow tmcp's private error.
|
|
2362
|
+
*
|
|
1192
2363
|
* @template {StandardSchema extends undefined ? never : StandardSchema} TSchema
|
|
2364
|
+
* @overload
|
|
1193
2365
|
* @param {string} message
|
|
1194
2366
|
* @param {TSchema} schema
|
|
2367
|
+
* @param {{ key?: string }} [options] `key` names this question so tmcp can match its answer on a retry. By default tmcp uses `"1"`, `"2"`, and so on. Set a name when the handler may ask different questions on different runs. When mixing named and numbered questions, use non-numeric names.
|
|
1195
2368
|
* @returns {Promise<ElicitResult & { content?: StandardSchemaV1.InferOutput<TSchema> }>}
|
|
1196
2369
|
*/
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
2370
|
+
/**
|
|
2371
|
+
* @template {StandardSchema extends undefined ? never : StandardSchema} TSchema
|
|
2372
|
+
* @param {string} message
|
|
2373
|
+
* @param {TSchema | string} schema_or_url
|
|
2374
|
+
* @param {{ key?: string }} [options]
|
|
2375
|
+
* @returns {Promise<Omit<ElicitResult, 'content'> | (ElicitResult & { content?: StandardSchemaV1.InferOutput<TSchema> })>}
|
|
2376
|
+
*/
|
|
2377
|
+
async elicitation(message, schema_or_url, options = {}) {
|
|
2378
|
+
const mode = typeof schema_or_url === 'string' ? 'url' : 'form';
|
|
2379
|
+
this.#assert_client_request_allowed(
|
|
2380
|
+
'elicitation',
|
|
2381
|
+
`${mode} mode elicitation`,
|
|
2382
|
+
mode,
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
const mrtr = this.#mrtr;
|
|
2386
|
+
if (mrtr !== undefined) {
|
|
2387
|
+
this.#assert_replayable(mrtr, 'elicitation');
|
|
2388
|
+
const key = this.#next_input_key(mrtr, options.key);
|
|
2389
|
+
const has_response =
|
|
2390
|
+
mrtr.input_responses !== undefined &&
|
|
2391
|
+
Object.hasOwn(mrtr.input_responses, key);
|
|
2392
|
+
if (has_response) {
|
|
2393
|
+
try {
|
|
2394
|
+
const provided = mrtr.input_responses?.[key];
|
|
2395
|
+
const parsed = v.safeParse(ElicitResultSchema, provided);
|
|
2396
|
+
if (!parsed.success) {
|
|
2397
|
+
throw new McpError(
|
|
2398
|
+
-32602,
|
|
2399
|
+
`Invalid input response for key "${key}": expected an elicitation result ({ action, content? })`,
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
const result =
|
|
2403
|
+
typeof schema_or_url !== 'string'
|
|
2404
|
+
? await this.#validate_elicit_result(
|
|
2405
|
+
parsed.output,
|
|
2406
|
+
schema_or_url,
|
|
2407
|
+
-32602,
|
|
2408
|
+
)
|
|
2409
|
+
: this.#without_elicit_content(parsed.output);
|
|
2410
|
+
mrtr.consumed_responses.set(
|
|
2411
|
+
key,
|
|
2412
|
+
typeof schema_or_url === 'string'
|
|
2413
|
+
? result
|
|
2414
|
+
: parsed.output,
|
|
2415
|
+
);
|
|
2416
|
+
return result;
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
this.#release_input_response(mrtr, key);
|
|
2419
|
+
throw error;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
const pending_request = this.#create_elicitation_request(
|
|
2423
|
+
message,
|
|
2424
|
+
schema_or_url,
|
|
2425
|
+
);
|
|
2426
|
+
mrtr.pending.set(key, pending_request);
|
|
2427
|
+
try {
|
|
2428
|
+
await pending_request;
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
if (mrtr.pending.get(key) === pending_request) {
|
|
2431
|
+
mrtr.pending.delete(key);
|
|
2432
|
+
mrtr.used_keys.delete(key);
|
|
2433
|
+
}
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
throw mrtr.signal;
|
|
2437
|
+
}
|
|
1200
2438
|
|
|
1201
2439
|
this.#lazyily_create_client();
|
|
2440
|
+
const request = await this.#create_elicitation_request(
|
|
2441
|
+
message,
|
|
2442
|
+
schema_or_url,
|
|
2443
|
+
);
|
|
1202
2444
|
const result = await this.#client?.request(
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
message,
|
|
1206
|
-
requestedSchema:
|
|
1207
|
-
await this.#options.adapter?.toJsonSchema(schema),
|
|
1208
|
-
},
|
|
2445
|
+
request.method,
|
|
2446
|
+
request.params,
|
|
1209
2447
|
'standalone',
|
|
1210
2448
|
);
|
|
1211
2449
|
const elicit_result = v.parse(ElicitResultSchema, result);
|
|
2450
|
+
return typeof schema_or_url !== 'string'
|
|
2451
|
+
? this.#validate_elicit_result(elicit_result, schema_or_url)
|
|
2452
|
+
: this.#without_elicit_content(elicit_result);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* URL elicitation is out of band, so form content must never reach the
|
|
2457
|
+
* handler or be carried into a later stateless retry.
|
|
2458
|
+
* @param {ElicitResult} elicit_result
|
|
2459
|
+
* @returns {Omit<ElicitResult, 'content'>}
|
|
2460
|
+
*/
|
|
2461
|
+
#without_elicit_content(elicit_result) {
|
|
2462
|
+
const result = { ...elicit_result };
|
|
2463
|
+
delete result.content;
|
|
2464
|
+
return result;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Validate the `content` of an elicit result against the schema the
|
|
2469
|
+
* input was requested with.
|
|
2470
|
+
* @template {StandardSchema extends undefined ? never : StandardSchema} TSchema
|
|
2471
|
+
* @param {ElicitResult} elicit_result
|
|
2472
|
+
* @param {TSchema} schema
|
|
2473
|
+
* @param {number} [invalid_content_code]
|
|
2474
|
+
* @returns {Promise<ElicitResult & { content?: StandardSchemaV1.InferOutput<TSchema> }>}
|
|
2475
|
+
*/
|
|
2476
|
+
async #validate_elicit_result(
|
|
2477
|
+
elicit_result,
|
|
2478
|
+
schema,
|
|
2479
|
+
invalid_content_code = -32603,
|
|
2480
|
+
) {
|
|
2481
|
+
if (elicit_result.action !== 'accept') return elicit_result;
|
|
1212
2482
|
let validated_result = schema['~standard'].validate(
|
|
1213
2483
|
elicit_result.content,
|
|
1214
2484
|
);
|
|
@@ -1216,7 +2486,7 @@ export class McpServer {
|
|
|
1216
2486
|
validated_result = await validated_result;
|
|
1217
2487
|
if (validated_result.issues) {
|
|
1218
2488
|
throw new McpError(
|
|
1219
|
-
|
|
2489
|
+
invalid_content_code,
|
|
1220
2490
|
`Invalid elicitation result: ${JSON.stringify(validated_result.issues)}`,
|
|
1221
2491
|
);
|
|
1222
2492
|
}
|
|
@@ -1224,15 +2494,19 @@ export class McpServer {
|
|
|
1224
2494
|
}
|
|
1225
2495
|
|
|
1226
2496
|
/**
|
|
1227
|
-
* Request language model sampling from the client
|
|
2497
|
+
* Request language model sampling from the client.
|
|
2498
|
+
*
|
|
2499
|
+
* When there is no open client session, tmcp returns the sampling request
|
|
2500
|
+
* to the client and asks it to retry the original request with the answer.
|
|
2501
|
+
* The handler then starts again from the beginning, so its definition must
|
|
2502
|
+
* set `replayable: true`. Always await this call. If a surrounding `catch`
|
|
2503
|
+
* handles errors, use `isInputRequired()` and rethrow tmcp's private error.
|
|
1228
2504
|
* @param {CreateMessageRequestParams} request
|
|
2505
|
+
* @param {{ key?: string }} [options] `key` names this request so tmcp can match its answer on a retry. By default tmcp uses `"1"`, `"2"`, and so on. Set a name when the handler may make different requests on different runs. When mixing named and numbered requests, use non-numeric names.
|
|
1229
2506
|
* @returns {Promise<CreateMessageResult>}
|
|
1230
2507
|
*/
|
|
1231
|
-
async message(request) {
|
|
1232
|
-
|
|
1233
|
-
throw new McpError(-32601, "Client doesn't support sampling");
|
|
1234
|
-
|
|
1235
|
-
this.#lazyily_create_client();
|
|
2508
|
+
async message(request, options = {}) {
|
|
2509
|
+
this.#assert_client_request_allowed('sampling', 'sampling');
|
|
1236
2510
|
|
|
1237
2511
|
// Validate the request
|
|
1238
2512
|
const validated_request = v.parse(
|
|
@@ -1240,6 +2514,45 @@ export class McpServer {
|
|
|
1240
2514
|
request,
|
|
1241
2515
|
);
|
|
1242
2516
|
|
|
2517
|
+
const mrtr = this.#mrtr;
|
|
2518
|
+
if (mrtr !== undefined) {
|
|
2519
|
+
this.#assert_replayable(mrtr, 'sampling');
|
|
2520
|
+
const key = this.#next_input_key(mrtr, options.key);
|
|
2521
|
+
const has_response =
|
|
2522
|
+
mrtr.input_responses !== undefined &&
|
|
2523
|
+
Object.hasOwn(mrtr.input_responses, key);
|
|
2524
|
+
if (has_response) {
|
|
2525
|
+
try {
|
|
2526
|
+
const provided = mrtr.input_responses?.[key];
|
|
2527
|
+
const parsed = v.safeParse(
|
|
2528
|
+
CreateMessageResultSchema,
|
|
2529
|
+
provided,
|
|
2530
|
+
);
|
|
2531
|
+
if (!parsed.success) {
|
|
2532
|
+
throw new McpError(
|
|
2533
|
+
-32602,
|
|
2534
|
+
`Invalid input response for key "${key}": expected a sampling result (CreateMessageResult)`,
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
mrtr.consumed_responses.set(key, parsed.output);
|
|
2538
|
+
return parsed.output;
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
this.#release_input_response(mrtr, key);
|
|
2541
|
+
throw error;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
mrtr.pending.set(
|
|
2545
|
+
key,
|
|
2546
|
+
Promise.resolve({
|
|
2547
|
+
method: 'sampling/createMessage',
|
|
2548
|
+
params: validated_request,
|
|
2549
|
+
}),
|
|
2550
|
+
);
|
|
2551
|
+
throw mrtr.signal;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
this.#lazyily_create_client();
|
|
2555
|
+
|
|
1243
2556
|
// Make the request to the client
|
|
1244
2557
|
const response = await this.#client?.request(
|
|
1245
2558
|
'sampling/createMessage',
|
|
@@ -1284,10 +2597,12 @@ export class McpServer {
|
|
|
1284
2597
|
);
|
|
1285
2598
|
}
|
|
1286
2599
|
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
2600
|
+
const store = this.#ctx_storage.getStore();
|
|
2601
|
+
const current_session_level = store?.stateless
|
|
2602
|
+
? store.sessionInfo?.logLevel
|
|
2603
|
+
: (store?.sessionInfo?.logLevel ??
|
|
2604
|
+
this.#options.logging?.default ??
|
|
2605
|
+
'info');
|
|
1291
2606
|
|
|
1292
2607
|
if (
|
|
1293
2608
|
current_session_level &&
|