toolcraft 0.0.232 → 0.0.234

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 (180) hide show
  1. package/composition.json +3 -3
  2. package/dist/applied-default.d.ts +3 -0
  3. package/dist/applied-default.js +16 -0
  4. package/dist/cli.js +508 -204
  5. package/dist/composition.json +3 -3
  6. package/dist/discriminator.d.ts +9 -0
  7. package/dist/discriminator.js +32 -0
  8. package/dist/error-report.d.ts +2 -18
  9. package/dist/error-report.js +12 -14
  10. package/dist/http.js +8 -1
  11. package/dist/human-in-loop/gate.js +6 -0
  12. package/dist/index.compile-check.js +22 -5
  13. package/dist/index.d.ts +12 -5
  14. package/dist/index.js +50 -0
  15. package/dist/json-schema-converter.d.ts +14 -6
  16. package/dist/json-schema-converter.js +150 -20
  17. package/dist/mcp-proxy-metadata.d.ts +2 -0
  18. package/dist/mcp-proxy-metadata.js +13 -0
  19. package/dist/mcp-proxy.d.ts +1 -0
  20. package/dist/mcp-proxy.js +65 -47
  21. package/dist/mcp-result.d.ts +3 -0
  22. package/dist/mcp-result.js +10 -0
  23. package/dist/mcp.js +211 -153
  24. package/dist/redaction.js +26 -17
  25. package/dist/renderer.js +40 -29
  26. package/dist/schema-member-names.d.ts +2 -0
  27. package/dist/schema-member-names.js +39 -0
  28. package/dist/schema-scope.d.ts +1 -0
  29. package/dist/schema-scope.js +29 -16
  30. package/dist/schema.d.ts +1 -1
  31. package/dist/sdk.d.ts +2 -1
  32. package/dist/sdk.js +164 -50
  33. package/dist/stream.js +24 -23
  34. package/dist/testing/diagnostic-value.d.ts +1 -0
  35. package/dist/testing/diagnostic-value.js +57 -0
  36. package/dist/testing/fakes.js +2 -1
  37. package/dist/testing/harness.js +1 -0
  38. package/dist/testing/memory-fs.js +42 -1
  39. package/dist/testing/parity.js +50 -6
  40. package/dist/union-validation.d.ts +5 -0
  41. package/dist/union-validation.js +36 -0
  42. package/node_modules/@poe-code/process-runner/dist/types.d.ts +2 -0
  43. package/node_modules/mcp-oauth-server/dist/index.js +47 -6
  44. package/node_modules/tiny-http-mcp-server/dist/cli.js +14 -2
  45. package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
  46. package/node_modules/tiny-http-mcp-server/dist/http-server.d.ts +7 -5
  47. package/node_modules/tiny-http-mcp-server/dist/http-server.js +3 -11
  48. package/node_modules/tiny-http-mcp-server/dist/http-transport.d.ts +6 -1
  49. package/node_modules/tiny-http-mcp-server/dist/http-transport.js +196 -15
  50. package/node_modules/tiny-http-mcp-server/dist/index.d.ts +2 -2
  51. package/node_modules/tiny-http-mcp-server/dist/modern-headers.d.ts +3 -0
  52. package/node_modules/tiny-http-mcp-server/dist/modern-headers.js +35 -0
  53. package/node_modules/tiny-http-mcp-server/dist/parse-body.d.ts +1 -0
  54. package/node_modules/tiny-http-mcp-server/dist/parse-body.js +9 -5
  55. package/node_modules/tiny-http-mcp-server/dist/test-support.js +17 -0
  56. package/node_modules/tiny-http-mcp-server/dist/testing.d.ts +1 -1
  57. package/node_modules/tiny-http-mcp-server/dist/testing.js +3 -2
  58. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +21 -4
  59. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +1 -1
  60. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +14 -6
  61. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/http-fetch.d.ts +1 -0
  62. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/http-fetch.js +8 -0
  63. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/http-response.d.ts +1 -0
  64. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/http-response.js +50 -0
  65. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +2 -0
  66. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +2 -0
  67. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +21 -3
  68. package/node_modules/tiny-http-mcp-server/package.json +1 -1
  69. package/node_modules/tiny-mcp-client/README.md +1 -0
  70. package/node_modules/tiny-mcp-client/dist/index.d.ts +178 -101
  71. package/node_modules/tiny-mcp-client/dist/index.js +4696 -291
  72. package/node_modules/tiny-mcp-client/package.json +2 -1
  73. package/node_modules/tiny-stdio-mcp-server/dist/base64.d.ts +1 -0
  74. package/node_modules/tiny-stdio-mcp-server/dist/base64.js +5 -0
  75. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  76. package/node_modules/tiny-stdio-mcp-server/dist/content/audio.d.ts +3 -0
  77. package/node_modules/tiny-stdio-mcp-server/dist/content/convert.d.ts +5 -1
  78. package/node_modules/tiny-stdio-mcp-server/dist/content/convert.js +41 -6
  79. package/node_modules/tiny-stdio-mcp-server/dist/content/file.d.ts +5 -0
  80. package/node_modules/tiny-stdio-mcp-server/dist/content/image.d.ts +3 -0
  81. package/node_modules/tiny-stdio-mcp-server/dist/headers.d.ts +10 -0
  82. package/node_modules/tiny-stdio-mcp-server/dist/headers.js +180 -0
  83. package/node_modules/tiny-stdio-mcp-server/dist/index.d.ts +5 -5
  84. package/node_modules/tiny-stdio-mcp-server/dist/index.js +1 -1
  85. package/node_modules/tiny-stdio-mcp-server/dist/input-request-schema.d.ts +220 -0
  86. package/node_modules/tiny-stdio-mcp-server/dist/input-request-schema.js +1995 -0
  87. package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.d.ts +1 -0
  88. package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.js +38 -25
  89. package/node_modules/tiny-stdio-mcp-server/dist/metadata.d.ts +2 -0
  90. package/node_modules/tiny-stdio-mcp-server/dist/metadata.js +37 -0
  91. package/node_modules/tiny-stdio-mcp-server/dist/protocol-validation.d.ts +4 -0
  92. package/node_modules/tiny-stdio-mcp-server/dist/protocol-validation.js +53 -0
  93. package/node_modules/tiny-stdio-mcp-server/dist/protocol.d.ts +17 -0
  94. package/node_modules/tiny-stdio-mcp-server/dist/protocol.js +228 -0
  95. package/node_modules/tiny-stdio-mcp-server/dist/request-cancellation.d.ts +1 -0
  96. package/node_modules/tiny-stdio-mcp-server/dist/request-cancellation.js +18 -0
  97. package/node_modules/tiny-stdio-mcp-server/dist/sampling-messages.d.ts +18 -0
  98. package/node_modules/tiny-stdio-mcp-server/dist/sampling-messages.js +34 -0
  99. package/node_modules/tiny-stdio-mcp-server/dist/schema.d.ts +4 -1
  100. package/node_modules/tiny-stdio-mcp-server/dist/server.d.ts +9 -4
  101. package/node_modules/tiny-stdio-mcp-server/dist/server.js +334 -135
  102. package/node_modules/tiny-stdio-mcp-server/dist/stdio-input.d.ts +3 -0
  103. package/node_modules/tiny-stdio-mcp-server/dist/stdio-input.js +50 -28
  104. package/node_modules/tiny-stdio-mcp-server/dist/subscriptions.d.ts +11 -0
  105. package/node_modules/tiny-stdio-mcp-server/dist/subscriptions.js +128 -0
  106. package/node_modules/tiny-stdio-mcp-server/dist/types.d.ts +58 -18
  107. package/node_modules/tiny-stdio-mcp-server/dist/types.js +2 -1
  108. package/node_modules/tiny-stdio-mcp-server/dist/uri.d.ts +1 -0
  109. package/node_modules/tiny-stdio-mcp-server/dist/uri.js +41 -0
  110. package/node_modules/tiny-stdio-mcp-server/package.json +8 -0
  111. package/node_modules/toolcraft-design/dist/command-registry.d.ts +11 -0
  112. package/node_modules/toolcraft-design/dist/command-registry.js +24 -0
  113. package/node_modules/toolcraft-design/dist/dashboard/ansi.d.ts +2 -0
  114. package/node_modules/toolcraft-design/dist/dashboard/ansi.js +81 -27
  115. package/node_modules/toolcraft-design/dist/dashboard/buffer.js +39 -25
  116. package/node_modules/toolcraft-design/dist/dashboard/components/border.js +6 -5
  117. package/node_modules/toolcraft-design/dist/dashboard/components/footer.js +26 -21
  118. package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.d.ts +1 -1
  119. package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.js +12 -6
  120. package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.d.ts +2 -1
  121. package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.js +51 -10
  122. package/node_modules/toolcraft-design/dist/dashboard/dashboard.d.ts +4 -0
  123. package/node_modules/toolcraft-design/dist/dashboard/dashboard.js +77 -4
  124. package/node_modules/toolcraft-design/dist/dashboard/index.d.ts +2 -0
  125. package/node_modules/toolcraft-design/dist/dashboard/index.js +2 -0
  126. package/node_modules/toolcraft-design/dist/dashboard/keymap.js +26 -6
  127. package/node_modules/toolcraft-design/dist/dashboard/layout.d.ts +1 -0
  128. package/node_modules/toolcraft-design/dist/dashboard/layout.js +19 -3
  129. package/node_modules/toolcraft-design/dist/dashboard/line-buffer.d.ts +9 -0
  130. package/node_modules/toolcraft-design/dist/dashboard/line-buffer.js +63 -0
  131. package/node_modules/toolcraft-design/dist/dashboard/output-preview.d.ts +9 -0
  132. package/node_modules/toolcraft-design/dist/dashboard/output-preview.js +70 -0
  133. package/node_modules/toolcraft-design/dist/dashboard/snapshot.js +3 -1
  134. package/node_modules/toolcraft-design/dist/dashboard/store.js +9 -3
  135. package/node_modules/toolcraft-design/dist/dashboard/terminal-strings.d.ts +6 -0
  136. package/node_modules/toolcraft-design/dist/dashboard/terminal-strings.js +108 -0
  137. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +3 -1
  138. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +11 -1
  139. package/node_modules/toolcraft-design/dist/dashboard/testing/pipeline-scenario.d.ts +1 -0
  140. package/node_modules/toolcraft-design/dist/dashboard/testing/pipeline-scenario.js +126 -0
  141. package/node_modules/toolcraft-design/dist/dashboard/types.d.ts +3 -1
  142. package/node_modules/toolcraft-design/dist/event-groups.d.ts +19 -0
  143. package/node_modules/toolcraft-design/dist/event-groups.js +49 -0
  144. package/node_modules/toolcraft-design/dist/index.d.ts +6 -1
  145. package/node_modules/toolcraft-design/dist/index.js +5 -1
  146. package/node_modules/toolcraft-design/dist/inline-notice.d.ts +14 -0
  147. package/node_modules/toolcraft-design/dist/inline-notice.js +28 -0
  148. package/node_modules/toolcraft-design/dist/metric.d.ts +8 -0
  149. package/node_modules/toolcraft-design/dist/metric.js +21 -0
  150. package/node_modules/toolcraft-design/dist/overlay-manager.d.ts +6 -0
  151. package/node_modules/toolcraft-design/dist/overlay-manager.js +10 -0
  152. package/node_modules/toolcraft-design/dist/primitives.d.ts +12 -0
  153. package/node_modules/toolcraft-design/dist/primitives.js +7 -0
  154. package/node_modules/toolcraft-design/dist/progress-group.d.ts +7 -0
  155. package/node_modules/toolcraft-design/dist/progress-group.js +10 -0
  156. package/node_modules/toolcraft-design/dist/render-performance.d.ts +33 -0
  157. package/node_modules/toolcraft-design/dist/render-performance.js +71 -0
  158. package/node_modules/toolcraft-design/dist/task-tree.d.ts +22 -0
  159. package/node_modules/toolcraft-design/dist/task-tree.js +79 -0
  160. package/node_modules/toolcraft-design/dist/viewport.d.ts +18 -0
  161. package/node_modules/toolcraft-design/dist/viewport.js +56 -0
  162. package/node_modules/toolcraft-schema/dist/clone-default.d.ts +1 -0
  163. package/node_modules/toolcraft-schema/dist/clone-default.js +26 -0
  164. package/node_modules/toolcraft-schema/dist/index.compile-check.js +1 -0
  165. package/node_modules/toolcraft-schema/dist/index.d.ts +18 -9
  166. package/node_modules/toolcraft-schema/dist/index.js +46 -8
  167. package/node_modules/toolcraft-schema/dist/json-schema/compiler.d.ts +1 -0
  168. package/node_modules/toolcraft-schema/dist/json-schema/compiler.js +6 -0
  169. package/node_modules/toolcraft-schema/dist/json-schema/evaluate.js +6 -3
  170. package/node_modules/toolcraft-schema/dist/json-schema/normalize-nullability.d.ts +1 -0
  171. package/node_modules/toolcraft-schema/dist/json-schema/normalize-nullability.js +69 -0
  172. package/node_modules/toolcraft-schema/dist/json-schema/types.d.ts +1 -0
  173. package/node_modules/toolcraft-schema/dist/json.d.ts +4 -1
  174. package/node_modules/toolcraft-schema/dist/json.js +57 -1
  175. package/node_modules/toolcraft-schema/dist/native-json-schema.d.ts +10 -0
  176. package/node_modules/toolcraft-schema/dist/native-json-schema.js +11 -0
  177. package/node_modules/toolcraft-schema/dist/validate.d.ts +5 -1
  178. package/node_modules/toolcraft-schema/dist/validate.js +61 -47
  179. package/node_modules/toolcraft-schema/package.json +1 -1
  180. package/package.json +6 -3
