toolcraft 0.0.5 → 0.0.7

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.
Files changed (149) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +77 -59
  4. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
  5. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
  6. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
  7. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
  8. package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
  9. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
  10. package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
  11. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
  20. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
  21. package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
  24. package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
  25. package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
  27. package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
  28. package/node_modules/@poe-code/agent-defs/package.json +20 -0
  29. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
  30. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
  31. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
  32. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
  33. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
  34. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
  35. package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
  38. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
  42. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
  43. package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
  44. package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
  45. package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
  46. package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
  47. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
  48. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
  49. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
  50. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
  51. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
  52. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
  53. package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
  54. package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
  55. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
  56. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
  57. package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
  58. package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
  59. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
  60. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
  61. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
  62. package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
  63. package/node_modules/@poe-code/config-mutations/package.json +33 -0
  64. package/node_modules/@poe-code/file-lock/README.md +52 -0
  65. package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
  66. package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
  67. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
  68. package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
  69. package/node_modules/@poe-code/file-lock/package.json +23 -0
  70. package/node_modules/auth-store/README.md +47 -0
  71. package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
  72. package/node_modules/auth-store/dist/create-secret-store.js +35 -0
  73. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
  74. package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
  75. package/node_modules/auth-store/dist/index.d.ts +7 -0
  76. package/node_modules/auth-store/dist/index.js +4 -0
  77. package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
  78. package/node_modules/auth-store/dist/keychain-store.js +111 -0
  79. package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
  80. package/node_modules/auth-store/dist/provider-store.js +28 -0
  81. package/node_modules/auth-store/dist/types.d.ts +20 -0
  82. package/node_modules/auth-store/dist/types.js +1 -0
  83. package/node_modules/auth-store/package.json +25 -0
  84. package/node_modules/mcp-oauth/README.md +31 -0
  85. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
  86. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
  87. package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
  88. package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
  89. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
  90. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
  91. package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
  92. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
  93. package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
  94. package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
  95. package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
  96. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
  97. package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
  98. package/node_modules/mcp-oauth/dist/client/types.js +1 -0
  99. package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
  100. package/node_modules/mcp-oauth/dist/index.js +7 -0
  101. package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
  102. package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
  103. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
  104. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
  105. package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
  106. package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
  107. package/node_modules/mcp-oauth/package.json +31 -0
  108. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
  109. package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
  110. package/node_modules/tiny-mcp-client/dist/index.js +1 -0
  111. package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
  112. package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
  113. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
  114. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
  115. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
  116. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
  117. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
  118. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
  119. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
  120. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
  121. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
  122. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
  123. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
  124. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
  125. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
  126. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
  127. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
  128. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
  129. package/node_modules/tiny-mcp-client/package.json +22 -0
  130. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
  131. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
  132. package/node_modules/tiny-mcp-client/src/index.ts +94 -0
  133. package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
  134. package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
  135. package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
  136. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
  137. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
  138. package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
  139. package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
  140. package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
  141. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
  142. package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
  144. package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
  145. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
  146. package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
  147. package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
  148. package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
  149. package/package.json +24 -11