@@ -4,6 +4,7 @@ import { formatErrorResponse, formatSuccessResponse } from "tiny-stdio-mcp-serve
4
4
  import { JsonRpcMessageError, readAndClassifyBody } from "./parse-body.js";
5
5
  import { createSessionStore, defaultSessionIdGenerator } from "./session.js";
6
6
  import { formatSseEvent, SSE_HEADERS } from "./sse.js";
7
+ import { validateModernHeaders } from "./modern-headers.js";
7
8
  const ALLOWED_METHODS = "POST, GET, DELETE, OPTIONS";
8
9
  const MCP_SESSION_ID_HEADER = "Mcp-Session-Id";
9
10
  const LOCAL_HOSTS = ["localhost", "127.0.0.1", "::1"];
@@ -25,6 +26,7 @@ export class StreamableHttpTransport {
25
26
  allowedOrigins;
26
27
  allowedHosts;
27
28
  maxRequestBytes;
29
+ maxResponseBytes;
28
30
  maxBatchSize;
29
31
  maxSessions;
30
32
  maxSessionsPerSubject;
@@ -45,6 +47,8 @@ export class StreamableHttpTransport {
45
47
  responseRequestIds = new WeakMap();
46
48
  responseOrigins = new WeakMap();
47
49
  responseRejectionReasons = new WeakMap();
50
+ modernRequests = new Map();
51
+ modernStreams = new Set();
48
52
  nextNotificationEventId = 1;
49
53
  nextRequestId = 1;
50
54
  activeToolCalls = 0;
@@ -62,10 +66,13 @@ export class StreamableHttpTransport {
62
66
  this.allowedHosts = new Set((options.allowedHosts ?? LOCAL_HOSTS).map((host) => this.normalizeHost(host)));
63
67
  this.maxRequestBytes = validateOptionalIntegerOption("maxRequestBytes", options.maxRequestBytes, 1);
64
68
  this.maxBatchSize = validateOptionalIntegerOption("maxBatchSize", options.maxBatchSize, 1);
69
+ this.maxResponseBytes = validateOptionalIntegerOption("maxResponseBytes", options.maxResponseBytes, 1) ?? 16 * 1024 * 1024;
65
70
  this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1) ?? 128;
66
71
  this.maxSessionsPerSubject =
67
- validateOptionalIntegerOption("maxSessionsPerSubject", options.maxSessionsPerSubject, 1) ?? 16;
68
- this.sessionTtlMs = validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1) ?? 15 * 60_000;
72
+ validateOptionalIntegerOption("maxSessionsPerSubject", options.maxSessionsPerSubject, 1) ??
73
+ 16;
74
+ this.sessionTtlMs =
75
+ validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1) ?? 15 * 60_000;
69
76
  this.maxStreamsPerSession =