@@ -0,0 +1,3566 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { ChildProcessWithoutNullStreams, SpawnOptions } from "node:child_process";
3
+ import { PassThrough } from "node:stream";
4
+ import type { Readable, Writable } from "node:stream";
5
+ import type { JSONRPCMessage as SdkJsonRpcMessage } from "@modelcontextprotocol/sdk/types.js";
6
+ import {
7
+ createOAuthClientProvider,
8
+ OAuthError,
9
+ type OAuthClientProvider,
10
+ type OAuthClientProviderOptions,
11
+ } from "mcp-oauth";
12
+ import type { Server as TinyStdioMcpServer } from "tiny-stdio-mcp-server";
13
+ import {
14
+ OAuthMetadataDiscovery,
15
+ parseBearerWwwAuthenticateHeader,
16
+ } from "./oauth-discovery.js";
17
+ import type {
18
+ OAuthDiscoveryCache,
19
+ } from "./oauth-discovery.js";
20
+
21
+ export {
22
+ OAuthMetadataDiscovery,
23
+ discoverOAuthMetadata,
24
+ parseBearerWwwAuthenticateHeader,
25
+ resolveAuthorizationServerMetadataUrl,
26
+ resolveProtectedResourceMetadataUrl,
27
+ } from "./oauth-discovery.js";
28
+ export {
29
+ createAuthStoreSessionStore,
30
+ createDefaultOAuthClientProvider,
31
+ } from "mcp-oauth";
32
+ export type {
33
+ OAuthDiscoveryCache,
34
+ } from "./oauth-discovery.js";
35
+ export type { OAuthAuthorizationServerMetadata, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthUnauthorizedChallenge } from "./oauth-discovery.js";
36
+ export type {
37
+ DefaultOAuthClientProviderOptions,
38
+ OAuthClientProvider,
39
+ OAuthClientProviderOptions,
40
+ OAuthSessionStore,
41
+ StoredOAuthSession,
42
+ } from "mcp-oauth";
43
+
44
+ export type RequestId = number | string;
45
+
46
+ export interface Implementation {
47
+ name: string;
48
+ version: string;
49
+ }
50
+
51
+ export interface ClientCapabilities {
52
+ roots?: {
53
+ listChanged?: boolean;
54
+ [key: string]: unknown;
55
+ };
56
+ sampling?: {
57
+ [key: string]: unknown;
58
+ };
59
+ experimental?: Record<string, unknown>;
60
+ }
61
+
62
+ export interface ServerCapabilities {
63
+ prompts?: {
64
+ listChanged?: boolean;
65
+ [key: string]: unknown;
66
+ };
67
+ resources?: {
68
+ subscribe?: boolean;
69
+ listChanged?: boolean;
70
+ [key: string]: unknown;
71
+ };
72
+ tools?: {
73
+ listChanged?: boolean;
74
+ [key: string]: unknown;
75
+ };
76
+ logging?: {
77
+ [key: string]: unknown;
78
+ };
79
+ completions?: {
80
+ [key: string]: unknown;
81
+ };
82
+ experimental?: Record<string, unknown>;
83
+ }
84
+
85
+ export interface InitializeParams {
86
+ protocolVersion: string;
87
+ capabilities: ClientCapabilities;
88
+ clientInfo: Implementation;
89
+ }
90
+
91
+ export interface InitializeResult {
92
+ protocolVersion: string;
93
+ capabilities: ServerCapabilities;
94
+ serverInfo: Implementation;
95
+ instructions?: string;
96
+ }
97
+
98
+ export interface McpClientOptions {
99
+ clientInfo: Implementation;
100
+ capabilities?: ClientCapabilities;
101
+ onToolsChanged?: () => void | Promise<void>;
102
+ onResourcesChanged?: () => void | Promise<void>;
103
+ onResourceUpdated?: (uri: string) => void | Promise<void>;
104
+ onPromptsChanged?: () => void | Promise<void>;
105
+ onLog?: (message: LogMessage) => void | Promise<void>;
106
+ onProgress?: (params: ProgressParams) => void | Promise<void>;
107
+ onSamplingRequest?: (
108
+ params: CreateMessageParams
109
+ ) => CreateMessageResult | Promise<CreateMessageResult>;
110
+ onRootsList?: () => Root[] | Promise<Root[]>;
111
+ }
112
+
113
+ const MCP_PROTOCOL_VERSION = "2025-03-26";
114
+
115
+ export class McpClient {
116
+ private currentState: "disconnected" | "initializing" | "ready" | "closed" = "disconnected";
117
+ private currentServerCapabilities: ServerCapabilities | null = null;
118
+ private currentServerInfo: Implementation | null = null;
119
+ private currentInstructions: string | undefined;
120
+ private readonly options: McpClientOptions;
121
+ private transport: McpTransport | null = null;
122
+ private messageLayer: JsonRpcMessageLayer | null = null;
123
+
124
+ constructor(options: McpClientOptions) {
125
+ this.options = options;
126
+ }
127
+
128
+ get state(): "disconnected" | "initializing" | "ready" | "closed" {
129
+ return this.currentState;
130
+ }
131
+
132
+ get serverCapabilities(): ServerCapabilities | null {
133
+ return this.currentServerCapabilities;
134
+ }
135
+
136
+ get serverInfo(): Implementation | null {
137
+ return this.currentServerInfo;
138
+ }
139
+
140
+ get instructions(): string | undefined {
141
+ return this.currentInstructions;
142
+ }
143
+
144
+ private getMessageLayerOrThrow(): JsonRpcMessageLayer {
145
+ if (this.currentState === "disconnected") {
146
+ throw new Error("MCP client is disconnected");
147
+ }
148
+
149
+ if (this.currentState === "closed") {
150
+ throw new Error("MCP client is closed");
151
+ }
152
+
153
+ if (this.messageLayer === null) {
154
+ throw new Error("MCP client is disconnected");
155
+ }
156
+
157
+ return this.messageLayer;
158
+ }
159
+
160
+ async connect(transport: McpTransport): Promise<InitializeResult> {
161
+ if (this.currentState !== "disconnected" && this.currentState !== "closed") {
162
+ throw new Error("MCP client is already connected");
163
+ }
164
+
165
+ const transportClosedReason = transport.closed
166
+ .then((closedEvent) => closedEvent.reason)
167
+ .catch((error: unknown) =>
168
+ error instanceof Error ? error : new Error(String(error))
169
+ );
170
+ const messageLayer = new JsonRpcMessageLayer(
171
+ transport.readable,
172
+ transport.writable,
173
+ 30_000,
174
+ transportClosedReason
175
+ );
176
+ const {
177
+ onSamplingRequest,
178
+ onRootsList,
179
+ onToolsChanged,
180
+ onResourcesChanged,
181
+ onResourceUpdated,
182
+ onPromptsChanged,
183
+ onLog,
184
+ onProgress,
185
+ } = this.options;
186
+
187
+ messageLayer.onRequest("ping", () => ({}));
188
+
189
+ if (onSamplingRequest !== undefined) {
190
+ messageLayer.onRequest("sampling/createMessage", (params) =>
191
+ onSamplingRequest(params as CreateMessageParams)
192
+ );
193
+ }
194
+
195
+ if (onRootsList !== undefined) {
196
+ messageLayer.onRequest("roots/list", async () => ({
197
+ roots: await onRootsList(),
198
+ }));
199
+ }
200
+
201
+ messageLayer.onNotification("notifications/tools/list_changed", async () => {
202
+ if (onToolsChanged === undefined) {
203
+ return;
204
+ }
205
+
206
+ await onToolsChanged();
207
+ });
208
+ messageLayer.onNotification("notifications/resources/list_changed", async () => {
209
+ if (onResourcesChanged === undefined) {
210
+ return;
211
+ }
212
+
213
+ await onResourcesChanged();
214
+ });
215
+ messageLayer.onNotification("notifications/resources/updated", async (params) => {
216
+ if (onResourceUpdated === undefined) {
217
+ return;
218
+ }
219
+
220
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
221
+ return;
222
+ }
223
+
224
+ const { uri } = params as { uri?: unknown };
225
+ if (typeof uri !== "string") {
226
+ return;
227
+ }
228
+
229
+ await onResourceUpdated(uri);
230
+ });
231
+ messageLayer.onNotification("notifications/prompts/list_changed", async () => {
232
+ if (onPromptsChanged === undefined) {
233
+ return;
234
+ }
235
+
236
+ await onPromptsChanged();
237
+ });
238
+ messageLayer.onNotification("notifications/message", async (params) => {
239
+ if (onLog === undefined || !isObjectRecord(params) || !isLogLevel(params.level)) {
240
+ return;
241
+ }
242
+
243
+ if (!hasOwn(params, "data")) {
244
+ return;
245
+ }
246
+
247
+ const message: LogMessage = {
248
+ level: params.level,
249
+ data: params.data,
250
+ };
251
+
252
+ if (params.logger !== undefined) {
253
+ if (typeof params.logger !== "string") {
254
+ return;
255
+ }
256
+
257
+ message.logger = params.logger;
258
+ }
259
+
260
+ await onLog(message);
261
+ });
262
+ messageLayer.onNotification("notifications/progress", async (params) => {
263
+ if (onProgress === undefined || !isObjectRecord(params)) {
264
+ return;
265
+ }
266
+
267
+ const { progressToken, progress } = params;
268
+ if (!isRequestId(progressToken) || typeof progress !== "number") {
269
+ return;
270
+ }
271
+
272
+ const progressParams: ProgressParams = {
273
+ progressToken,
274
+ progress,
275
+ };
276
+
277
+ if (params.total !== undefined) {
278
+ if (typeof params.total !== "number") {
279
+ return;
280
+ }
281
+
282
+ progressParams.total = params.total;
283
+ }
284
+
285
+ if (params.message !== undefined) {
286
+ if (typeof params.message !== "string") {
287
+ return;
288
+ }
289
+
290
+ progressParams.message = params.message;
291
+ }
292
+
293
+ await onProgress(progressParams);
294
+ });
295
+ messageLayer.onNotification("notifications/cancelled", () => undefined);
296
+
297
+ this.transport = transport;
298
+ this.messageLayer = messageLayer;
299
+ this.currentState = "initializing";
300
+ transport.closed
301
+ .then((closedEvent) => {
302
+ if (this.transport !== transport) {
303
+ return;
304
+ }
305
+
306
+ this.messageLayer?.dispose(closedEvent.reason);
307
+ this.messageLayer = null;
308
+ this.transport = null;
309
+ this.currentState = "closed";
310
+ })
311
+ .catch((error: unknown) => {
312
+ if (this.transport !== transport) {
313
+ return;
314
+ }
315
+
316
+ const reason = error instanceof Error ? error : new Error(String(error));
317
+ this.messageLayer?.dispose(reason);
318
+ this.messageLayer = null;
319
+ this.transport = null;
320
+ this.currentState = "closed";
321
+ });
322
+
323
+ const capabilities: ClientCapabilities = {
324
+ ...(this.options.capabilities ?? {}),
325
+ };
326
+
327
+ if (onSamplingRequest !== undefined && capabilities.sampling === undefined) {
328
+ capabilities.sampling = {};
329
+ }
330
+
331
+ if (onRootsList !== undefined) {
332
+ capabilities.roots = {
333
+ ...(capabilities.roots ?? {}),
334
+ };
335
+ }
336
+
337
+ const initializeResult = (await messageLayer.sendRequest("initialize", {
338
+ protocolVersion: MCP_PROTOCOL_VERSION,
339
+ clientInfo: this.options.clientInfo,
340
+ capabilities,
341
+ })) as InitializeResult;
342
+
343
+ if (initializeResult.protocolVersion !== MCP_PROTOCOL_VERSION) {
344
+ throw new McpError(
345
+ ERROR_INVALID_REQUEST,
346
+ `Unsupported protocol version: ${initializeResult.protocolVersion}`
347
+ );
348
+ }
349
+
350
+ this.currentServerCapabilities = initializeResult.capabilities;
351
+ this.currentServerInfo = initializeResult.serverInfo;
352
+ this.currentInstructions = initializeResult.instructions;
353
+ messageLayer.sendNotification("notifications/initialized");
354
+ this.currentState = "ready";
355
+
356
+ return initializeResult;
357
+ }
358
+
359
+ private getServerCapabilitiesOrThrow(): ServerCapabilities {
360
+ if (this.currentServerCapabilities === null) {
361
+ throw new Error("MCP client has not completed initialization");
362
+ }
363
+
364
+ return this.currentServerCapabilities;
365
+ }
366
+
367
+ async listTools(params: PaginatedParams = {}): Promise<{ tools: Tool[]; nextCursor?: string }> {
368
+ const messageLayer = this.getMessageLayerOrThrow();
369
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
370
+ if (serverCapabilities.tools === undefined) {
371
+ throw new Error("Server does not support tools");
372
+ }
373
+
374
+ const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
375
+ return (await messageLayer.sendRequest("tools/list", requestParams)) as {
376
+ tools: Tool[];
377
+ nextCursor?: string;
378
+ };
379
+ }
380
+
381
+ async callTool(params: CallToolParams, options: CallToolOptions = {}): Promise<CallToolResult> {
382
+ const messageLayer = this.getMessageLayerOrThrow();
383
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
384
+ if (serverCapabilities.tools === undefined) {
385
+ throw new Error("Server does not support tools");
386
+ }
387
+
388
+ const requestParams =
389
+ options.progressToken === undefined
390
+ ? params
391
+ : {
392
+ ...params,
393
+ _meta: {
394
+ progressToken: options.progressToken,
395
+ },
396
+ };
397
+
398
+ let requestId: RequestId | undefined;
399
+ const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
400
+ onRequestId: (nextRequestId) => {
401
+ requestId = nextRequestId;
402
+ },
403
+ }) as Promise<CallToolResult>;
404
+ if (options.signal === undefined) {
405
+ return await requestPromise;
406
+ }
407
+ const signal = options.signal;
408
+
409
+ let abortListener: (() => void) | undefined;
410
+ const abortPromise = new Promise<CallToolResult>((_, reject) => {
411
+ const rejectWithAbortReason = () => {
412
+ if (requestId !== undefined) {
413
+ messageLayer.sendNotification("notifications/cancelled", { requestId });
414
+ }
415
+ reject(signal.reason);
416
+ };
417
+
418
+ abortListener = rejectWithAbortReason;
419
+ signal.addEventListener("abort", abortListener, { once: true });
420
+ if (signal.aborted) {
421
+ signal.removeEventListener("abort", abortListener);
422
+ rejectWithAbortReason();
423
+ }
424
+ });
425
+
426
+ try {
427
+ return (await Promise.race([requestPromise, abortPromise])) as CallToolResult;
428
+ } finally {
429
+ if (abortListener !== undefined) {
430
+ signal.removeEventListener("abort", abortListener);
431
+ }
432
+ }
433
+ }
434
+
435
+ async listResources(
436
+ params: PaginatedParams = {}
437
+ ): Promise<{ resources: Resource[]; nextCursor?: string }> {
438
+ const messageLayer = this.getMessageLayerOrThrow();
439
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
440
+ if (serverCapabilities.resources === undefined) {
441
+ throw new Error("Server does not support resources");
442
+ }
443
+
444
+ const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
445
+ return (await messageLayer.sendRequest("resources/list", requestParams)) as {
446
+ resources: Resource[];
447
+ nextCursor?: string;
448
+ };
449
+ }
450
+
451
+ async listResourceTemplates(
452
+ params: PaginatedParams = {}
453
+ ): Promise<{ resourceTemplates: ResourceTemplate[]; nextCursor?: string }> {
454
+ const messageLayer = this.getMessageLayerOrThrow();
455
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
456
+ if (serverCapabilities.resources === undefined) {
457
+ throw new Error("Server does not support resources");
458
+ }
459
+
460
+ const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
461
+ return (await messageLayer.sendRequest("resources/templates/list", requestParams)) as {
462
+ resourceTemplates: ResourceTemplate[];
463
+ nextCursor?: string;
464
+ };
465
+ }
466
+
467
+ async readResource(params: ReadResourceParams): Promise<{ contents: ResourceContents[] }> {
468
+ const messageLayer = this.getMessageLayerOrThrow();
469
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
470
+ if (serverCapabilities.resources === undefined) {
471
+ throw new Error("Server does not support resources");
472
+ }
473
+
474
+ return (await messageLayer.sendRequest("resources/read", params)) as {
475
+ contents: ResourceContents[];
476
+ };
477
+ }
478
+
479
+ async subscribe(uri: string): Promise<void> {
480
+ const messageLayer = this.getMessageLayerOrThrow();
481
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
482
+ if (serverCapabilities.resources?.subscribe !== true) {
483
+ throw new Error("Server does not support resource subscriptions");
484
+ }
485
+
486
+ await messageLayer.sendRequest("resources/subscribe", { uri });
487
+ }
488
+
489
+ async unsubscribe(uri: string): Promise<void> {
490
+ const messageLayer = this.getMessageLayerOrThrow();
491
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
492
+ if (serverCapabilities.resources?.subscribe !== true) {
493
+ throw new Error("Server does not support resource subscriptions");
494
+ }
495
+
496
+ await messageLayer.sendRequest("resources/unsubscribe", { uri });
497
+ }
498
+
499
+ async listPrompts(params: PaginatedParams = {}): Promise<{ prompts: Prompt[]; nextCursor?: string }> {
500
+ const messageLayer = this.getMessageLayerOrThrow();
501
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
502
+ if (serverCapabilities.prompts === undefined) {
503
+ throw new Error("Server does not support prompts");
504
+ }
505
+
506
+ const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
507
+ return (await messageLayer.sendRequest("prompts/list", requestParams)) as {
508
+ prompts: Prompt[];
509
+ nextCursor?: string;
510
+ };
511
+ }
512
+
513
+ async getPrompt(params: GetPromptParams): Promise<GetPromptResult> {
514
+ const messageLayer = this.getMessageLayerOrThrow();
515
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
516
+ if (serverCapabilities.prompts === undefined) {
517
+ throw new Error("Server does not support prompts");
518
+ }
519
+
520
+ return (await messageLayer.sendRequest("prompts/get", params)) as GetPromptResult;
521
+ }
522
+
523
+ async complete(params: CompleteParams): Promise<CompleteResult> {
524
+ const messageLayer = this.getMessageLayerOrThrow();
525
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
526
+ if (serverCapabilities.completions === undefined) {
527
+ throw new Error("Server does not support completions");
528
+ }
529
+
530
+ return (await messageLayer.sendRequest("completion/complete", params)) as CompleteResult;
531
+ }
532
+
533
+ async setLogLevel(level: LogLevel): Promise<void> {
534
+ const messageLayer = this.getMessageLayerOrThrow();
535
+ const serverCapabilities = this.getServerCapabilitiesOrThrow();
536
+ if (serverCapabilities.logging === undefined) {
537
+ throw new Error("Server does not support logging");
538
+ }
539
+
540
+ await messageLayer.sendRequest("logging/setLevel", { level });
541
+ }
542
+
543
+ async cancel(requestId: RequestId, reason?: string): Promise<void> {
544
+ const messageLayer = this.getMessageLayerOrThrow();
545
+ const params: { requestId: RequestId; reason?: string } = { requestId };
546
+
547
+ if (reason !== undefined) {
548
+ params.reason = reason;
549
+ }
550
+
551
+ messageLayer.sendNotification("notifications/cancelled", params);
552
+ }
553
+
554
+ async sendRootsChanged(): Promise<void> {
555
+ const messageLayer = this.getMessageLayerOrThrow();
556
+ messageLayer.sendNotification("notifications/roots/list_changed");
557
+ }
558
+
559
+ async ping(): Promise<void> {
560
+ const messageLayer = this.getMessageLayerOrThrow();
561
+ await messageLayer.sendRequest("ping");
562
+ }
563
+
564
+ async close(): Promise<void> {
565
+ if (this.currentState === "closed") {
566
+ return;
567
+ }
568
+
569
+ const closeError = new Error("MCP client closed");
570
+ this.messageLayer?.dispose(closeError);
571
+ this.transport?.dispose(closeError);
572
+ this.messageLayer = null;
573
+ this.transport = null;
574
+ this.currentState = "closed";
575
+ }
576
+ }
577
+
578
+ export interface Tool {
579
+ name: string;
580
+ description?: string;
581
+ inputSchema: Record<string, unknown>;
582
+ annotations?: ToolAnnotations;
583
+ }
584
+
585
+ export interface ToolAnnotations {
586
+ title?: string;
587
+ readOnlyHint?: boolean;
588
+ destructiveHint?: boolean;
589
+ idempotentHint?: boolean;
590
+ openWorldHint?: boolean;
591
+ }
592
+
593
+ export interface CallToolParams {
594
+ name: string;
595
+ arguments?: Record<string, unknown>;
596
+ }
597
+
598
+ export interface CallToolOptions {
599
+ signal?: AbortSignal;
600
+ progressToken?: ProgressToken;
601
+ }
602
+
603
+ export interface ReadResourceParams {
604
+ uri: string;
605
+ }
606
+
607
+ export interface Resource {
608
+ uri: string;
609
+ name: string;
610
+ description?: string;
611
+ mimeType?: string;
612
+ size?: number;
613
+ }
614
+
615
+ export interface ResourceTemplate {
616
+ uriTemplate: string;
617
+ name: string;
618
+ description?: string;
619
+ mimeType?: string;
620
+ }
621
+
622
+ export interface PaginatedParams {
623
+ cursor?: string;
624
+ }
625
+
626
+ export interface PaginatedResult {
627
+ nextCursor?: string;
628
+ }
629
+
630
+ export interface TextResourceContents {
631
+ uri: string;
632
+ mimeType?: string;
633
+ text: string;
634
+ }
635
+
636
+ export interface BlobResourceContents {
637
+ uri: string;
638
+ mimeType?: string;
639
+ blob: string;
640
+ }
641
+
642
+ export type ResourceContents = TextResourceContents | BlobResourceContents;
643
+
644
+ export interface TextContent {
645
+ type: "text";
646
+ text: string;
647
+ }
648
+
649
+ export interface ImageContent {
650
+ type: "image";
651
+ data: string;
652
+ mimeType: string;
653
+ }
654
+
655
+ export interface AudioContent {
656
+ type: "audio";
657
+ data: string;
658
+ mimeType: string;
659
+ }
660
+
661
+ export interface EmbeddedResource {
662
+ type: "resource";
663
+ resource: ResourceContents;
664
+ }
665
+
666
+ export type ContentItem = TextContent | ImageContent | AudioContent | EmbeddedResource;
667
+
668
+ export interface Prompt {
669
+ name: string;
670
+ description?: string;
671
+ arguments?: PromptArgument[];
672
+ }
673
+
674
+ export interface PromptArgument {
675
+ name: string;
676
+ description?: string;
677
+ required?: boolean;
678
+ }
679
+
680
+ export interface PromptMessage {
681
+ role: "user" | "assistant";
682
+ content: ContentItem;
683
+ }
684
+
685
+ export interface GetPromptResult {
686
+ description?: string;
687
+ messages: PromptMessage[];
688
+ }
689
+
690
+ export interface GetPromptParams {
691
+ name: string;
692
+ arguments?: Record<string, string>;
693
+ }
694
+
695
+ export interface CallToolResult {
696
+ content: ContentItem[];
697
+ isError?: boolean;
698
+ }
699
+
700
+ export interface Root {
701
+ uri: string;
702
+ name?: string;
703
+ }
704
+
705
+ export type LogLevel =
706
+ | "debug"
707
+ | "info"
708
+ | "notice"
709
+ | "warning"
710
+ | "error"
711
+ | "critical"
712
+ | "alert"
713
+ | "emergency";
714
+
715
+ export interface LogMessage {
716
+ level: LogLevel;
717
+ logger?: string;
718
+ data: unknown;
719
+ }
720
+
721
+ export type ProgressToken = RequestId;
722
+
723
+ export interface ProgressParams {
724
+ progressToken: ProgressToken;
725
+ progress: number;
726
+ total?: number;
727
+ message?: string;
728
+ }
729
+
730
+ export interface ModelHint {
731
+ name?: string;
732
+ }
733
+
734
+ export interface ModelPreferences {
735
+ hints?: ModelHint[];
736
+ costPriority?: number;
737
+ speedPriority?: number;
738
+ intelligencePriority?: number;
739
+ }
740
+
741
+ export interface SamplingMessage {
742
+ role: "user" | "assistant";
743
+ content: ContentItem | ContentItem[];
744
+ }
745
+
746
+ export type IncludeContext = "none" | "thisServer" | "allServers";
747
+
748
+ export interface CreateMessageParams {
749
+ messages: SamplingMessage[];
750
+ modelPreferences?: ModelPreferences;
751
+ systemPrompt?: string;
752
+ includeContext?: IncludeContext;
753
+ temperature?: number;
754
+ maxTokens: number;
755
+ stopSequences?: string[];
756
+ metadata?: Record<string, unknown>;
757
+ }
758
+
759
+ export interface CreateMessageResult {
760
+ model: string;
761
+ content: ContentItem | ContentItem[];
762
+ role: "user" | "assistant";
763
+ stopReason: string;
764
+ }
765
+
766
+ export interface PromptReference {
767
+ type: "ref/prompt";
768
+ name: string;
769
+ }
770
+
771
+ export interface ResourceReference {
772
+ type: "ref/resource";
773
+ uri: string;
774
+ }
775
+
776
+ export interface CompleteArgument {
777
+ name: string;
778
+ value: string;
779
+ }
780
+
781
+ export interface CompleteParams {
782
+ ref: PromptReference | ResourceReference;
783
+ argument: CompleteArgument;
784
+ }
785
+
786
+ export interface Completion {
787
+ values: string[];
788
+ hasMore?: boolean;
789
+ total?: number;
790
+ }
791
+
792
+ export interface CompleteResult {
793
+ completion: Completion;
794
+ }
795
+
796
+ export const ERROR_PARSE = -32700;
797
+ export const ERROR_INVALID_REQUEST = -32600;
798
+ export const ERROR_METHOD_NOT_FOUND = -32601;
799
+ export const ERROR_INVALID_PARAMS = -32602;
800
+ export const ERROR_INTERNAL = -32603;
801
+
802
+ export interface McpTransportClosedEvent {
803
+ reason: Error;
804
+ code?: number;
805
+ signal?: NodeJS.Signals;
806
+ }
807
+
808
+ export interface McpTransport {
809
+ readable: Readable;
810
+ writable: Writable;
811
+ closed: Promise<McpTransportClosedEvent>;
812
+ dispose(reason?: Error): void;
813
+ }
814
+
815
+ export interface InMemoryServerTransport {
816
+ readable: Readable;
817
+ writable: Writable;
818
+ }
819
+
820
+ export interface InMemoryTransportPair {
821
+ clientTransport: McpTransport;
822
+ serverTransport: InMemoryServerTransport;
823
+ }
824
+
825
+ export function createInMemoryTransportPair(): InMemoryTransportPair {
826
+ const clientToServer = new PassThrough();
827
+ const serverToClient = new PassThrough();
828
+ let disposed = false;
829
+ let resolveClosed:
830
+ | ((closedEvent: McpTransportClosedEvent) => void)
831
+ | undefined;
832
+
833
+ const closed = new Promise<McpTransportClosedEvent>((resolve) => {
834
+ resolveClosed = resolve;
835
+ });
836
+
837
+ const resolveClosedOnce = (reason: Error): void => {
838
+ if (resolveClosed === undefined) {
839
+ return;
840
+ }
841
+
842
+ const currentResolve = resolveClosed;
843
+ resolveClosed = undefined;
844
+ currentResolve({ reason });
845
+ };
846
+
847
+ const dispose = (reason = new Error("In-memory transport disposed")): void => {
848
+ if (disposed) {
849
+ return;
850
+ }
851
+
852
+ disposed = true;
853
+
854
+ if (!clientToServer.destroyed && !clientToServer.writableEnded) {
855
+ clientToServer.end();
856
+ }
857
+
858
+ if (!serverToClient.destroyed && !serverToClient.writableEnded) {
859
+ serverToClient.end();
860
+ }
861
+
862
+ resolveClosedOnce(reason);
863
+ };
864
+
865
+ clientToServer.once("error", (error) => {
866
+ dispose(error instanceof Error ? error : new Error(String(error)));
867
+ });
868
+ serverToClient.once("error", (error) => {
869
+ dispose(error instanceof Error ? error : new Error(String(error)));
870
+ });
871
+
872
+ return {
873
+ clientTransport: {
874
+ readable: serverToClient,
875
+ writable: clientToServer,
876
+ closed,
877
+ dispose,
878
+ },
879
+ serverTransport: {
880
+ readable: clientToServer,
881
+ writable: serverToClient,
882
+ },
883
+ };
884
+ }
885
+
886
+ interface SdkTransportAdapterInput {
887
+ onclose?: () => void;
888
+ onerror?: (error: Error) => void;
889
+ onmessage?: (message: SdkJsonRpcMessage, extra?: any) => void;
890
+ start(): Promise<void>;
891
+ close(): Promise<void>;
892
+ send(message: SdkJsonRpcMessage): Promise<void>;
893
+ }
894
+
895
+ export interface McpClientConnection {
896
+ connect(transport: McpTransport): Promise<void>;
897
+ close(): Promise<void>;
898
+ }
899
+
900
+ interface SdkServerConnection {
901
+ connect(transport: unknown): Promise<void>;
902
+ }
903
+
904
+ export interface SdkTestPair<TClient extends McpClientConnection> {
905
+ client: TClient;
906
+ cleanup: () => Promise<void>;
907
+ }
908
+
909
+ export class SdkTransportAdapter implements McpTransport {
910
+ readonly readable: Readable;
911
+ readonly writable: Writable;
912
+ readonly closed: Promise<McpTransportClosedEvent>;
913
+ private readonly sdkTransport: SdkTransportAdapterInput;
914
+ private readonly readStream = new PassThrough();
915
+ private readonly writeStream = new PassThrough();
916
+ private resolveClosed:
917
+ | ((closedEvent: McpTransportClosedEvent) => void)
918
+ | undefined;
919
+ private disposed = false;
920
+
921
+ constructor(sdkTransport: SdkTransportAdapterInput) {
922
+ this.sdkTransport = sdkTransport;
923
+ this.readable = this.readStream;
924
+ this.writable = this.writeStream;
925
+ this.closed = new Promise((resolve) => {
926
+ this.resolveClosed = resolve;
927
+ });
928
+
929
+ this.sdkTransport.onmessage = (message) => {
930
+ this.readStream.write(serializeJsonRpcMessage(message as JsonRpcMessage));
931
+ };
932
+ this.sdkTransport.onclose = () => {
933
+ this.dispose(new Error("SDK transport closed"));
934
+ };
935
+ this.sdkTransport.onerror = (error) => {
936
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
937
+ };
938
+
939
+ this.readStream.once("error", (error) => {
940
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
941
+ });
942
+ this.writeStream.once("error", (error) => {
943
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
944
+ });
945
+
946
+ this.consumeWrittenLines().catch((error) => {
947
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
948
+ });
949
+ this.sdkTransport.start().catch((error) => {
950
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
951
+ });
952
+ }
953
+
954
+ dispose(reason = new Error("SDK transport adapter disposed")): void {
955
+ if (this.disposed) {
956
+ return;
957
+ }
958
+
959
+ this.disposed = true;
960
+
961
+ if (!this.writeStream.destroyed && !this.writeStream.writableEnded) {
962
+ this.writeStream.end();
963
+ }
964
+
965
+ if (!this.readStream.destroyed && !this.readStream.writableEnded) {
966
+ this.readStream.end();
967
+ }
968
+
969
+ if (this.resolveClosed !== undefined) {
970
+ const resolveClosed = this.resolveClosed;
971
+ this.resolveClosed = undefined;
972
+ resolveClosed({ reason });
973
+ }
974
+
975
+ this.sdkTransport.close().catch(() => undefined);
976
+ }
977
+
978
+ private async consumeWrittenLines(): Promise<void> {
979
+ for await (const line of readLines(this.writeStream)) {
980
+ if (this.disposed || line.length === 0) {
981
+ continue;
982
+ }
983
+
984
+ let parsedMessage: unknown;
985
+ try {
986
+ parsedMessage = JSON.parse(line);
987
+ } catch {
988
+ throw new Error(`Malformed JSON line: ${line}`);
989
+ }
990
+
991
+ if (typeof parsedMessage !== "object" || parsedMessage === null || Array.isArray(parsedMessage)) {
992
+ throw new Error(`Malformed JSON line: ${line}`);
993
+ }
994
+
995
+ await this.sdkTransport.send(parsedMessage as SdkJsonRpcMessage);
996
+ }
997
+ }
998
+ }
999
+
1000
+ export async function createSdkTestPair<TClient extends McpClientConnection>(
1001
+ server: SdkServerConnection,
1002
+ createClient: () => TClient
1003
+ ): Promise<SdkTestPair<TClient>> {
1004
+ const { InMemoryTransport } = await import("@modelcontextprotocol/sdk/inMemory.js");
1005
+ const [clientSdkTransport, serverSdkTransport] = InMemoryTransport.createLinkedPair();
1006
+ const clientTransport = new SdkTransportAdapter(clientSdkTransport);
1007
+ const serverPromise = server.connect(serverSdkTransport);
1008
+ const client = createClient();
1009
+
1010
+ try {
1011
+ await client.connect(clientTransport);
1012
+ } catch (error) {
1013
+ clientTransport.dispose(new Error("SDK test pair setup failed"));
1014
+ await clientSdkTransport.close();
1015
+ await serverSdkTransport.close();
1016
+ await serverPromise;
1017
+ throw error;
1018
+ }
1019
+
1020
+ const cleanup = async (): Promise<void> => {
1021
+ await client.close();
1022
+ clientTransport.dispose(new Error("SDK test pair cleanup"));
1023
+ await clientSdkTransport.close();
1024
+ await serverSdkTransport.close();
1025
+ await serverPromise;
1026
+ };
1027
+
1028
+ return { client, cleanup };
1029
+ }
1030
+
1031
+ export async function createMockEchoToolServer(): Promise<SdkServerConnection> {
1032
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1033
+ await Promise.all([
1034
+ import("@modelcontextprotocol/sdk/server/index.js"),
1035
+ import("@modelcontextprotocol/sdk/types.js"),
1036
+ ]);
1037
+
1038
+ const server = new Server(
1039
+ { name: "mock-echo-tool-server", version: "1.0.0" },
1040
+ { capabilities: { tools: {} } }
1041
+ );
1042
+ const echoTool = {
1043
+ name: "echo",
1044
+ description: "Echoes the provided message.",
1045
+ inputSchema: {
1046
+ type: "object" as const,
1047
+ properties: {
1048
+ message: {
1049
+ type: "string",
1050
+ },
1051
+ },
1052
+ required: ["message"],
1053
+ },
1054
+ };
1055
+
1056
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1057
+ tools: [echoTool],
1058
+ }));
1059
+
1060
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1061
+ if (request.params.name !== "echo") {
1062
+ throw new Error(`Unknown tool: ${request.params.name}`);
1063
+ }
1064
+
1065
+ const message = request.params.arguments?.message;
1066
+ if (typeof message !== "string") {
1067
+ throw new Error("Echo tool requires a string message argument");
1068
+ }
1069
+
1070
+ return {
1071
+ content: [
1072
+ {
1073
+ type: "text" as const,
1074
+ text: message,
1075
+ },
1076
+ ],
1077
+ };
1078
+ });
1079
+
1080
+ return server;
1081
+ }
1082
+
1083
+ export async function createMockMultiToolServer(): Promise<SdkServerConnection> {
1084
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1085
+ await Promise.all([
1086
+ import("@modelcontextprotocol/sdk/server/index.js"),
1087
+ import("@modelcontextprotocol/sdk/types.js"),
1088
+ ]);
1089
+
1090
+ const server = new Server(
1091
+ { name: "mock-multi-tool-server", version: "1.0.0" },
1092
+ { capabilities: { tools: {} } }
1093
+ );
1094
+ const tools = [
1095
+ {
1096
+ name: "add",
1097
+ description: "Adds two numbers and returns the sum as text.",
1098
+ inputSchema: {
1099
+ type: "object" as const,
1100
+ properties: {
1101
+ a: { type: "number" },
1102
+ b: { type: "number" },
1103
+ },
1104
+ required: ["a", "b"],
1105
+ },
1106
+ },
1107
+ {
1108
+ name: "greet",
1109
+ description: "Greets a user with optional formal tone.",
1110
+ inputSchema: {
1111
+ type: "object" as const,
1112
+ properties: {
1113
+ name: { type: "string" },
1114
+ formal: { type: "boolean" },
1115
+ },
1116
+ required: ["name"],
1117
+ },
1118
+ },
1119
+ {
1120
+ name: "fail",
1121
+ description: "Returns an intentional tool error payload.",
1122
+ inputSchema: {
1123
+ type: "object" as const,
1124
+ properties: {},
1125
+ },
1126
+ },
1127
+ ];
1128
+
1129
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
1130
+
1131
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1132
+ if (request.params.name === "add") {
1133
+ const a = request.params.arguments?.a;
1134
+ const b = request.params.arguments?.b;
1135
+
1136
+ if (typeof a !== "number" || typeof b !== "number") {
1137
+ throw new Error("Add tool requires numeric a and b arguments");
1138
+ }
1139
+
1140
+ return {
1141
+ content: [
1142
+ {
1143
+ type: "text" as const,
1144
+ text: String(a + b),
1145
+ },
1146
+ ],
1147
+ };
1148
+ }
1149
+
1150
+ if (request.params.name === "greet") {
1151
+ const name = request.params.arguments?.name;
1152
+ const formal = request.params.arguments?.formal;
1153
+
1154
+ if (typeof name !== "string") {
1155
+ throw new Error("Greet tool requires a string name argument");
1156
+ }
1157
+ if (formal !== undefined && typeof formal !== "boolean") {
1158
+ throw new Error("Greet tool formal argument must be boolean when provided");
1159
+ }
1160
+
1161
+ return {
1162
+ content: [
1163
+ {
1164
+ type: "text" as const,
1165
+ text: formal ? `Good day, ${name}.` : `Hello, ${name}!`,
1166
+ },
1167
+ ],
1168
+ };
1169
+ }
1170
+
1171
+ if (request.params.name === "fail") {
1172
+ return {
1173
+ isError: true,
1174
+ content: [
1175
+ {
1176
+ type: "text" as const,
1177
+ text: "Intentional tool failure.",
1178
+ },
1179
+ ],
1180
+ };
1181
+ }
1182
+
1183
+ throw new Error(`Unknown tool: ${request.params.name}`);
1184
+ });
1185
+
1186
+ return server;
1187
+ }
1188
+
1189
+ export async function createMockPaginatedToolsServer(): Promise<SdkServerConnection> {
1190
+ const [{ Server }, { ListToolsRequestSchema }] = await Promise.all([
1191
+ import("@modelcontextprotocol/sdk/server/index.js"),
1192
+ import("@modelcontextprotocol/sdk/types.js"),
1193
+ ]);
1194
+
1195
+ const server = new Server(
1196
+ { name: "mock-paginated-tools-server", version: "1.0.0" },
1197
+ { capabilities: { tools: {} } }
1198
+ );
1199
+ const pageSize = 5;
1200
+ const tools = Array.from({ length: 20 }, (_, index) => ({
1201
+ name: `tool-${index + 1}`,
1202
+ description: `Mock paginated tool ${index + 1}.`,
1203
+ inputSchema: {
1204
+ type: "object" as const,
1205
+ properties: {},
1206
+ },
1207
+ }));
1208
+
1209
+ server.setRequestHandler(ListToolsRequestSchema, async (request) => {
1210
+ const cursor = request.params?.cursor;
1211
+ const startIndex = cursor === undefined ? 0 : Number(cursor);
1212
+
1213
+ if (
1214
+ !Number.isInteger(startIndex) ||
1215
+ startIndex < 0 ||
1216
+ startIndex > tools.length ||
1217
+ startIndex % pageSize !== 0
1218
+ ) {
1219
+ throw new Error(`Invalid cursor: ${String(cursor)}`);
1220
+ }
1221
+
1222
+ const pageTools = tools.slice(startIndex, startIndex + pageSize);
1223
+ const nextIndex = startIndex + pageSize;
1224
+ if (nextIndex >= tools.length) {
1225
+ return { tools: pageTools };
1226
+ }
1227
+
1228
+ return {
1229
+ tools: pageTools,
1230
+ nextCursor: String(nextIndex),
1231
+ };
1232
+ });
1233
+
1234
+ return server;
1235
+ }
1236
+
1237
+ export async function createMockResourceServer(): Promise<SdkServerConnection> {
1238
+ const [
1239
+ { Server },
1240
+ {
1241
+ ListResourcesRequestSchema,
1242
+ ListResourceTemplatesRequestSchema,
1243
+ ReadResourceRequestSchema,
1244
+ },
1245
+ ] = await Promise.all([
1246
+ import("@modelcontextprotocol/sdk/server/index.js"),
1247
+ import("@modelcontextprotocol/sdk/types.js"),
1248
+ ]);
1249
+
1250
+ const server = new Server(
1251
+ { name: "mock-resource-server", version: "1.0.0" },
1252
+ { capabilities: { resources: {} } }
1253
+ );
1254
+ const resources = [
1255
+ {
1256
+ uri: "file:///readme.txt",
1257
+ name: "readme.txt",
1258
+ mimeType: "text/plain",
1259
+ },
1260
+ {
1261
+ uri: "file:///image.png",
1262
+ name: "image.png",
1263
+ mimeType: "image/png",
1264
+ },
1265
+ ];
1266
+ const resourceContentsByUri = new Map<string, ResourceContents>([
1267
+ [
1268
+ "file:///readme.txt",
1269
+ {
1270
+ uri: "file:///readme.txt",
1271
+ mimeType: "text/plain",
1272
+ text: "This is a mock README resource.",
1273
+ },
1274
+ ],
1275
+ [
1276
+ "file:///image.png",
1277
+ {
1278
+ uri: "file:///image.png",
1279
+ mimeType: "image/png",
1280
+ blob: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8Xw8AAoMBgL9qj3QAAAAASUVORK5CYII=",
1281
+ },
1282
+ ],
1283
+ ]);
1284
+ const resourceTemplates = [
1285
+ {
1286
+ uriTemplate: "file:///{path}",
1287
+ name: "file-template",
1288
+ },
1289
+ ];
1290
+
1291
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1292
+ resources,
1293
+ }));
1294
+
1295
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1296
+ const resourceContent = resourceContentsByUri.get(request.params.uri);
1297
+ if (resourceContent === undefined) {
1298
+ throw new Error(`Unknown resource: ${request.params.uri}`);
1299
+ }
1300
+
1301
+ return {
1302
+ contents: [resourceContent],
1303
+ };
1304
+ });
1305
+
1306
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
1307
+ resourceTemplates,
1308
+ }));
1309
+
1310
+ return server;
1311
+ }
1312
+
1313
+ export async function createMockSubscribableResourceServer(): Promise<
1314
+ SdkServerConnection & {
1315
+ triggerResourceUpdated: (uri: string, updatedText?: string) => Promise<void>;
1316
+ triggerResourceListChanged: () => Promise<void>;
1317
+ }
1318
+ > {
1319
+ const [
1320
+ { Server },
1321
+ {
1322
+ ListResourcesRequestSchema,
1323
+ ReadResourceRequestSchema,
1324
+ SubscribeRequestSchema,
1325
+ UnsubscribeRequestSchema,
1326
+ },
1327
+ ] =
1328
+ await Promise.all([
1329
+ import("@modelcontextprotocol/sdk/server/index.js"),
1330
+ import("@modelcontextprotocol/sdk/types.js"),
1331
+ ]);
1332
+
1333
+ const server = new Server(
1334
+ { name: "mock-subscribable-resource-server", version: "1.0.0" },
1335
+ { capabilities: { resources: { subscribe: true, listChanged: true } } }
1336
+ );
1337
+ const resources = [
1338
+ {
1339
+ uri: "file:///readme.txt",
1340
+ name: "readme.txt",
1341
+ mimeType: "text/plain",
1342
+ },
1343
+ ];
1344
+ const resourceContentsByUri = new Map<string, ResourceContents>([
1345
+ [
1346
+ "file:///readme.txt",
1347
+ {
1348
+ uri: "file:///readme.txt",
1349
+ mimeType: "text/plain",
1350
+ text: "Initial subscribable resource text.",
1351
+ },
1352
+ ],
1353
+ ]);
1354
+ const subscribedUris = new Set<string>();
1355
+
1356
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1357
+ resources,
1358
+ }));
1359
+
1360
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1361
+ const resourceContent = resourceContentsByUri.get(request.params.uri);
1362
+ if (resourceContent === undefined) {
1363
+ throw new Error(`Unknown resource: ${request.params.uri}`);
1364
+ }
1365
+
1366
+ return {
1367
+ contents: [resourceContent],
1368
+ };
1369
+ });
1370
+
1371
+ server.setRequestHandler(SubscribeRequestSchema, async (request) => {
1372
+ subscribedUris.add(request.params.uri);
1373
+ return {};
1374
+ });
1375
+
1376
+ server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
1377
+ subscribedUris.delete(request.params.uri);
1378
+ return {};
1379
+ });
1380
+
1381
+ return Object.assign(server, {
1382
+ triggerResourceUpdated: async (uri: string, updatedText?: string): Promise<void> => {
1383
+ if (updatedText !== undefined) {
1384
+ const existingContent = resourceContentsByUri.get(uri);
1385
+ if (existingContent === undefined || "blob" in existingContent) {
1386
+ throw new Error(`Unknown text resource: ${uri}`);
1387
+ }
1388
+
1389
+ resourceContentsByUri.set(uri, {
1390
+ uri,
1391
+ mimeType: existingContent.mimeType,
1392
+ text: updatedText,
1393
+ });
1394
+ }
1395
+
1396
+ if (!subscribedUris.has(uri)) {
1397
+ return;
1398
+ }
1399
+
1400
+ await server.sendResourceUpdated({ uri });
1401
+ },
1402
+ triggerResourceListChanged: async (): Promise<void> => {
1403
+ await server.sendResourceListChanged();
1404
+ },
1405
+ });
1406
+ }
1407
+
1408
+ export async function createMockPromptServer(): Promise<SdkServerConnection> {
1409
+ const [
1410
+ { Server },
1411
+ { ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, McpError: SdkMcpError },
1412
+ ] =
1413
+ await Promise.all([
1414
+ import("@modelcontextprotocol/sdk/server/index.js"),
1415
+ import("@modelcontextprotocol/sdk/types.js"),
1416
+ ]);
1417
+
1418
+ const server = new Server(
1419
+ { name: "mock-prompt-server", version: "1.0.0" },
1420
+ { capabilities: { prompts: {} } }
1421
+ );
1422
+ const promptTemplates = [
1423
+ {
1424
+ prompt: {
1425
+ name: "code_review",
1426
+ description: "Review code for correctness and maintainability.",
1427
+ arguments: [
1428
+ {
1429
+ name: "code",
1430
+ description: "Code to review.",
1431
+ required: true,
1432
+ },
1433
+ ],
1434
+ },
1435
+ messages: [
1436
+ {
1437
+ role: "user" as const,
1438
+ textTemplate: "Please review the following code:\n{{code}}",
1439
+ },
1440
+ {
1441
+ role: "assistant" as const,
1442
+ textTemplate: "I will review the code for potential issues and improvements.",
1443
+ },
1444
+ ],
1445
+ },
1446
+ {
1447
+ prompt: {
1448
+ name: "summarize",
1449
+ description: "Summarize the provided text.",
1450
+ },
1451
+ messages: [
1452
+ {
1453
+ role: "user" as const,
1454
+ textTemplate: "Please summarize the provided text.",
1455
+ },
1456
+ ],
1457
+ },
1458
+ ];
1459
+ const promptsByName = new Map(promptTemplates.map((template) => [template.prompt.name, template]));
1460
+
1461
+ const renderPromptMessage = (
1462
+ textTemplate: string,
1463
+ argumentsMap: Record<string, unknown> | undefined
1464
+ ): string => {
1465
+ if (argumentsMap === undefined) {
1466
+ return textTemplate;
1467
+ }
1468
+
1469
+ let renderedText = textTemplate;
1470
+ for (const [name, value] of Object.entries(argumentsMap)) {
1471
+ if (typeof value !== "string") {
1472
+ continue;
1473
+ }
1474
+
1475
+ renderedText = renderedText.split(`{{${name}}}`).join(value);
1476
+ }
1477
+
1478
+ return renderedText;
1479
+ };
1480
+
1481
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
1482
+ prompts: promptTemplates.map((template) => template.prompt),
1483
+ }));
1484
+
1485
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1486
+ const template = promptsByName.get(request.params.name);
1487
+ if (template === undefined) {
1488
+ throw new SdkMcpError(ErrorCode.InvalidParams, `Unknown prompt: ${request.params.name}`);
1489
+ }
1490
+
1491
+ const requestArguments = request.params.arguments;
1492
+ for (const argument of template.prompt.arguments ?? []) {
1493
+ if (!argument.required) {
1494
+ continue;
1495
+ }
1496
+
1497
+ const value = requestArguments?.[argument.name];
1498
+ if (typeof value !== "string") {
1499
+ throw new SdkMcpError(
1500
+ ErrorCode.InvalidParams,
1501
+ `Missing required prompt argument: ${argument.name}`
1502
+ );
1503
+ }
1504
+ }
1505
+
1506
+ return {
1507
+ description: template.prompt.description,
1508
+ messages: template.messages.map((message) => ({
1509
+ role: message.role,
1510
+ content: {
1511
+ type: "text" as const,
1512
+ text: renderPromptMessage(message.textTemplate, requestArguments),
1513
+ },
1514
+ })),
1515
+ };
1516
+ });
1517
+
1518
+ return server;
1519
+ }
1520
+
1521
+ export async function createMockCompletionServer(): Promise<SdkServerConnection> {
1522
+ const [{ Server }, { CompleteRequestSchema }] = await Promise.all([
1523
+ import("@modelcontextprotocol/sdk/server/index.js"),
1524
+ import("@modelcontextprotocol/sdk/types.js"),
1525
+ ]);
1526
+
1527
+ const server = new Server(
1528
+ { name: "mock-completion-server", version: "1.0.0" },
1529
+ { capabilities: { completions: {} } }
1530
+ );
1531
+ const promptArgumentCompletions = new Map<string, string[]>([
1532
+ ["code_review:language", ["python", "pydantic", "pytest", "pytorch", "pyright", "rust"]],
1533
+ ]);
1534
+ const maxValues = 3;
1535
+
1536
+ server.setRequestHandler(CompleteRequestSchema, async (request) => {
1537
+ if (request.params.ref.type !== "ref/prompt") {
1538
+ return {
1539
+ completion: {
1540
+ values: [],
1541
+ },
1542
+ };
1543
+ }
1544
+
1545
+ const completionKey = `${request.params.ref.name}:${request.params.argument.name}`;
1546
+ const candidates = promptArgumentCompletions.get(completionKey) ?? [];
1547
+ const partialValue = request.params.argument.value.toLowerCase();
1548
+ const matchingValues = candidates.filter((candidate) =>
1549
+ candidate.toLowerCase().startsWith(partialValue)
1550
+ );
1551
+ const values = matchingValues.slice(0, maxValues);
1552
+
1553
+ if (values.length < matchingValues.length) {
1554
+ return {
1555
+ completion: {
1556
+ values,
1557
+ hasMore: true,
1558
+ total: matchingValues.length,
1559
+ },
1560
+ };
1561
+ }
1562
+
1563
+ return {
1564
+ completion: {
1565
+ values,
1566
+ },
1567
+ };
1568
+ });
1569
+
1570
+ return server;
1571
+ }
1572
+
1573
+ export async function createMockProgressServer(): Promise<SdkServerConnection> {
1574
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1575
+ await Promise.all([
1576
+ import("@modelcontextprotocol/sdk/server/index.js"),
1577
+ import("@modelcontextprotocol/sdk/types.js"),
1578
+ ]);
1579
+
1580
+ const server = new Server(
1581
+ { name: "mock-progress-server", version: "1.0.0" },
1582
+ { capabilities: { tools: {} } }
1583
+ );
1584
+ const totalSteps = 4;
1585
+ const tool = {
1586
+ name: "slow_task",
1587
+ description: "Runs a simulated slow task and streams progress updates.",
1588
+ inputSchema: {
1589
+ type: "object" as const,
1590
+ properties: {},
1591
+ additionalProperties: false,
1592
+ },
1593
+ };
1594
+
1595
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1596
+ tools: [tool],
1597
+ }));
1598
+
1599
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1600
+ if (request.params.name !== "slow_task") {
1601
+ throw new Error(`Unknown tool: ${request.params.name}`);
1602
+ }
1603
+
1604
+ const progressToken = request.params._meta?.progressToken;
1605
+ if (progressToken !== undefined) {
1606
+ for (let step = 1; step <= totalSteps; step += 1) {
1607
+ await extra.sendNotification({
1608
+ method: "notifications/progress",
1609
+ params: {
1610
+ progressToken,
1611
+ progress: step,
1612
+ total: totalSteps,
1613
+ message: `Completed step ${step} of ${totalSteps}`,
1614
+ },
1615
+ });
1616
+
1617
+ await new Promise<void>((resolve) => {
1618
+ setTimeout(resolve, 5);
1619
+ });
1620
+ }
1621
+ }
1622
+
1623
+ return {
1624
+ content: [
1625
+ {
1626
+ type: "text" as const,
1627
+ text: "slow_task complete",
1628
+ },
1629
+ ],
1630
+ };
1631
+ });
1632
+
1633
+ return server;
1634
+ }
1635
+
1636
+ export interface MockSlowToolServerOptions {
1637
+ delayMs?: number;
1638
+ pollIntervalMs?: number;
1639
+ }
1640
+
1641
+ export async function createMockSlowToolServer(
1642
+ options: MockSlowToolServerOptions = {}
1643
+ ): Promise<
1644
+ SdkServerConnection & {
1645
+ wasStarted: () => boolean;
1646
+ getStartedRequestIds: () => RequestId[];
1647
+ wasCancelled: () => boolean;
1648
+ getCancelledRequestIds: () => RequestId[];
1649
+ }
1650
+ > {
1651
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1652
+ await Promise.all([
1653
+ import("@modelcontextprotocol/sdk/server/index.js"),
1654
+ import("@modelcontextprotocol/sdk/types.js"),
1655
+ ]);
1656
+
1657
+ const defaultDelayMs = options.delayMs ?? 1_000;
1658
+ const pollIntervalMs = options.pollIntervalMs ?? 10;
1659
+ if (!Number.isFinite(defaultDelayMs) || defaultDelayMs < 0) {
1660
+ throw new Error("createMockSlowToolServer delayMs must be a finite non-negative number");
1661
+ }
1662
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
1663
+ throw new Error("createMockSlowToolServer pollIntervalMs must be a finite positive number");
1664
+ }
1665
+
1666
+ const server = new Server(
1667
+ { name: "mock-slow-tool-server", version: "1.0.0" },
1668
+ { capabilities: { tools: {} } }
1669
+ );
1670
+ const startedRequestIds = new Set<RequestId>();
1671
+ const cancelledRequestIds = new Set<RequestId>();
1672
+ const tool = {
1673
+ name: "slow",
1674
+ description: "Delays the response and supports cancellation.",
1675
+ inputSchema: {
1676
+ type: "object" as const,
1677
+ properties: {
1678
+ delayMs: {
1679
+ type: "number",
1680
+ },
1681
+ },
1682
+ additionalProperties: false,
1683
+ },
1684
+ };
1685
+
1686
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1687
+ tools: [tool],
1688
+ }));
1689
+
1690
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1691
+ if (request.params.name !== "slow") {
1692
+ throw new Error(`Unknown tool: ${request.params.name}`);
1693
+ }
1694
+ startedRequestIds.add(extra.requestId);
1695
+
1696
+ const delayArgument = request.params.arguments?.delayMs;
1697
+ const delayMs =
1698
+ delayArgument === undefined
1699
+ ? defaultDelayMs
1700
+ : typeof delayArgument === "number" && Number.isFinite(delayArgument) && delayArgument >= 0
1701
+ ? delayArgument
1702
+ : NaN;
1703
+ if (!Number.isFinite(delayMs)) {
1704
+ throw new Error("slow tool delayMs argument must be a finite non-negative number");
1705
+ }
1706
+
1707
+ const startedAt = Date.now();
1708
+ while (Date.now() - startedAt < delayMs) {
1709
+ if (extra.signal.aborted) {
1710
+ cancelledRequestIds.add(extra.requestId);
1711
+ throw new Error("slow tool cancelled");
1712
+ }
1713
+
1714
+ const elapsedMs = Date.now() - startedAt;
1715
+ const remainingMs = delayMs - elapsedMs;
1716
+ await new Promise<void>((resolve) => {
1717
+ setTimeout(resolve, Math.max(0, Math.min(pollIntervalMs, remainingMs)));
1718
+ });
1719
+ }
1720
+
1721
+ if (extra.signal.aborted) {
1722
+ cancelledRequestIds.add(extra.requestId);
1723
+ throw new Error("slow tool cancelled");
1724
+ }
1725
+
1726
+ return {
1727
+ content: [
1728
+ {
1729
+ type: "text" as const,
1730
+ text: `slow complete after ${delayMs}ms`,
1731
+ },
1732
+ ],
1733
+ };
1734
+ });
1735
+
1736
+ return Object.assign(server, {
1737
+ wasStarted: (): boolean => startedRequestIds.size > 0,
1738
+ getStartedRequestIds: (): RequestId[] => Array.from(startedRequestIds),
1739
+ wasCancelled: (): boolean => cancelledRequestIds.size > 0,
1740
+ getCancelledRequestIds: (): RequestId[] => Array.from(cancelledRequestIds),
1741
+ });
1742
+ }
1743
+
1744
+ export async function createMockErrorServer(): Promise<SdkServerConnection> {
1745
+ const [
1746
+ { Server },
1747
+ { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError: SdkMcpError },
1748
+ ] = await Promise.all([
1749
+ import("@modelcontextprotocol/sdk/server/index.js"),
1750
+ import("@modelcontextprotocol/sdk/types.js"),
1751
+ ]);
1752
+
1753
+ const server = new Server(
1754
+ { name: "mock-error-server", version: "1.0.0" },
1755
+ { capabilities: { tools: {} } }
1756
+ );
1757
+ const tools = [
1758
+ {
1759
+ name: "invalid_params",
1760
+ description: "Returns a JSON-RPC Invalid Params error.",
1761
+ inputSchema: {
1762
+ type: "object" as const,
1763
+ properties: {},
1764
+ additionalProperties: false,
1765
+ },
1766
+ },
1767
+ {
1768
+ name: "is_error",
1769
+ description: "Returns a tools/call result with isError: true.",
1770
+ inputSchema: {
1771
+ type: "object" as const,
1772
+ properties: {},
1773
+ additionalProperties: false,
1774
+ },
1775
+ },
1776
+ {
1777
+ name: "internal_error",
1778
+ description: "Throws an internal server error.",
1779
+ inputSchema: {
1780
+ type: "object" as const,
1781
+ properties: {},
1782
+ additionalProperties: false,
1783
+ },
1784
+ },
1785
+ ];
1786
+
1787
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1788
+ tools,
1789
+ }));
1790
+
1791
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1792
+ if (request.params.name === "invalid_params") {
1793
+ throw new SdkMcpError(
1794
+ ErrorCode.InvalidParams,
1795
+ "Intentional invalid params error from mock-error-server."
1796
+ );
1797
+ }
1798
+
1799
+ if (request.params.name === "is_error") {
1800
+ return {
1801
+ isError: true,
1802
+ content: [
1803
+ {
1804
+ type: "text" as const,
1805
+ text: "Intentional isError tool failure.",
1806
+ },
1807
+ ],
1808
+ };
1809
+ }
1810
+
1811
+ if (request.params.name === "internal_error") {
1812
+ throw new Error("Intentional internal error from mock-error-server.");
1813
+ }
1814
+
1815
+ throw new Error(`Unknown tool: ${request.params.name}`);
1816
+ });
1817
+
1818
+ return server;
1819
+ }
1820
+
1821
+ export async function createMockSamplingServer(): Promise<SdkServerConnection> {
1822
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1823
+ await Promise.all([
1824
+ import("@modelcontextprotocol/sdk/server/index.js"),
1825
+ import("@modelcontextprotocol/sdk/types.js"),
1826
+ ]);
1827
+
1828
+ const server = new Server(
1829
+ { name: "mock-sampling-server", version: "1.0.0" },
1830
+ { capabilities: { tools: {} } }
1831
+ );
1832
+ const tool = {
1833
+ name: "sample_message",
1834
+ description: "Requests sampling/createMessage from the client and returns the sampled text.",
1835
+ inputSchema: {
1836
+ type: "object" as const,
1837
+ properties: {
1838
+ topic: {
1839
+ type: "string",
1840
+ },
1841
+ },
1842
+ required: ["topic"],
1843
+ additionalProperties: false,
1844
+ },
1845
+ };
1846
+
1847
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1848
+ tools: [tool],
1849
+ }));
1850
+
1851
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1852
+ if (request.params.name !== "sample_message") {
1853
+ throw new Error(`Unknown tool: ${request.params.name}`);
1854
+ }
1855
+
1856
+ const topic = request.params.arguments?.topic;
1857
+ if (typeof topic !== "string") {
1858
+ throw new Error("sample_message requires a string topic argument");
1859
+ }
1860
+
1861
+ const samplingResult = await server.createMessage({
1862
+ messages: [
1863
+ {
1864
+ role: "user",
1865
+ content: {
1866
+ type: "text",
1867
+ text: `Provide a concise sentence about ${topic}.`,
1868
+ },
1869
+ },
1870
+ ],
1871
+ maxTokens: 64,
1872
+ modelPreferences: {
1873
+ hints: [{ name: "mock-sampling-model" }],
1874
+ speedPriority: 0.2,
1875
+ intelligencePriority: 0.9,
1876
+ },
1877
+ systemPrompt: "Return exactly one concise sentence.",
1878
+ });
1879
+
1880
+ const sampledText =
1881
+ samplingResult.content.type === "text"
1882
+ ? samplingResult.content.text
1883
+ : JSON.stringify(samplingResult.content);
1884
+
1885
+ return {
1886
+ content: [
1887
+ {
1888
+ type: "text" as const,
1889
+ text: `Sampled response: ${sampledText}`,
1890
+ },
1891
+ ],
1892
+ };
1893
+ });
1894
+
1895
+ return server;
1896
+ }
1897
+
1898
+ export async function createMockRootsServer(): Promise<SdkServerConnection> {
1899
+ const [
1900
+ { Server },
1901
+ {
1902
+ CallToolRequestSchema,
1903
+ ListToolsRequestSchema,
1904
+ RootsListChangedNotificationSchema,
1905
+ },
1906
+ ] =
1907
+ await Promise.all([
1908
+ import("@modelcontextprotocol/sdk/server/index.js"),
1909
+ import("@modelcontextprotocol/sdk/types.js"),
1910
+ ]);
1911
+
1912
+ const server = new Server(
1913
+ { name: "mock-roots-server", version: "1.0.0" },
1914
+ { capabilities: { tools: {} } }
1915
+ );
1916
+ const tool = {
1917
+ name: "roots_summary",
1918
+ description: "Requests roots/list from the client and returns a summary of roots.",
1919
+ inputSchema: {
1920
+ type: "object" as const,
1921
+ properties: {},
1922
+ additionalProperties: false,
1923
+ },
1924
+ };
1925
+
1926
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1927
+ tools: [tool],
1928
+ }));
1929
+
1930
+ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
1931
+ await server.listRoots();
1932
+ });
1933
+
1934
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1935
+ if (request.params.name !== "roots_summary") {
1936
+ throw new Error(`Unknown tool: ${request.params.name}`);
1937
+ }
1938
+
1939
+ const rootsResult = await server.listRoots();
1940
+ const rootSummary = rootsResult.roots
1941
+ .map((root) => (root.name === undefined ? root.uri : `${root.name} (${root.uri})`))
1942
+ .join(", ");
1943
+
1944
+ return {
1945
+ content: [
1946
+ {
1947
+ type: "text" as const,
1948
+ text: rootSummary.length === 0 ? "Roots: none" : `Roots: ${rootSummary}`,
1949
+ },
1950
+ ],
1951
+ };
1952
+ });
1953
+
1954
+ return server;
1955
+ }
1956
+
1957
+ export async function createMockLoggingServer(): Promise<SdkServerConnection> {
1958
+ const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] =
1959
+ await Promise.all([
1960
+ import("@modelcontextprotocol/sdk/server/index.js"),
1961
+ import("@modelcontextprotocol/sdk/types.js"),
1962
+ ]);
1963
+
1964
+ const server = new Server(
1965
+ { name: "mock-logging-server", version: "1.0.0" },
1966
+ { capabilities: { tools: {}, logging: {} } }
1967
+ );
1968
+ const tool = {
1969
+ name: "emit_logs",
1970
+ description: "Emits mock log messages at multiple levels.",
1971
+ inputSchema: {
1972
+ type: "object" as const,
1973
+ properties: {},
1974
+ additionalProperties: false,
1975
+ },
1976
+ };
1977
+
1978
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1979
+ tools: [tool],
1980
+ }));
1981
+
1982
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1983
+ if (request.params.name !== "emit_logs") {
1984
+ throw new Error(`Unknown tool: ${request.params.name}`);
1985
+ }
1986
+
1987
+ await server.sendLoggingMessage({
1988
+ level: "debug",
1989
+ logger: "mock-logging-server",
1990
+ data: { message: "Debug message" },
1991
+ });
1992
+ await server.sendLoggingMessage({
1993
+ level: "info",
1994
+ logger: "mock-logging-server",
1995
+ data: { message: "Info message" },
1996
+ });
1997
+ await server.sendLoggingMessage({
1998
+ level: "error",
1999
+ logger: "mock-logging-server",
2000
+ data: { message: "Error message" },
2001
+ });
2002
+
2003
+ return {
2004
+ content: [
2005
+ {
2006
+ type: "text" as const,
2007
+ text: "Emitted log messages.",
2008
+ },
2009
+ ],
2010
+ };
2011
+ });
2012
+
2013
+ return server;
2014
+ }
2015
+
2016
+ export async function createMockFullFeaturedServer(): Promise<SdkServerConnection> {
2017
+ const [
2018
+ { Server },
2019
+ {
2020
+ CallToolRequestSchema,
2021
+ CompleteRequestSchema,
2022
+ GetPromptRequestSchema,
2023
+ ListPromptsRequestSchema,
2024
+ ListResourcesRequestSchema,
2025
+ ListToolsRequestSchema,
2026
+ ReadResourceRequestSchema,
2027
+ },
2028
+ ] = await Promise.all([
2029
+ import("@modelcontextprotocol/sdk/server/index.js"),
2030
+ import("@modelcontextprotocol/sdk/types.js"),
2031
+ ]);
2032
+
2033
+ const server = new Server(
2034
+ { name: "mock-full-featured-server", version: "1.0.0" },
2035
+ {
2036
+ capabilities: {
2037
+ tools: {},
2038
+ resources: {},
2039
+ prompts: {},
2040
+ logging: {},
2041
+ completions: {},
2042
+ },
2043
+ }
2044
+ );
2045
+ const tool = {
2046
+ name: "full_featured_ping",
2047
+ description: "Returns a text response and emits an info log.",
2048
+ inputSchema: {
2049
+ type: "object" as const,
2050
+ properties: {},
2051
+ additionalProperties: false,
2052
+ },
2053
+ };
2054
+ const resources = [
2055
+ {
2056
+ uri: "file:///full-featured.txt",
2057
+ name: "full-featured.txt",
2058
+ mimeType: "text/plain",
2059
+ },
2060
+ ];
2061
+ const prompts = [
2062
+ {
2063
+ name: "full_featured_prompt",
2064
+ description: "Returns a short prompt message for a topic.",
2065
+ arguments: [
2066
+ {
2067
+ name: "topic",
2068
+ description: "Topic to include in the prompt output.",
2069
+ required: false,
2070
+ },
2071
+ ],
2072
+ },
2073
+ ];
2074
+ const completionValues = ["alpha", "beta", "gamma"];
2075
+
2076
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2077
+ tools: [tool],
2078
+ }));
2079
+
2080
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2081
+ if (request.params.name !== "full_featured_ping") {
2082
+ throw new Error(`Unknown tool: ${request.params.name}`);
2083
+ }
2084
+
2085
+ await server.sendLoggingMessage({
2086
+ level: "info",
2087
+ logger: "mock-full-featured-server",
2088
+ data: {
2089
+ message: "full_featured_ping called",
2090
+ },
2091
+ });
2092
+
2093
+ return {
2094
+ content: [
2095
+ {
2096
+ type: "text" as const,
2097
+ text: "full_featured_ping ok",
2098
+ },
2099
+ ],
2100
+ };
2101
+ });
2102
+
2103
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2104
+ resources,
2105
+ }));
2106
+
2107
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2108
+ if (request.params.uri !== "file:///full-featured.txt") {
2109
+ throw new Error(`Unknown resource: ${request.params.uri}`);
2110
+ }
2111
+
2112
+ return {
2113
+ contents: [
2114
+ {
2115
+ uri: "file:///full-featured.txt",
2116
+ mimeType: "text/plain",
2117
+ text: "Mock full-featured resource",
2118
+ },
2119
+ ],
2120
+ };
2121
+ });
2122
+
2123
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
2124
+ prompts,
2125
+ }));
2126
+
2127
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
2128
+ if (request.params.name !== "full_featured_prompt") {
2129
+ throw new Error(`Unknown prompt: ${request.params.name}`);
2130
+ }
2131
+
2132
+ const topic = request.params.arguments?.topic;
2133
+ const topicText = typeof topic === "string" && topic.length > 0 ? topic : "general";
2134
+
2135
+ return {
2136
+ description: "Mock prompt from full-featured server.",
2137
+ messages: [
2138
+ {
2139
+ role: "user" as const,
2140
+ content: {
2141
+ type: "text" as const,
2142
+ text: `Provide a short summary for ${topicText}.`,
2143
+ },
2144
+ },
2145
+ ],
2146
+ };
2147
+ });
2148
+
2149
+ server.setRequestHandler(CompleteRequestSchema, async (request) => {
2150
+ if (
2151
+ request.params.ref.type !== "ref/prompt" ||
2152
+ request.params.ref.name !== "full_featured_prompt" ||
2153
+ request.params.argument.name !== "topic"
2154
+ ) {
2155
+ return {
2156
+ completion: {
2157
+ values: [],
2158
+ },
2159
+ };
2160
+ }
2161
+
2162
+ const partialValue = request.params.argument.value.toLowerCase();
2163
+ const values = completionValues.filter((value) => value.startsWith(partialValue));
2164
+
2165
+ return {
2166
+ completion: {
2167
+ values,
2168
+ },
2169
+ };
2170
+ });
2171
+
2172
+ return server;
2173
+ }
2174
+
2175
+ export async function createTestPair<TClient extends McpClientConnection>(
2176
+ server: TinyStdioMcpServer,
2177
+ createClient: () => TClient
2178
+ ): Promise<SdkTestPair<TClient>> {
2179
+ const { clientTransport, serverTransport } = createInMemoryTransportPair();
2180
+ const serverPromise = server.connect(serverTransport);
2181
+ const client = createClient();
2182
+
2183
+ try {
2184
+ await client.connect(clientTransport);
2185
+ } catch (error) {
2186
+ clientTransport.dispose(new Error("tiny-stdio-mcp-server test pair setup failed"));
2187
+ await serverPromise;
2188
+ throw error;
2189
+ }
2190
+
2191
+ const cleanup = async (): Promise<void> => {
2192
+ await client.close();
2193
+ clientTransport.dispose(new Error("tiny-stdio-mcp-server test pair cleanup"));
2194
+ await serverPromise;
2195
+ };
2196
+
2197
+ return { client, cleanup };
2198
+ }
2199
+
2200
+ export type StdioSpawn = (
2201
+ command: string,
2202
+ args: ReadonlyArray<string>,
2203
+ options: SpawnOptions
2204
+ ) => ChildProcessWithoutNullStreams;
2205
+
2206
+ export interface StdioTransportOptions {
2207
+ command: string;
2208
+ args?: string[];
2209
+ cwd?: string;
2210
+ env?: NodeJS.ProcessEnv;
2211
+ spawn?: StdioSpawn;
2212
+ }
2213
+
2214
+ export type HttpTransportFetch = (
2215
+ input: string | URL,
2216
+ init?: RequestInit
2217
+ ) => Promise<Response>;
2218
+
2219
+ export interface HttpTransportOptions {
2220
+ url: string;
2221
+ headers?: HeadersInit;
2222
+ fetch?: HttpTransportFetch;
2223
+ oauth?: OAuthClientProviderOptions;
2224
+ oauthDiscoveryCache?: OAuthDiscoveryCache;
2225
+ }
2226
+
2227
+ function defaultStdioSpawn(
2228
+ command: string,
2229
+ args: ReadonlyArray<string>,
2230
+ options: SpawnOptions
2231
+ ): ChildProcessWithoutNullStreams {
2232
+ return spawn(command, args, options) as ChildProcessWithoutNullStreams;
2233
+ }
2234
+
2235
+ function defaultHttpTransportFetch(input: string | URL, init?: RequestInit): Promise<Response> {
2236
+ return fetch(input, init);
2237
+ }
2238
+
2239
+ export class StdioTransport implements McpTransport {
2240
+ readonly readable: Readable;
2241
+ readonly writable: Writable;
2242
+ readonly closed: Promise<McpTransportClosedEvent>;
2243
+ private readonly child: ChildProcessWithoutNullStreams;
2244
+ private disposed = false;
2245
+ private stderrOutput = "";
2246
+ private static readonly STDERR_MAX_LENGTH = 65_536;
2247
+
2248
+ constructor({
2249
+ command,
2250
+ args = [],
2251
+ cwd,
2252
+ env,
2253
+ spawn: spawnProcess = defaultStdioSpawn,
2254
+ }: StdioTransportOptions) {
2255
+ this.child = spawnProcess(command, args, {
2256
+ cwd,
2257
+ env,
2258
+ stdio: ["pipe", "pipe", "pipe"],
2259
+ });
2260
+
2261
+ const child = this.child;
2262
+
2263
+ this.readable = child.stdout;
2264
+ this.writable = child.stdin;
2265
+ child.stderr.on("data", (chunk: unknown) => {
2266
+ this.stderrOutput += chunkToString(chunk);
2267
+ if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
2268
+ this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
2269
+ }
2270
+ });
2271
+ this.closed = new Promise((resolve) => {
2272
+ let settled = false;
2273
+ const resolveClosed = (event: McpTransportClosedEvent) => {
2274
+ if (settled) {
2275
+ return;
2276
+ }
2277
+
2278
+ settled = true;
2279
+ resolve(event);
2280
+ };
2281
+
2282
+ child.once("exit", (code, signal) => {
2283
+ const closedEvent: McpTransportClosedEvent = {
2284
+ reason: new Error("Stdio transport process exited"),
2285
+ };
2286
+
2287
+ if (code !== null) {
2288
+ closedEvent.code = code;
2289
+ }
2290
+
2291
+ if (signal !== null) {
2292
+ closedEvent.signal = signal;
2293
+ }
2294
+
2295
+ resolveClosed(closedEvent);
2296
+ });
2297
+
2298
+ child.once("error", (error) => {
2299
+ const closedEvent: McpTransportClosedEvent = {
2300
+ reason: error instanceof Error ? error : new Error(String(error)),
2301
+ };
2302
+
2303
+ if (child.exitCode !== null) {
2304
+ closedEvent.code = child.exitCode;
2305
+ }
2306
+
2307
+ if (child.signalCode !== null) {
2308
+ closedEvent.signal = child.signalCode;
2309
+ }
2310
+
2311
+ resolveClosed(closedEvent);
2312
+ });
2313
+ });
2314
+ }
2315
+
2316
+ getStderrOutput(): string {
2317
+ return this.stderrOutput;
2318
+ }
2319
+
2320
+ dispose(reason = new Error("Stdio transport disposed")): void {
2321
+ void reason;
2322
+
2323
+ if (this.disposed) {
2324
+ return;
2325
+ }
2326
+
2327
+ this.disposed = true;
2328
+
2329
+ if (!this.child.stdin.destroyed && !this.child.stdin.writableEnded) {
2330
+ this.child.stdin.end();
2331
+ }
2332
+
2333
+ if (this.child.exitCode === null && this.child.signalCode === null && !this.child.killed) {
2334
+ this.child.kill("SIGTERM");
2335
+ }
2336
+ }
2337
+ }
2338
+
2339
+ export class HttpTransport implements McpTransport {
2340
+ readonly readable: Readable;
2341
+ readonly writable: Writable;
2342
+ readonly closed: Promise<McpTransportClosedEvent>;
2343
+ private readonly url: string;
2344
+ private readonly headers: HeadersInit;
2345
+ private readonly fetchImpl: HttpTransportFetch;
2346
+ private readonly readStream = new PassThrough();
2347
+ private readonly writeStream = new PassThrough();
2348
+ private resolveClosed:
2349
+ | ((closedEvent: McpTransportClosedEvent) => void)
2350
+ | undefined;
2351
+ private sessionId: string | undefined;
2352
+ private lastEventId: string | undefined;
2353
+ private getSseStreamStarted = false;
2354
+ private disposed = false;
2355
+ private readonly oauthProvider: OAuthClientProvider | undefined;
2356
+ private readonly oauthMetadataDiscovery: OAuthMetadataDiscovery | undefined;
2357
+ private readonly inFlightFetchAbortControllers = new Set<AbortController>();
2358
+ private readonly openSseReaders = new Set<ReadableStreamDefaultReader<Uint8Array>>();
2359
+
2360
+ constructor({
2361
+ url,
2362
+ headers = {},
2363
+ fetch: fetchImpl = defaultHttpTransportFetch,
2364
+ oauth,
2365
+ oauthDiscoveryCache,
2366
+ }: HttpTransportOptions) {
2367
+ this.url = url;
2368
+ this.headers = headers;
2369
+ this.fetchImpl = fetchImpl;
2370
+ this.oauthProvider = oauth === undefined
2371
+ ? undefined
2372
+ : createOAuthClientProvider(oauth);
2373
+ this.oauthMetadataDiscovery = oauth === undefined
2374
+ ? undefined
2375
+ : new OAuthMetadataDiscovery({
2376
+ fetch: (input, init) => this.fetchWithAbort(input, init ?? {}),
2377
+ cache: oauthDiscoveryCache,
2378
+ });
2379
+ this.readable = this.readStream;
2380
+ this.writable = this.writeStream;
2381
+ this.closed = new Promise((resolve) => {
2382
+ this.resolveClosed = resolve;
2383
+ });
2384
+
2385
+ this.readStream.once("error", (error) => {
2386
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
2387
+ });
2388
+ this.writeStream.once("error", (error) => {
2389
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
2390
+ });
2391
+
2392
+ this.consumeWrittenLines().catch((error) => {
2393
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
2394
+ });
2395
+ }
2396
+
2397
+ dispose(reason = new Error("HTTP transport disposed")): void {
2398
+ if (this.disposed) {
2399
+ return;
2400
+ }
2401
+
2402
+ this.disposed = true;
2403
+ this.abortInFlightFetches();
2404
+ this.cancelOpenSseReaders();
2405
+ this.terminateSession();
2406
+
2407
+ if (!this.writeStream.destroyed && !this.writeStream.writableEnded) {
2408
+ this.writeStream.end();
2409
+ }
2410
+
2411
+ if (!this.readStream.destroyed && !this.readStream.writableEnded) {
2412
+ this.readStream.end();
2413
+ }
2414
+
2415
+ const resolveClosed = this.resolveClosed;
2416
+ this.resolveClosed = undefined;
2417
+ resolveClosed?.({ reason });
2418
+ }
2419
+
2420
+ private abortInFlightFetches(): void {
2421
+ for (const abortController of this.inFlightFetchAbortControllers) {
2422
+ abortController.abort();
2423
+ }
2424
+
2425
+ this.inFlightFetchAbortControllers.clear();
2426
+ }
2427
+
2428
+ private cancelOpenSseReaders(): void {
2429
+ for (const reader of this.openSseReaders) {
2430
+ void reader.cancel().catch(() => undefined);
2431
+ }
2432
+
2433
+ this.openSseReaders.clear();
2434
+ }
2435
+
2436
+ private async fetchWithAbort(input: string | URL, init: RequestInit): Promise<Response> {
2437
+ const abortController = new AbortController();
2438
+ this.inFlightFetchAbortControllers.add(abortController);
2439
+
2440
+ try {
2441
+ return await this.fetchImpl(input, {
2442
+ ...init,
2443
+ signal: abortController.signal,
2444
+ });
2445
+ } finally {
2446
+ this.inFlightFetchAbortControllers.delete(abortController);
2447
+ }
2448
+ }
2449
+
2450
+ private async consumeWrittenLines(): Promise<void> {
2451
+ for await (const line of readLines(this.writeStream)) {
2452
+ if (this.disposed || line.length === 0) {
2453
+ continue;
2454
+ }
2455
+
2456
+ const hasSessionId = this.sessionId !== undefined;
2457
+ const response = await this.fetchWithOAuthRetry({
2458
+ method: "POST",
2459
+ createHeaders: () => this.createPostHeaders(),
2460
+ body: line,
2461
+ });
2462
+
2463
+ if (hasSessionId && response.status === 404) {
2464
+ this.sessionId = undefined;
2465
+ this.dispose(new Error("HTTP transport session expired (404 response)"));
2466
+ return;
2467
+ }
2468
+
2469
+ await this.throwForPostHttpError(response);
2470
+ this.captureSessionId(response);
2471
+ this.maybeOpenGetSseStream();
2472
+ await this.forwardResponseMessages(response);
2473
+ }
2474
+ }
2475
+
2476
+ private async createPostHeaders(): Promise<Headers> {
2477
+ const headers = new Headers(this.headers);
2478
+ headers.set("Accept", "application/json, text/event-stream");
2479
+ headers.set("Content-Type", "application/json");
2480
+ if (this.sessionId !== undefined) {
2481
+ headers.set("Mcp-Session-Id", this.sessionId);
2482
+ }
2483
+ return this.authorizeRequestHeaders(headers);
2484
+ }
2485
+
2486
+ private async createGetHeaders(): Promise<Headers> {
2487
+ const headers = new Headers(this.headers);
2488
+ headers.set("Accept", "text/event-stream");
2489
+ if (this.sessionId !== undefined) {
2490
+ headers.set("Mcp-Session-Id", this.sessionId);
2491
+ }
2492
+ if (this.lastEventId !== undefined) {
2493
+ headers.set("Last-Event-ID", this.lastEventId);
2494
+ }
2495
+ return this.authorizeRequestHeaders(headers);
2496
+ }
2497
+
2498
+ private async createDeleteHeaders(sessionId: string): Promise<Headers> {
2499
+ const headers = new Headers(this.headers);
2500
+ headers.set("Mcp-Session-Id", sessionId);
2501
+ return this.authorizeRequestHeaders(headers);
2502
+ }
2503
+
2504
+ private async authorizeRequestHeaders(headers: Headers): Promise<Headers> {
2505
+ await this.oauthProvider?.authorizeRequest?.({
2506
+ requestUrl: new URL(this.url),
2507
+ headers,
2508
+ fetch: this.fetchImpl,
2509
+ });
2510
+ return headers;
2511
+ }
2512
+
2513
+ private captureSessionId(response: Response): void {
2514
+ const sessionId = response.headers.get("Mcp-Session-Id");
2515
+ if (sessionId === null || sessionId.length === 0) {
2516
+ return;
2517
+ }
2518
+
2519
+ this.sessionId = sessionId;
2520
+ }
2521
+
2522
+ private maybeOpenGetSseStream(): void {
2523
+ if (this.disposed || this.sessionId === undefined || this.getSseStreamStarted) {
2524
+ return;
2525
+ }
2526
+
2527
+ this.getSseStreamStarted = true;
2528
+ this.consumeGetSseStream().catch((error) => {
2529
+ if (error instanceof HttpTransportGetSseNotSupportedError || this.disposed) {
2530
+ return;
2531
+ }
2532
+
2533
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
2534
+ });
2535
+ }
2536
+
2537
+ private terminateSession(): void {
2538
+ if (this.sessionId === undefined) {
2539
+ return;
2540
+ }
2541
+
2542
+ const sessionId = this.sessionId;
2543
+ this.sessionId = undefined;
2544
+
2545
+ this.sendSessionTerminationRequest(sessionId).catch(() => undefined);
2546
+ }
2547
+
2548
+ private async sendSessionTerminationRequest(sessionId: string): Promise<void> {
2549
+ const response = await this.fetchImpl(this.url, {
2550
+ method: "DELETE",
2551
+ headers: await this.createDeleteHeaders(sessionId),
2552
+ });
2553
+
2554
+ if (response.status === 405) {
2555
+ return;
2556
+ }
2557
+ }
2558
+
2559
+ private async consumeGetSseStream(): Promise<void> {
2560
+ const response = await this.fetchWithOAuthRetry({
2561
+ method: "GET",
2562
+ createHeaders: () => this.createGetHeaders(),
2563
+ });
2564
+
2565
+ if (response.status === 405) {
2566
+ throw new HttpTransportGetSseNotSupportedError();
2567
+ }
2568
+
2569
+ const contentType = response.headers.get("Content-Type");
2570
+ if (contentType === null) {
2571
+ return;
2572
+ }
2573
+
2574
+ if (contentType.toLowerCase().includes("text/event-stream")) {
2575
+ await this.forwardSseResponseMessages(response);
2576
+ this.getSseStreamStarted = false;
2577
+ if (!this.disposed && this.sessionId !== undefined && this.lastEventId !== undefined) {
2578
+ this.maybeOpenGetSseStream();
2579
+ }
2580
+ }
2581
+ }
2582
+
2583
+ private async throwForPostHttpError(response: Response): Promise<void> {
2584
+ if (response.status < 400) {
2585
+ return;
2586
+ }
2587
+
2588
+ const responseBody = (await response.text()).trim();
2589
+ const statusDescriptor = `${response.status} ${response.statusText}`.trim();
2590
+ const message = responseBody.length === 0
2591
+ ? `HTTP transport POST failed (${statusDescriptor})`
2592
+ : `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
2593
+ throw new Error(message);
2594
+ }
2595
+
2596
+ private async maybeHandleUnauthorizedResponse(response: Response): Promise<boolean> {
2597
+ if (response.status !== 401 || this.oauthProvider === undefined) {
2598
+ return false;
2599
+ }
2600
+
2601
+ const discoveryClient = this.oauthMetadataDiscovery;
2602
+ if (discoveryClient === undefined) {
2603
+ return false;
2604
+ }
2605
+
2606
+ const challenge = parseBearerWwwAuthenticateHeader(response.headers.get("WWW-Authenticate"));
2607
+ const resourceMetadataUrl = challenge?.params.resource_metadata;
2608
+ const discovery = await discoveryClient.discover(this.url, {
2609
+ resourceMetadataUrl,
2610
+ });
2611
+ const result = await this.oauthProvider.handleUnauthorized({
2612
+ requestUrl: new URL(this.url),
2613
+ response: response.clone(),
2614
+ challenge,
2615
+ discovery,
2616
+ fetch: this.fetchImpl,
2617
+ });
2618
+
2619
+ if (result.action === "retry") {
2620
+ return true;
2621
+ }
2622
+
2623
+ if (result.error !== undefined) {
2624
+ throw result.error;
2625
+ }
2626
+
2627
+ return false;
2628
+ }
2629
+
2630
+ private async forwardResponseMessages(response: Response): Promise<void> {
2631
+ if (response.status === 202) {
2632
+ return;
2633
+ }
2634
+
2635
+ const contentType = response.headers.get("Content-Type");
2636
+ if (contentType === null) {
2637
+ return;
2638
+ }
2639
+
2640
+ const normalizedContentType = contentType.toLowerCase();
2641
+ if (normalizedContentType.includes("text/event-stream")) {
2642
+ await this.forwardSseResponseMessages(response);
2643
+ return;
2644
+ }
2645
+
2646
+ if (normalizedContentType.includes("application/json")) {
2647
+ await this.forwardJsonResponseMessage(response);
2648
+ }
2649
+ }
2650
+
2651
+ private async forwardSseResponseMessages(response: Response): Promise<void> {
2652
+ if (response.body === null) {
2653
+ return;
2654
+ }
2655
+
2656
+ const parser = new SseParser();
2657
+ const decoder = new TextDecoder();
2658
+ const reader = response.body.getReader();
2659
+ this.openSseReaders.add(reader);
2660
+
2661
+ try {
2662
+ while (true) {
2663
+ const { done, value } = await reader.read();
2664
+ if (done) {
2665
+ break;
2666
+ }
2667
+
2668
+ if (value === undefined) {
2669
+ continue;
2670
+ }
2671
+
2672
+ const messages = parser.push(decoder.decode(value, { stream: true }));
2673
+ this.writeSseMessages(messages);
2674
+ this.lastEventId = parser.lastEventId;
2675
+ }
2676
+
2677
+ const trailingChunk = decoder.decode();
2678
+ if (trailingChunk.length > 0) {
2679
+ this.writeSseMessages(parser.push(trailingChunk));
2680
+ this.lastEventId = parser.lastEventId;
2681
+ }
2682
+
2683
+ this.writeSseMessages(parser.flush());
2684
+ this.lastEventId = parser.lastEventId;
2685
+ } finally {
2686
+ this.openSseReaders.delete(reader);
2687
+ reader.releaseLock();
2688
+ }
2689
+ }
2690
+
2691
+ private async forwardJsonResponseMessage(response: Response): Promise<void> {
2692
+ const payload = await response.text();
2693
+ if (payload.length === 0) {
2694
+ return;
2695
+ }
2696
+
2697
+ const parsedPayload = JSON.parse(payload) as unknown;
2698
+ this.writeReadableLine(JSON.stringify(parsedPayload));
2699
+ }
2700
+
2701
+ private writeSseMessages(messages: ParsedSseMessage[]): void {
2702
+ for (const message of messages) {
2703
+ this.writeReadableLine(message.data);
2704
+ }
2705
+ }
2706
+
2707
+ private writeReadableLine(line: string): void {
2708
+ if (this.disposed || this.readStream.destroyed || this.readStream.writableEnded) {
2709
+ return;
2710
+ }
2711
+
2712
+ this.readStream.write(`${line}\n`);
2713
+ }
2714
+
2715
+ private async fetchWithOAuthRetry(input: {
2716
+ method: "GET" | "POST";
2717
+ createHeaders: () => Promise<Headers>;
2718
+ body?: BodyInit;
2719
+ }): Promise<Response> {
2720
+ const request = async (): Promise<Response> =>
2721
+ this.fetchWithAbort(this.url, {
2722
+ method: input.method,
2723
+ headers: await input.createHeaders(),
2724
+ body: input.body,
2725
+ });
2726
+
2727
+ let response = await request();
2728
+ if (await this.maybeHandleUnauthorizedResponse(response)) {
2729
+ response = await request();
2730
+ }
2731
+
2732
+ const oauthError = this.oauthProvider === undefined
2733
+ ? null
2734
+ : this.readOAuthChallengeError(response);
2735
+ if (oauthError !== null) {
2736
+ throw oauthError;
2737
+ }
2738
+
2739
+ return response;
2740
+ }
2741
+
2742
+ private readOAuthChallengeError(response: Response): OAuthError | null {
2743
+ if (response.status !== 401 && response.status !== 403) {
2744
+ return null;
2745
+ }
2746
+
2747
+ const challenge = parseBearerWwwAuthenticateHeader(response.headers.get("WWW-Authenticate"));
2748
+ const error = challenge?.params.error;
2749
+ if (error === undefined || error.length === 0) {
2750
+ return null;
2751
+ }
2752
+
2753
+ return new OAuthError(
2754
+ {
2755
+ error,
2756
+ error_description: challenge?.params.error_description,
2757
+ error_uri: challenge?.params.error_uri,
2758
+ },
2759
+ response.status
2760
+ );
2761
+ }
2762
+ }
2763
+
2764
+ class HttpTransportGetSseNotSupportedError extends Error {
2765
+ constructor() {
2766
+ super("HTTP transport server does not support GET SSE streams");
2767
+ }
2768
+ }
2769
+
2770
+ export interface JsonRpcRequest {
2771
+ jsonrpc: "2.0";
2772
+ id: RequestId;
2773
+ method: string;
2774
+ params?: unknown;
2775
+ }
2776
+
2777
+ export interface JsonRpcNotification {
2778
+ jsonrpc: "2.0";
2779
+ method: string;
2780
+ params?: unknown;
2781
+ }
2782
+
2783
+ export interface JsonRpcErrorObject {
2784
+ code: number;
2785
+ message: string;
2786
+ data?: unknown;
2787
+ }
2788
+
2789
+ export class McpError extends Error {
2790
+ readonly code: number;
2791
+ declare readonly data?: unknown;
2792
+
2793
+ constructor(code: number, message: string, data?: unknown) {
2794
+ super(message);
2795
+ this.name = "McpError";
2796
+ this.code = code;
2797
+ if (data !== undefined) {
2798
+ this.data = data;
2799
+ }
2800
+ }
2801
+ }
2802
+
2803
+ export interface JsonRpcSuccessResponse {
2804
+ jsonrpc: "2.0";
2805
+ id: RequestId;
2806
+ result: unknown;
2807
+ }
2808
+
2809
+ export interface JsonRpcErrorResponse {
2810
+ jsonrpc: "2.0";
2811
+ id: RequestId;
2812
+ error: JsonRpcErrorObject;
2813
+ }
2814
+
2815
+ export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
2816
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
2817
+
2818
+ export type ParsedJsonRpcMessage =
2819
+ | {
2820
+ type: "request";
2821
+ message: JsonRpcRequest;
2822
+ }
2823
+ | {
2824
+ type: "notification";
2825
+ message: JsonRpcNotification;
2826
+ }
2827
+ | {
2828
+ type: "response";
2829
+ message: JsonRpcResponse;
2830
+ }
2831
+ | {
2832
+ type: "invalid";
2833
+ id: RequestId | null;
2834
+ error: McpError;
2835
+ };
2836
+
2837
+ export function serializeJsonRpcMessage(message: JsonRpcMessage): string {
2838
+ return `${JSON.stringify(message)}\n`;
2839
+ }
2840
+
2841
+ function chunkToString(chunk: unknown): string {
2842
+ if (typeof chunk === "string") {
2843
+ return chunk;
2844
+ }
2845
+
2846
+ if (chunk instanceof Uint8Array) {
2847
+ return Buffer.from(chunk).toString("utf8");
2848
+ }
2849
+
2850
+ return String(chunk);
2851
+ }
2852
+
2853
+ function normalizeLine(line: string): string {
2854
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
2855
+ }
2856
+
2857
+ export async function* readLines(stream: Readable): AsyncGenerator<string> {
2858
+ let buffer = "";
2859
+
2860
+ for await (const chunk of stream as AsyncIterable<unknown>) {
2861
+ buffer += chunkToString(chunk);
2862
+
2863
+ while (true) {
2864
+ const newlineIndex = buffer.indexOf("\n");
2865
+ if (newlineIndex === -1) {
2866
+ break;
2867
+ }
2868
+
2869
+ const line = buffer.slice(0, newlineIndex);
2870
+ buffer = buffer.slice(newlineIndex + 1);
2871
+ yield normalizeLine(line);
2872
+ }
2873
+ }
2874
+
2875
+ if (buffer.length > 0) {
2876
+ yield normalizeLine(buffer);
2877
+ }
2878
+ }
2879
+
2880
+ export interface ParsedSseMessage {
2881
+ data: string;
2882
+ id?: string;
2883
+ }
2884
+
2885
+ export class SseParser {
2886
+ private buffer = "";
2887
+ private eventType: string | undefined;
2888
+ private dataLines: string[] = [];
2889
+ private eventId = "";
2890
+ private hasEventId = false;
2891
+ private _lastEventId: string | undefined;
2892
+
2893
+ get lastEventId(): string | undefined {
2894
+ return this._lastEventId;
2895
+ }
2896
+
2897
+ push(chunk: string): ParsedSseMessage[] {
2898
+ if (chunk.length === 0) {
2899
+ return [];
2900
+ }
2901
+
2902
+ this.buffer += chunk;
2903
+ const messages: ParsedSseMessage[] = [];
2904
+
2905
+ while (true) {
2906
+ const newlineIndex = this.buffer.indexOf("\n");
2907
+ if (newlineIndex === -1) {
2908
+ break;
2909
+ }
2910
+
2911
+ const line = normalizeLine(this.buffer.slice(0, newlineIndex));
2912
+ this.buffer = this.buffer.slice(newlineIndex + 1);
2913
+ this.consumeLine(line, messages);
2914
+ }
2915
+
2916
+ return messages;
2917
+ }
2918
+
2919
+ flush(): ParsedSseMessage[] {
2920
+ const messages: ParsedSseMessage[] = [];
2921
+
2922
+ if (this.buffer.length > 0) {
2923
+ this.consumeLine(normalizeLine(this.buffer), messages);
2924
+ this.buffer = "";
2925
+ }
2926
+
2927
+ this.emitEvent(messages);
2928
+ return messages;
2929
+ }
2930
+
2931
+ private consumeLine(line: string, messages: ParsedSseMessage[]): void {
2932
+ if (line.length === 0) {
2933
+ this.emitEvent(messages);
2934
+ return;
2935
+ }
2936
+
2937
+ if (line.startsWith(":")) {
2938
+ return;
2939
+ }
2940
+
2941
+ const separatorIndex = line.indexOf(":");
2942
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
2943
+ const rawValue = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
2944
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
2945
+
2946
+ if (field === "event") {
2947
+ this.eventType = value;
2948
+ return;
2949
+ }
2950
+
2951
+ if (field === "data") {
2952
+ this.dataLines.push(value);
2953
+ return;
2954
+ }
2955
+
2956
+ if (field === "id") {
2957
+ if (value.includes("\0")) {
2958
+ return;
2959
+ }
2960
+
2961
+ this.eventId = value;
2962
+ this.hasEventId = true;
2963
+ }
2964
+ }
2965
+
2966
+ private emitEvent(messages: ParsedSseMessage[]): void {
2967
+ const eventType = this.eventType ?? "message";
2968
+
2969
+ if (this.hasEventId) {
2970
+ this._lastEventId = this.eventId;
2971
+ }
2972
+
2973
+ if (this.dataLines.length === 0 || eventType !== "message") {
2974
+ this.resetEvent();
2975
+ return;
2976
+ }
2977
+
2978
+ const message: ParsedSseMessage = {
2979
+ data: this.dataLines.join("\n"),
2980
+ };
2981
+
2982
+ if (this.hasEventId) {
2983
+ message.id = this.eventId;
2984
+ }
2985
+
2986
+ messages.push(message);
2987
+ this.resetEvent();
2988
+ }
2989
+
2990
+ private resetEvent(): void {
2991
+ this.eventType = undefined;
2992
+ this.dataLines = [];
2993
+ this.eventId = "";
2994
+ this.hasEventId = false;
2995
+ }
2996
+ }
2997
+
2998
+ interface PendingRequest {
2999
+ resolve: (result: unknown) => void;
3000
+ reject: (error: unknown) => void;
3001
+ timeout: ReturnType<typeof setTimeout>;
3002
+ }
3003
+
3004
+ interface ActiveIncomingRequest {
3005
+ cancelled: boolean;
3006
+ }
3007
+
3008
+ export interface JsonRpcRequestOptions {
3009
+ timeoutMs?: number;
3010
+ onRequestId?: (requestId: RequestId) => void;
3011
+ }
3012
+
3013
+ interface JsonRpcRequestContext {
3014
+ id: RequestId;
3015
+ method: string;
3016
+ }
3017
+
3018
+ type JsonRpcRequestHandler = (
3019
+ params: unknown,
3020
+ context: JsonRpcRequestContext
3021
+ ) => unknown | Promise<unknown>;
3022
+
3023
+ interface JsonRpcNotificationContext {
3024
+ method: string;
3025
+ }
3026
+
3027
+ type JsonRpcNotificationHandler = (
3028
+ params: unknown,
3029
+ context: JsonRpcNotificationContext
3030
+ ) => unknown | Promise<unknown>;
3031
+
3032
+ export class JsonRpcMessageLayer {
3033
+ readonly requestTimeoutMs: number;
3034
+ private readonly input: Readable;
3035
+ private readonly output: Writable;
3036
+ private readonly inputClosedReason: Promise<Error> | undefined;
3037
+ private nextRequestId = 1;
3038
+ private disposedError: Error | undefined;
3039
+ private readonly pendingRequests = new Map<RequestId, PendingRequest>();
3040
+ private readonly activeIncomingRequests = new Map<RequestId, ActiveIncomingRequest>();
3041
+ private readonly requestHandlers = new Map<string, JsonRpcRequestHandler>();
3042
+ private readonly notificationHandlers = new Map<string, JsonRpcNotificationHandler>();
3043
+
3044
+ constructor(
3045
+ input: Readable,
3046
+ output: Writable,
3047
+ requestTimeoutMs = 30_000,
3048
+ inputClosedReason?: Promise<Error>
3049
+ ) {
3050
+ if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs < 0) {
3051
+ throw new Error("requestTimeoutMs must be a non-negative finite number");
3052
+ }
3053
+
3054
+ this.input = input;
3055
+ this.output = output;
3056
+ this.inputClosedReason = inputClosedReason;
3057
+ this.requestTimeoutMs = requestTimeoutMs;
3058
+ this.consumeInput().catch(() => undefined);
3059
+ }
3060
+
3061
+ sendNotification(method: string, params?: unknown): void {
3062
+ if (this.disposedError !== undefined) {
3063
+ throw this.disposedError;
3064
+ }
3065
+
3066
+ const message: JsonRpcNotification = {
3067
+ jsonrpc: "2.0",
3068
+ method,
3069
+ };
3070
+
3071
+ if (params !== undefined) {
3072
+ message.params = params;
3073
+ }
3074
+
3075
+ this.output.write(serializeJsonRpcMessage(message));
3076
+ }
3077
+
3078
+ onRequest(method: string, handler: JsonRpcRequestHandler): void {
3079
+ this.requestHandlers.set(method, handler);
3080
+ }
3081
+
3082
+ onNotification(method: string, handler: JsonRpcNotificationHandler): void {
3083
+ this.notificationHandlers.set(method, handler);
3084
+ }
3085
+
3086
+ sendRequest(
3087
+ method: string,
3088
+ params?: unknown,
3089
+ options: JsonRpcRequestOptions = {}
3090
+ ): Promise<unknown> {
3091
+ if (this.disposedError !== undefined) {
3092
+ throw this.disposedError;
3093
+ }
3094
+
3095
+ const id = this.nextRequestId;
3096
+ this.nextRequestId += 1;
3097
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
3098
+
3099
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
3100
+ throw new Error("timeoutMs must be a non-negative finite number");
3101
+ }
3102
+ if (options.onRequestId !== undefined) {
3103
+ options.onRequestId(id);
3104
+ }
3105
+
3106
+ const message: JsonRpcRequest = {
3107
+ jsonrpc: "2.0",
3108
+ id,
3109
+ method,
3110
+ };
3111
+
3112
+ if (params !== undefined) {
3113
+ message.params = params;
3114
+ }
3115
+
3116
+ return new Promise((resolve, reject) => {
3117
+ const timeout = setTimeout(() => {
3118
+ this.pendingRequests.delete(id);
3119
+ reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
3120
+ }, timeoutMs);
3121
+
3122
+ this.pendingRequests.set(id, { resolve, reject, timeout });
3123
+
3124
+ try {
3125
+ this.output.write(serializeJsonRpcMessage(message));
3126
+ } catch (error) {
3127
+ clearTimeout(timeout);
3128
+ this.pendingRequests.delete(id);
3129
+ reject(error);
3130
+ }
3131
+ });
3132
+ }
3133
+
3134
+ dispose(reason = new Error("JSON-RPC message layer disposed")): void {
3135
+ if (this.disposedError !== undefined) {
3136
+ return;
3137
+ }
3138
+
3139
+ this.disposedError = reason;
3140
+
3141
+ for (const pending of this.pendingRequests.values()) {
3142
+ clearTimeout(pending.timeout);
3143
+ pending.reject(reason);
3144
+ }
3145
+
3146
+ this.pendingRequests.clear();
3147
+ this.activeIncomingRequests.clear();
3148
+ }
3149
+
3150
+ private async consumeInput(): Promise<void> {
3151
+ try {
3152
+ for await (const line of readLines(this.input)) {
3153
+ if (this.disposedError !== undefined) {
3154
+ break;
3155
+ }
3156
+
3157
+ if (line.length === 0) {
3158
+ continue;
3159
+ }
3160
+
3161
+ let parsedLine: unknown;
3162
+ try {
3163
+ parsedLine = JSON.parse(line);
3164
+ } catch {
3165
+ await this.processParsedMessage({
3166
+ type: "invalid",
3167
+ id: null,
3168
+ error: parseError(),
3169
+ });
3170
+ continue;
3171
+ }
3172
+
3173
+ if (Array.isArray(parsedLine)) {
3174
+ for (const message of parsedLine) {
3175
+ if (this.disposedError !== undefined) {
3176
+ break;
3177
+ }
3178
+
3179
+ await this.processParsedMessage(parseJsonRpcPayload(message));
3180
+ }
3181
+ continue;
3182
+ }
3183
+
3184
+ await this.processParsedMessage(parseJsonRpcPayload(parsedLine));
3185
+ }
3186
+ } catch (error) {
3187
+ if (this.disposedError === undefined) {
3188
+ this.dispose(
3189
+ error instanceof Error
3190
+ ? error
3191
+ : new Error(`JSON-RPC input stream failed: ${String(error)}`)
3192
+ );
3193
+ }
3194
+ return;
3195
+ }
3196
+
3197
+ if (this.disposedError === undefined) {
3198
+ const streamClosedReason = await this.resolveInputStreamClosedReason();
3199
+ if (this.disposedError === undefined) {
3200
+ this.dispose(streamClosedReason);
3201
+ }
3202
+ }
3203
+ }
3204
+
3205
+ private async resolveInputStreamClosedReason(): Promise<Error> {
3206
+ const streamClosedError = new Error("JSON-RPC input stream closed");
3207
+ if (this.inputClosedReason === undefined) {
3208
+ return streamClosedError;
3209
+ }
3210
+
3211
+ try {
3212
+ return await Promise.race([
3213
+ this.inputClosedReason,
3214
+ new Promise<Error>((resolve) => {
3215
+ setTimeout(() => {
3216
+ resolve(streamClosedError);
3217
+ }, 50);
3218
+ }),
3219
+ ]);
3220
+ } catch {
3221
+ return streamClosedError;
3222
+ }
3223
+ }
3224
+
3225
+ private async processParsedMessage(parsed: ParsedJsonRpcMessage): Promise<void> {
3226
+ if (parsed.type === "request") {
3227
+ const handler = this.requestHandlers.get(parsed.message.method);
3228
+ if (handler === undefined) {
3229
+ this.output.write(
3230
+ serializeJsonRpcMessage({
3231
+ jsonrpc: "2.0",
3232
+ id: parsed.message.id,
3233
+ error: {
3234
+ code: ERROR_METHOD_NOT_FOUND,
3235
+ message: `Method not found: ${parsed.message.method}`,
3236
+ },
3237
+ })
3238
+ );
3239
+ return;
3240
+ }
3241
+
3242
+ this.handleIncomingRequest(parsed.message, handler);
3243
+ return;
3244
+ }
3245
+
3246
+ if (parsed.type === "notification") {
3247
+ if (parsed.message.method === "notifications/cancelled") {
3248
+ this.handleCancellationNotification(parsed.message.params);
3249
+ }
3250
+
3251
+ const handler = this.notificationHandlers.get(parsed.message.method);
3252
+ if (handler === undefined) {
3253
+ return;
3254
+ }
3255
+
3256
+ try {
3257
+ await handler(parsed.message.params, {
3258
+ method: parsed.message.method,
3259
+ });
3260
+ } catch {
3261
+ return;
3262
+ }
3263
+ return;
3264
+ }
3265
+
3266
+ if (parsed.type === "invalid") {
3267
+ const errorResponse: {
3268
+ jsonrpc: "2.0";
3269
+ id: RequestId | null;
3270
+ error: JsonRpcErrorObject;
3271
+ } = {
3272
+ jsonrpc: "2.0",
3273
+ id: parsed.id,
3274
+ error: {
3275
+ code: parsed.error.code,
3276
+ message: parsed.error.message,
3277
+ },
3278
+ };
3279
+
3280
+ if (parsed.error.data !== undefined) {
3281
+ errorResponse.error.data = parsed.error.data;
3282
+ }
3283
+
3284
+ this.output.write(`${JSON.stringify(errorResponse)}\n`);
3285
+ return;
3286
+ }
3287
+
3288
+ if (parsed.type !== "response") {
3289
+ return;
3290
+ }
3291
+
3292
+ const pending = this.pendingRequests.get(parsed.message.id);
3293
+ if (pending === undefined) {
3294
+ return;
3295
+ }
3296
+
3297
+ this.pendingRequests.delete(parsed.message.id);
3298
+ clearTimeout(pending.timeout);
3299
+ if ("result" in parsed.message) {
3300
+ pending.resolve(parsed.message.result);
3301
+ return;
3302
+ }
3303
+
3304
+ pending.reject(
3305
+ new McpError(
3306
+ parsed.message.error.code,
3307
+ parsed.message.error.message,
3308
+ parsed.message.error.data
3309
+ )
3310
+ );
3311
+ }
3312
+
3313
+ private handleIncomingRequest(message: JsonRpcRequest, handler: JsonRpcRequestHandler): void {
3314
+ const activeRequest: ActiveIncomingRequest = {
3315
+ cancelled: false,
3316
+ };
3317
+ this.activeIncomingRequests.set(message.id, activeRequest);
3318
+
3319
+ void (async () => {
3320
+ try {
3321
+ const result = await handler(message.params, {
3322
+ id: message.id,
3323
+ method: message.method,
3324
+ });
3325
+
3326
+ if (this.disposedError !== undefined || activeRequest.cancelled) {
3327
+ return;
3328
+ }
3329
+
3330
+ this.output.write(
3331
+ serializeJsonRpcMessage({
3332
+ jsonrpc: "2.0",
3333
+ id: message.id,
3334
+ result,
3335
+ })
3336
+ );
3337
+ } catch (error) {
3338
+ if (this.disposedError !== undefined || activeRequest.cancelled) {
3339
+ return;
3340
+ }
3341
+
3342
+ const errorMessage = error instanceof Error ? error.message : String(error);
3343
+ this.output.write(
3344
+ serializeJsonRpcMessage({
3345
+ jsonrpc: "2.0",
3346
+ id: message.id,
3347
+ error: {
3348
+ code: ERROR_INTERNAL,
3349
+ message: errorMessage,
3350
+ },
3351
+ })
3352
+ );
3353
+ } finally {
3354
+ const inFlightRequest = this.activeIncomingRequests.get(message.id);
3355
+ if (inFlightRequest === activeRequest) {
3356
+ this.activeIncomingRequests.delete(message.id);
3357
+ }
3358
+ }
3359
+ })();
3360
+ }
3361
+
3362
+ private handleCancellationNotification(params: unknown): void {
3363
+ if (!isObjectRecord(params)) {
3364
+ return;
3365
+ }
3366
+
3367
+ const requestId = params.requestId;
3368
+ if (!isRequestId(requestId)) {
3369
+ return;
3370
+ }
3371
+
3372
+ const activeRequest = this.activeIncomingRequests.get(requestId);
3373
+ if (activeRequest === undefined) {
3374
+ return;
3375
+ }
3376
+
3377
+ activeRequest.cancelled = true;
3378
+ this.activeIncomingRequests.delete(requestId);
3379
+ }
3380
+ }
3381
+
3382
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
3383
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3384
+ }
3385
+
3386
+ function hasOwn(
3387
+ value: Record<string, unknown>,
3388
+ property: string
3389
+ ): property is keyof typeof value {
3390
+ return Object.prototype.hasOwnProperty.call(value, property);
3391
+ }
3392
+
3393
+ function isRequestId(value: unknown): value is RequestId {
3394
+ return typeof value === "string" || typeof value === "number";
3395
+ }
3396
+
3397
+ function isLogLevel(value: unknown): value is LogLevel {
3398
+ return (
3399
+ value === "debug" ||
3400
+ value === "info" ||
3401
+ value === "notice" ||
3402
+ value === "warning" ||
3403
+ value === "error" ||
3404
+ value === "critical" ||
3405
+ value === "alert" ||
3406
+ value === "emergency"
3407
+ );
3408
+ }
3409
+
3410
+ function toRequestId(value: unknown): RequestId | null {
3411
+ return isRequestId(value) ? value : null;
3412
+ }
3413
+
3414
+ function parseError(): McpError {
3415
+ return new McpError(ERROR_PARSE, "Parse error");
3416
+ }
3417
+
3418
+ function invalidRequest(): McpError {
3419
+ return new McpError(ERROR_INVALID_REQUEST, "Invalid Request");
3420
+ }
3421
+
3422
+ function isJsonRpcErrorObject(value: unknown): value is JsonRpcErrorObject {
3423
+ if (!isObjectRecord(value)) {
3424
+ return false;
3425
+ }
3426
+
3427
+ if (typeof value.code !== "number" || typeof value.message !== "string") {
3428
+ return false;
3429
+ }
3430
+
3431
+ return value.data === undefined || hasOwn(value, "data");
3432
+ }
3433
+
3434
+ export function parseJsonRpcMessage(line: string): ParsedJsonRpcMessage {
3435
+ let parsed: unknown;
3436
+ try {
3437
+ parsed = JSON.parse(line);
3438
+ } catch {
3439
+ return {
3440
+ type: "invalid",
3441
+ id: null,
3442
+ error: parseError(),
3443
+ };
3444
+ }
3445
+
3446
+ return parseJsonRpcPayload(parsed);
3447
+ }
3448
+
3449
+ function parseJsonRpcPayload(parsed: unknown): ParsedJsonRpcMessage {
3450
+ if (!isObjectRecord(parsed)) {
3451
+ return {
3452
+ type: "invalid",
3453
+ id: null,
3454
+ error: invalidRequest(),
3455
+ };
3456
+ }
3457
+
3458
+ const id = toRequestId(parsed.id);
3459
+
3460
+ if (parsed.jsonrpc !== "2.0") {
3461
+ return {
3462
+ type: "invalid",
3463
+ id,
3464
+ error: invalidRequest(),
3465
+ };
3466
+ }
3467
+
3468
+ const hasMethod = hasOwn(parsed, "method");
3469
+ const hasId = hasOwn(parsed, "id");
3470
+
3471
+ if (hasMethod) {
3472
+ if (typeof parsed.method !== "string") {
3473
+ return {
3474
+ type: "invalid",
3475
+ id,
3476
+ error: invalidRequest(),
3477
+ };
3478
+ }
3479
+
3480
+ if (hasId) {
3481
+ if (!isRequestId(parsed.id)) {
3482
+ return {
3483
+ type: "invalid",
3484
+ id: null,
3485
+ error: invalidRequest(),
3486
+ };
3487
+ }
3488
+
3489
+ const request: JsonRpcRequest = {
3490
+ jsonrpc: "2.0",
3491
+ id: parsed.id,
3492
+ method: parsed.method,
3493
+ };
3494
+
3495
+ if (hasOwn(parsed, "params")) {
3496
+ request.params = parsed.params;
3497
+ }
3498
+
3499
+ return {
3500
+ type: "request",
3501
+ message: request,
3502
+ };
3503
+ }
3504
+
3505
+ const notification: JsonRpcNotification = {
3506
+ jsonrpc: "2.0",
3507
+ method: parsed.method,
3508
+ };
3509
+
3510
+ if (hasOwn(parsed, "params")) {
3511
+ notification.params = parsed.params;
3512
+ }
3513
+
3514
+ return {
3515
+ type: "notification",
3516
+ message: notification,
3517
+ };
3518
+ }
3519
+
3520
+ if (!hasId || !isRequestId(parsed.id)) {
3521
+ return {
3522
+ type: "invalid",
3523
+ id,
3524
+ error: invalidRequest(),
3525
+ };
3526
+ }
3527
+
3528
+ const hasResult = hasOwn(parsed, "result");
3529
+ const hasError = hasOwn(parsed, "error");
3530
+
3531
+ if (hasResult === hasError) {
3532
+ return {
3533
+ type: "invalid",
3534
+ id: parsed.id,
3535
+ error: invalidRequest(),
3536
+ };
3537
+ }
3538
+
3539
+ if (hasResult) {
3540
+ return {
3541
+ type: "response",
3542
+ message: {
3543
+ jsonrpc: "2.0",
3544
+ id: parsed.id,
3545
+ result: parsed.result,
3546
+ },
3547
+ };
3548
+ }
3549
+
3550
+ if (!isJsonRpcErrorObject(parsed.error)) {
3551
+ return {
3552
+ type: "invalid",
3553
+ id: parsed.id,
3554
+ error: invalidRequest(),
3555
+ };
3556
+ }
3557
+
3558
+ return {
3559
+ type: "response",
3560
+ message: {
3561
+ jsonrpc: "2.0",
3562
+ id: parsed.id,
3563
+ error: parsed.error,
3564
+ },
3565
+ };
3566
+ }