70
77
  validateOptionalIntegerOption("maxStreamsPerSession", options.maxStreamsPerSession, 1) ?? 1;
71
78
  this.maxStreamBufferBytes =
@@ -120,6 +127,12 @@ export class StreamableHttpTransport {
120
127
  this.respondWithRejection(res, 403, "origin_not_allowed", `Origin ${JSON.stringify(this.readOrigin(req) ?? "")} is not allowed; add it to allowedOrigins.`);
121
128
  return;
122
129
  }
130
+ if (req.headers["mcp-protocol-version"] === "2026-07-28" &&
131
+ req.method !== "POST" &&
132
+ req.method !== "OPTIONS") {
133
+ this.respondWithStatus(res, 405, undefined, { Allow: "POST, OPTIONS" });
134
+ return;
135
+ }
123
136
  switch (req.method) {
124
137
  case "POST":
125
138
  await this.handlePost(req, res);
@@ -172,6 +185,12 @@ export class StreamableHttpTransport {
172
185
  }
173
186
  async close() {
174
187
  this.closed = true;
188
+ for (const [response, controller] of this.modernRequests) {
189
+ controller.abort();
190
+ response.end();
191
+ }
192
+ this.modernRequests.clear();
193
+ this.modernStreams.clear();
175
194
  if (this.sessionExpiryInterval !== undefined) {
176
195
  clearInterval(this.sessionExpiryInterval);
177
196
  this.sessionExpiryInterval = undefined;
@@ -221,6 +240,20 @@ export class StreamableHttpTransport {
221
240
  this.respondWithJsonRpcError(res, 400, code, message, id);
222
241
  return;
223
242
  }
243
+ const modern = req.headers["mcp-protocol-version"] === "2026-07-28" ||
244
+ classified.messages.some((message) => {
245
+ if (!("method" in message))
246
+ return false;
247
+ const metadata = message.params?._meta;
248
+ return (message.method === "server/discover" ||
249
+ (typeof metadata === "object" &&
250
+ metadata !== null &&
251
+ Object.prototype.hasOwnProperty.call(metadata, "io.modelcontextprotocol/protocolVersion")));
252
+ });
253
+ if (modern) {
254
+ await this.handleModernPost(req, res, classified);
255
+ return;
256
+ }
224
257
  const initMessage = classified.messages.find((message) => this.isRequest(message) && message.method === "initialize");
225
258
  let sessionId;
226
259
  let activeSession;
@@ -237,7 +270,8 @@ export class StreamableHttpTransport {
237
270
  return;
238
271
  }
239
272
  const authSubject = this.readAuthSubject(req);
240
- if (authSubject !== undefined && this.sessionCount(authSubject) >= this.maxSessionsPerSubject) {
273
+ if (authSubject !== undefined &&
274
+ this.sessionCount(authSubject) >= this.maxSessionsPerSubject) {
241
275
  this.respondWithRejection(res, 429, "subject_session_limit_reached", "The authenticated subject has reached its session limit; close a session or retry later.");
242
276
  return;
243
277
  }
@@ -283,6 +317,15 @@ export class StreamableHttpTransport {
283
317
  if (!("method" in message)) {
284
318
  continue;
285
319
  }
320
+ if (this.sessionIdGenerator === undefined &&
321
+ (message.method === "resources/subscribe" || message.method === "resources/unsubscribe")) {
322
+ if (this.isRequest(message))
323
+ responses.push(formatErrorResponse(message.id, {
324
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
325
+ message: "Method not found"
326
+ }));
327
+ continue;
328
+ }
286
329
  const session = activeSession;
287
330
  if (session !== undefined &&
288
331
  message.method !== "initialize" &&
@@ -333,7 +376,23 @@ export class StreamableHttpTransport {
333
376
  this.activeToolCalls -= 1;
334
377
  }
335
378
  }
336
- const { error, result } = handled;
379
+ const { error } = handled;
380
+ let { result } = handled;
381
+ if (error === undefined &&
382
+ message.method === "initialize" &&
383
+ result !== undefined &&
384
+ this.sessionIdGenerator === undefined) {
385
+ const initialization = result;
386
+ const capabilities = structuredClone(initialization.capabilities);
387
+ for (const category of ["tools", "prompts", "resources"]) {
388
+ const capability = capabilities[category];
389
+ if (capability !== undefined)
390
+ delete capability.listChanged;
391
+ }
392
+ if (capabilities.resources !== undefined)
393
+ delete capabilities.resources.subscribe;
394
+ result = { ...initialization, capabilities };
395
+ }
337
396
  if (isToolCall) {
338
397
  this.emit({
339
398
  type: "tool.end",
@@ -380,12 +439,115 @@ export class StreamableHttpTransport {
380
439
  this.respondWithStatus(res, 200, sessionId, { "Content-Type": "application/json" }, body);
381
440
  return;
382
441
  }
442
+ const frames = formattedResponses.map((data) => formatSseEvent({ data }));
443
+ if (frames.some((frame) => Buffer.byteLength(frame, "utf8") > this.maxResponseBytes)) {
444
+ res.destroy();
445
+ return;
446
+ }
383
447
  res.writeHead(200, this.withSessionHeader(SSE_HEADERS, sessionId, res));
384
- for (const formattedResponse of formattedResponses) {
385
- res.write(formatSseEvent({ data: formattedResponse }));
448
+ for (const frame of frames) {
449
+ this.writeToLiveStream(res, frame);
386
450
  }
387
451
  res.end();
388
452
  }
453
+ async handleModernPost(req, res, body) {
454
+ const message = body.messages[0];
455
+ if (body.isBatch ||
456
+ body.messages.length !== 1 ||
457
+ message === undefined ||
458
+ !("method" in message)) {
459
+ this.respondWithJsonRpcError(res, 400, JSON_RPC_ERROR_CODES.INVALID_REQUEST, "Modern HTTP requires a single request or notification");
460
+ return;
461
+ }
462
+ const id = this.isRequest(message) ? message.id : null;
463
+ if (!this.acceptsResponseType(req, "application/json") ||
464
+ !this.acceptsResponseType(req, "text/event-stream")) {
465
+ this.respondWithRejection(res, 406, "response_type_not_acceptable", "Accept must allow application/json and text/event-stream.");
466
+ return;
467
+ }
468
+ const headerError = validateModernHeaders(req.headers, message);
469
+ if (headerError !== undefined) {
470
+ this.respondWithJsonRpcError(res, 400, headerError.code, headerError.message, id);
471
+ return;
472
+ }
473
+ const controller = new AbortController();
474
+ const cancel = () => controller.abort();
475
+ res.once("close", cancel);
476
+ req.once("aborted", cancel);
477
+ this.modernRequests.set(res, controller);
478
+ let streaming = false;
479
+ const write = (data) => {
480
+ if (controller.signal.aborted || res.destroyed || res.writableEnded)
481
+ return;
482
+ const frame = formatSseEvent({ data });
483
+ if (Buffer.byteLength(frame, "utf8") > this.maxResponseBytes) {
484
+ controller.abort(new Error("Modern SSE response size limit exceeded"));
485
+ res.destroy();
486
+ return;
487
+ }
488
+ if ((res.writableLength ?? 0) > this.maxStreamBufferBytes) {
489
+ controller.abort(new Error("Modern SSE output buffer limit exceeded"));
490
+ res.destroy();
491
+ return;
492
+ }
493
+ if (!streaming) {
494
+ res.writeHead(200, this.withSessionHeader({ ...SSE_HEADERS, "X-Accel-Buffering": "no" }, undefined, res));
495
+ streaming = true;
496
+ res.flushHeaders();
497
+ this.modernStreams.add(res);
498
+ this.startSseKeepAlive();
499
+ }
500
+ res.write(frame);
501
+ };
502
+ const session = this.server.createMessageSession((notification) => write(JSON.stringify(notification)));
503
+ try {
504
+ if (!this.isRequest(message) && !message.method.startsWith("notifications/")) {
505
+ this.respondWithJsonRpcError(res, 400, JSON_RPC_ERROR_CODES.INVALID_REQUEST, "A request ID is required");
506
+ return;
507
+ }
508
+ const handled = await this.runWithRequestContext(req, () => session.handleMessage(message.method, message.params, {
509
+ ...(this.isRequest(message) ? { requestId: message.id } : {}),
510
+ signal: controller.signal,
511
+ parameterHeaders: req.headers
512
+ }));
513
+ if (controller.signal.aborted || res.destroyed || res.writableEnded)
514
+ return;
515
+ if (!this.isRequest(message) ||
516
+ (handled.result === undefined && handled.error === undefined)) {
517
+ if (streaming)
518
+ res.end();
519
+ else
520
+ this.respondWithStatus(res, 202);
521
+ return;
522
+ }
523
+ const formatted = handled.error === undefined
524
+ ? formatSuccessResponse(message.id, handled.result)
525
+ : formatErrorResponse(message.id, handled.error);
526
+ const status = handled.error?.code === JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND
527
+ ? 404
528
+ : handled.error?.code === JSON_RPC_ERROR_CODES.INVALID_PARAMS ||
529
+ handled.error?.code === JSON_RPC_ERROR_CODES.UNSUPPORTED_PROTOCOL_VERSION ||
530
+ handled.error?.code === -32020 ||
531
+ handled.error?.code === -32021
532
+ ? 400
533
+ : 200;
534
+ if (streaming || (!this.enableJsonResponse && status === 200)) {
535
+ write(formatted);
536
+ if (!res.destroyed)
537
+ res.end();
538
+ }
539
+ else
540
+ this.respondWithStatus(res, status, undefined, { "Content-Type": "application/json" }, formatted);
541
+ }
542
+ finally {
543
+ session.close();
544
+ res.off("close", cancel);
545
+ req.off("aborted", cancel);
546
+ this.modernRequests.delete(res);
547
+ this.modernStreams.delete(res);
548
+ this.stopSseKeepAliveIfIdle();
549
+ }
550
+ }
389
551
  async handleGet(req, res) {
390
552
  if (this.sessionIdGenerator === undefined) {
391
553
  this.respondWithStatus(res, 405, undefined, {
@@ -675,10 +837,14 @@ export class StreamableHttpTransport {
675
837
  return;
676
838
  }
677
839
  this.sseKeepAliveInterval = setInterval(() => {
840
+ for (const response of this.modernStreams) {
841
+ if (!response.writableEnded)
842
+ this.writeToLiveStream(response, ": keepalive\n\n");
843
+ }
678
844
  for (const streams of this.sseStreams.values()) {
679
845
  for (const response of streams) {
680
846
  if (!response.writableEnded) {
681
- this.writeToLiveGetStream(response, ": keepalive\n\n");
847
+ this.writeToLiveStream(response, ": keepalive\n\n");
682
848
  }
683
849
  }
684
850
  }
@@ -686,7 +852,8 @@ export class StreamableHttpTransport {
686
852
  this.sseKeepAliveInterval.unref();
687
853
  }
688
854
  stopSseKeepAliveIfIdle() {
689
- if ([...this.sseStreams.values()].some((streams) => streams.size > 0)) {
855
+ if (this.modernStreams.size > 0 ||
856
+ [...this.sseStreams.values()].some((streams) => streams.size > 0)) {
690
857
  return;
691
858
  }
692
859
  this.stopSseKeepAlive();
@@ -704,15 +871,19 @@ export class StreamableHttpTransport {
704
871
  }
705
872
  const id = this.nextNotificationEventId++;
706
873
  const data = JSON.stringify(notification);
707
- this.recordSseEvent(sessionId, id, data);
708
874
  const streams = this.sseStreams.get(sessionId);
709
- if (streams === undefined) {
710
- return;
711
- }
712
875
  const event = formatSseEvent({
713
876
  id: String(id),
714
877
  data
715
878
  });
879
+ if (Buffer.byteLength(event, "utf8") > this.maxResponseBytes) {
880
+ for (const response of streams ?? [])
881
+ response.destroy();
882
+ return;
883
+ }
884
+ this.recordSseEvent(sessionId, id, data);
885
+ if (streams === undefined)
886
+ return;
716
887
  let latestResponse;
717
888
  for (const response of streams) {
718
889
  if (!response.writableEnded) {
@@ -720,10 +891,16 @@ export class StreamableHttpTransport {
720
891
  }
721
892
  }
722
893
  if (latestResponse !== undefined) {
723
- this.writeToLiveGetStream(latestResponse, event);
894
+ this.writeToLiveStream(latestResponse, event);
724
895
  }
725
896
  }
726
- writeToLiveGetStream(response, data) {
897
+ writeToLiveStream(response, data) {
898
+ if (response.destroyed || response.writableEnded)
899
+ return;
900
+ if (Buffer.byteLength(data, "utf8") > this.maxResponseBytes) {
901
+ response.destroy();
902
+ return;
903
+ }
727
904
  if ((response.writableLength ?? 0) > this.maxStreamBufferBytes) {
728
905
  response.end();
729
906
  return;
@@ -754,7 +931,7 @@ export class StreamableHttpTransport {
754
931
  const history = this.sseEventHistory.get(sessionId) ?? [];
755
932
  for (const event of history) {
756
933
  if (event.id > lastEventId && !res.writableEnded) {
757
- this.writeToLiveGetStream(res, formatSseEvent({ id: String(event.id), data: event.data }));
934
+ this.writeToLiveStream(res, formatSseEvent({ id: String(event.id), data: event.data }));
758
935
  }
759
936
  }
760
937
  }
@@ -877,6 +1054,10 @@ export class StreamableHttpTransport {
877
1054
  this.respondWithStatus(res, statusCode, undefined, { "Content-Type": "application/json" }, JSON.stringify({ error: reason, message }));
878
1055
  }
879
1056
  respondWithStatus(res, statusCode, sessionId, headers, body) {
1057
+ if (body !== undefined && Buffer.byteLength(body, "utf8") > this.maxResponseBytes) {
1058
+ res.destroy();
1059
+ return;
1060
+ }
880
1061
  res.writeHead(statusCode, this.withSessionHeader(headers, sessionId, res));
881
1062
  res.end(body);
882
1063
  }
@@ -1,9 +1,9 @@
1
1
  export { createServer, defineSchema, Image, Audio, File, toContentBlocks, fileTypeFromBuffer, JSON_RPC_ERROR_CODES, ToolError } from "tiny-stdio-mcp-server";
2
- export type { Server, TypedSchema, ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, ToolReturn, ServerOptions, ToolHandler, ToolDefinition, Tool, ToolAnnotations, ToolExecution, Icon, ContentAnnotations, ResourceLink, CallToolResult, PromptContentItem, PromptArgument, Prompt, PromptMessage, GetPromptResult, PromptHandler, PromptDefinition, Resource, ResourceTemplate, ResourceContents, ReadResourceResult, ResourceHandler, ResourceDefinition, ResourceTemplateDefinition, HandleResult, ContentItem, JSONSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult } from "tiny-stdio-mcp-server";
2
+ export type { Server, MessageRequestContext, MessageSession, MessageSessionContext, TypedSchema, TypedOutputSchema, ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, ToolReturn, ServerOptions, ToolHandler, HandlerRequestContext, InputRequiredResult, ToolDefinition, Tool, ToolAnnotations, ToolExecution, Icon, ContentAnnotations, ResourceLink, CallToolResult, PromptContentItem, PromptArgument, Prompt, PromptMessage, GetPromptResult, PromptHandler, PromptDefinition, Resource, ResourceTemplate, ResourceContents, ReadResourceResult, ResourceHandler, ResourceDefinition, ResourceTemplateDefinition, HandleResult, ContentItem, JSONSchema, OutputSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult, DiscoverResult } from "tiny-stdio-mcp-server";
3
3
  export { createExpressMiddleware, createExpressOAuthHandlers, createProtectedResourceMetadataRouter } from "./express-middleware.js";
4
4
  export type { CreateExpressOAuthHandlersOptions } from "./express-middleware.js";
5
5
  export { createHttpServer, createProtectedResourceMetadataDocument } from "./http-server.js";
6
- export type { HttpToolContext, HttpToolHandler, HttpAdditionalRequestHandler, HttpListenOptions, HttpServer, HttpServerHandle, HttpTransportOptions, ProtectedResourceMetadataOptions, TinyHttpMcpServerOAuthOptions } from "./http-server.js";
6
+ export type { HttpToolContext, HttpRequestContext, HttpToolHandler, HttpAdditionalRequestHandler, HttpListenOptions, HttpServer, HttpServerHandle, HttpTransportOptions, ProtectedResourceMetadataOptions, TinyHttpMcpServerOAuthOptions } from "./http-server.js";
7
7
  export { TokenVerificationError } from "./auth.js";
8
8
  export type { RequestAuthInfo, TokenVerifier, VerifiedAccessToken } from "./auth.js";
9
9
  export { StreamableHttpTransport } from "./http-transport.js";
@@ -0,0 +1,3 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+ import { type JSONRPCError, type JSONRPCNotification, type JSONRPCRequest } from "tiny-stdio-mcp-server";
3
+ export declare function validateModernHeaders(headers: IncomingHttpHeaders, request: JSONRPCRequest | JSONRPCNotification): JSONRPCError | undefined;
@@ -0,0 +1,35 @@
1
+ import { JSON_RPC_ERROR_CODES } from "tiny-stdio-mcp-server";
2
+ import { decodeHeaderValue } from "tiny-stdio-mcp-server/headers";
3
+ export function validateModernHeaders(headers, request) {
4
+ const metadata = request.params?._meta;
5
+ const version = typeof metadata === "object" && metadata !== null
6
+ ? metadata["io.modelcontextprotocol/protocolVersion"]
7
+ : undefined;
8
+ if (typeof version !== "string")
9
+ return {
10
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
11
+ message: "Missing protocol version request metadata"
12
+ };
13
+ const expected = { "mcp-protocol-version": version };
14
+ // Notifications require the protocol version header but no method/name mirrors.
15
+ if ("id" in request) {
16
+ expected["mcp-method"] = request.method;
17
+ if (request.method === "tools/call" || request.method === "prompts/get")
18
+ expected["mcp-name"] = request.params?.name;
19
+ else if (request.method === "resources/read")
20
+ expected["mcp-name"] = request.params?.uri;
21
+ }
22
+ for (const [field, value] of Object.entries(expected)) {
23
+ const header = headers[field];
24
+ const actual = field === "mcp-name"
25
+ ? decodeHeaderValue(header)
26
+ : typeof header === "string"
27
+ ? header
28
+ : undefined;
29
+ if (typeof value !== "string" || actual === undefined || actual !== value)
30
+ return {
31
+ code: -32020,
32
+ message: `Header mismatch: ${field} must match the request body`
33
+ };
34
+ }
35
+ }
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  import type { JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse } from "tiny-stdio-mcp-server";
3
3
  export interface ClassifiedBody {
4
+ isBatch: boolean;
4
5
  entries: Array<JSONRPCMessage | null>;
5
6
  messages: JSONRPCMessage[];
6
7
  hasRequests: boolean;
@@ -21,9 +21,7 @@ function isValidResponse(value) {
21
21
  if (!hasOwn(value, "id")) {
22
22
  return false;
23
23
  }
24
- if (value.id !== null &&
25
- typeof value.id !== "string" &&
26
- typeof value.id !== "number") {
24
+ if (value.id !== null && typeof value.id !== "string" && typeof value.id !== "number") {
27
25
  return false;
28
26
  }
29
27
  const hasResult = hasOwn(value, "result");
@@ -61,7 +59,12 @@ async function readStreamBody(req, maxBytes) {
61
59
  }
62
60
  chunks.push(bytes);
63
61
  }
64
- return Buffer.concat(chunks).toString("utf8");
62
+ try {
63
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
64
+ }
65
+ catch {
66
+ throw new Error("Parse error");
67
+ }
65
68
  }
66
69
  async function readRawBody(req, preParsed, options) {
67
70
  const reqWithBody = req;
@@ -145,6 +148,7 @@ export async function readAndClassifyBody(req, preParsed, options = {}) {
145
148
  entries.push(request);
146
149
  }
147
150
  return {
151
+ isBatch,
148
152
  entries,
149
153
  messages,
150
154
  hasRequests: requests.length > 0,
@@ -152,6 +156,6 @@ export async function readAndClassifyBody(req, preParsed, options = {}) {
152
156
  hasResponses: responses.length > 0,
153
157
  requests,
154
158
  notifications,
155
- responses,
159
+ responses
156
160
  };
157
161
  }
@@ -166,6 +166,7 @@ async function fetchInMemory(server, url, init) {
166
166
  response.headerValues = new Headers();
167
167
  response.headersSent = false;
168
168
  response.writableEnded = false;
169
+ response.destroyed = false;
169
170
  const setResponseHeader = (name, value) => {
170
171
  response.headerValues.delete(name);
171
172
  if (Array.isArray(value) && name.toLowerCase() === "set-cookie") {
@@ -224,6 +225,22 @@ async function fetchInMemory(server, url, init) {
224
225
  }
225
226
  return response;
226
227
  });
228
+ response.destroy = ((error) => {
229
+ if (!response.destroyed) {
230
+ response.destroyed = true;
231
+ try {
232
+ if (error === undefined)
233
+ streamController?.close();
234
+ else
235
+ streamController?.error(error);
236
+ }
237
+ catch {
238
+ // The consumer may have cancelled the stream already.
239
+ }
240
+ response.emit("close");
241
+ }
242
+ return response;
243
+ });
227
244
  response.flushHeaders = (() => {
228
245
  response.headersSent = true;
229
246
  });
@@ -56,4 +56,4 @@ export declare function createInMemoryTokenVerifier(options?: Partial<{
56
56
  now: () => number;
57
57
  }>): InMemoryTokenVerifier;
58
58
  export declare function createHttpTestPair(server: HttpServer): Promise<HttpTestPair>;
59
- export declare function createHttpTestPairWithTinyClient(server: HttpServer): Promise<TinyHttpTestPair>;
59
+ export declare function createHttpTestPairWithTinyClient(server: HttpServer, clientOptions?: Pick<import("tiny-mcp-client").McpClientOptions, "protocolVersion">): Promise<TinyHttpTestPair>;
@@ -135,7 +135,7 @@ export async function createHttpTestPair(server) {
135
135
  }
136
136
  };
137
137
  }
138
- export async function createHttpTestPairWithTinyClient(server) {
138
+ export async function createHttpTestPairWithTinyClient(server, clientOptions = {}) {
139
139
  let tinyMcpClient;
140
140
  try {
141
141
  tinyMcpClient = await import("tiny-mcp-client");
@@ -147,7 +147,8 @@ export async function createHttpTestPairWithTinyClient(server) {
147
147
  const handle = await server.listenHttp({ port: 0 });
148
148
  const requests = [];
149
149
  const client = new tinyMcpClient.McpClient({
150
- clientInfo: { name: "tiny-http-test-client", version: "1.0.0" }
150
+ clientInfo: { name: "tiny-http-test-client", version: "1.0.0" },
151
+ ...clientOptions
151
152
  });
152
153
  const transport = new tinyMcpClient.HttpTransport({
153
154
  url: handle.url,
@@ -1,3 +1,5 @@
1
+ import { isIP } from "node:net";
2
+ import { fetchMcpResponse } from "../http-fetch.js";
1
3
  import { URL } from "node:url";
2
4
  import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
3
5
  import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
@@ -62,6 +64,15 @@ export function createDefaultOAuthClientProvider(options) {
62
64
  async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false) {
63
65
  const canonicalResource = canonicalizeResourceIndicator(resource);
64
66
  let session = await loadSession(canonicalResource);
67
+ if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
68
+ throw new Error("OAuth discovery authorization-server issuer mismatch");
69
+ }
70
+ if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
71
+ || getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
72
+ || (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
73
+ await clearSession(canonicalResource);
74
+ session = null;
75
+ }
65
76
  const sessionDiscovery = resolveDiscovery(discovery, session);
66
77
  if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
67
78
  return session;
@@ -281,14 +292,16 @@ export function createDefaultOAuthClientProvider(options) {
281
292
  }
282
293
  }
283
294
  const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
284
- const response = await fetch(registrationEndpoint, {
295
+ const signal = AbortSignal.timeout(30_000);
296
+ const response = await fetchMcpResponse(fetch, registrationEndpoint, {
285
297
  method: "POST",
286
298
  headers: {
287
299
  "Content-Type": "application/json"
288
300
  },
289
- body: JSON.stringify(registrationBody)
301
+ body: JSON.stringify(registrationBody),
302
+ signal
290
303
  });
291
- const payload = await readOAuthJsonObjectResponse(response);
304
+ const payload = await readOAuthJsonObjectResponse(response, signal);
292
305
  const clientId = getOwnString(payload, "client_id");
293
306
  if (clientId === undefined || clientId.trim().length === 0) {
294
307
  throw new Error("OAuth client registration response missing client_id");
@@ -545,10 +558,14 @@ function isLoopbackHostname(hostname) {
545
558
  const normalizedHostname = normalizeHostname(hostname);
546
559
  return (normalizedHostname === "localhost" ||
547
560
  normalizedHostname === "::1" ||
548
- normalizedHostname.startsWith("127."));
561
+ normalizedHostname === "[::1]" ||
562
+ (isIP(normalizedHostname) === 4 && normalizedHostname.startsWith("127.")));
549
563
  }
550
564
  function assertSecureUrl(value, label) {
551
565
  const url = new URL(value);
566
+ if (url.username !== "" || url.password !== "" || url.hash !== "") {
567
+ throw new Error(`${label} must not include credentials or fragment`);
568
+ }
552
569
  if (url.protocol === "https:") {
553
570
  return;
554
571
  }
@@ -36,5 +36,5 @@ export declare function refreshAccessToken(input: {
36
36
  fetch: OAuthMetadataFetch;
37
37
  now: () => number;
38
38
  }): Promise<StoredOAuthTokens>;
39
- export declare function readOAuthJsonObjectResponse(response: Response): Promise<Record<string, unknown>>;
39
+ export declare function readOAuthJsonObjectResponse(response: Response, signal?: AbortSignal): Promise<Record<string, unknown>>;
40
40
  export {};
@@ -1,4 +1,6 @@
1
1
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
2
+ import { readBoundedResponseText } from "../http-response.js";
3
+ import { fetchMcpResponse } from "../http-fetch.js";
2
4
  const MAX_JS_DATE_MS = 8_640_000_000_000_000;
3
5
  export class OAuthError extends Error {
4
6
  error;
@@ -68,14 +70,16 @@ async function requestTokens(input) {
68
70
  if (input.clientSecret !== undefined) {
69
71
  body.set("client_secret", input.clientSecret);
70
72
  }
71
- const response = await input.fetch(input.tokenEndpoint, {
73
+ const signal = AbortSignal.timeout(30_000);
74
+ const response = await fetchMcpResponse(input.fetch, input.tokenEndpoint, {
72
75
  method: "POST",
73
76
  headers: {
74
77
  "Content-Type": "application/x-www-form-urlencoded"
75
78
  },
76
- body: body.toString()
79
+ body: body.toString(),
80
+ signal
77
81
  });
78
- const payload = await readOAuthJsonObjectResponse(response);
82
+ const payload = await readOAuthJsonObjectResponse(response, signal);
79
83
  const accessToken = getOwnEntry(payload, "access_token");
80
84
  if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
81
85
  throw new Error("OAuth token response missing access_token");
@@ -115,13 +119,17 @@ async function requestTokens(input) {
115
119
  scope: normalizedScope === undefined ? undefined : normalizedScope
116
120
  };
117
121
  }
118
- export async function readOAuthJsonObjectResponse(response) {
122
+ export async function readOAuthJsonObjectResponse(response, signal) {
119
123
  const fallbackError = createFallbackOAuthError(response.status);
120
124
  let payload;
121
125
  try {
122
- payload = await response.json();
126
+ payload = JSON.parse(await readBoundedResponseText(response, 1024 * 1024, undefined, signal));
123
127
  }
124
- catch {
128
+ catch (error) {
129
+ if (signal?.aborted)
130
+ throw signal.reason;
131
+ if (error instanceof Error && error.message.startsWith("HTTP response exceeds "))
132
+ throw error;
125
133
  if (!response.ok) {
126
134
  throw fallbackError;
127
135
  }
@@ -0,0 +1 @@
1
+ export declare function fetchMcpResponse(fetchImplementation: (input: string | URL, init?: RequestInit) => Promise<Response>, input: string | URL, init?: RequestInit): Promise<Response>;
@@ -0,0 +1,8 @@
1
+ export async function fetchMcpResponse(fetchImplementation, input, init = {}) {
2
+ const response = await fetchImplementation(input, { ...init, redirect: "error" });
3
+ if (response.redirected || response.type === "opaqueredirect") {
4
+ void response.body?.cancel().catch(() => undefined);
5
+ throw new Error("MCP HTTP redirects are not allowed");
6
+ }
7
+ return response;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function readBoundedResponseText(response: Response, maxBytes: number, readers?: Set<ReadableStreamDefaultReader<Uint8Array>>, signal?: AbortSignal): Promise<string>;