tiny-http-mcp-server 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -29,6 +29,8 @@ const HELP_TEXT = [
29
29
  " --allowed-origin <url> Allowed CORS Origin value (repeatable)",
30
30
  " --max-request-bytes <bytes>",
31
31
  " Maximum JSON request body size",
32
+ " --max-response-bytes <bytes>",
33
+ " Maximum JSON body or SSE frame size (default: 16777216)",
32
34
  " --max-batch-size <count>",
33
35
  " Maximum JSON-RPC batch member count",
34
36
  " --max-sessions <count> Maximum active sessions (default: 128)",
@@ -38,13 +40,15 @@ const HELP_TEXT = [
38
40
  " --max-streams-per-session <count>",
39
41
  " Maximum concurrent GET SSE streams per session",
40
42
  " --max-stream-buffer-bytes <bytes>",
41
- " Maximum buffered bytes per GET SSE stream (default: 1048576)",
43
+ " Maximum buffered bytes per SSE stream (default: 1048576)",
42
44
  " --max-sse-event-history <count>",
43
45
  " Number of SSE events retained for Last-Event-ID replay",
44
46
  " --sse-keep-alive-ms <ms>",
45
- " GET SSE keepalive interval (default: 30000; 0 disables)",
47
+ " SSE keepalive interval (default: 30000; 0 disables)",
46
48
  " --max-concurrent-tool-calls <count>",
47
49
  " Maximum concurrent tool calls across sessions (default: 4)",
50
+ " --max-active-requests <count>",
51
+ " Maximum active requests across connections (default: 128)",
48
52
  " --max-queued-tool-calls <count>",
49
53
  " Maximum waiting tool calls (default: 64; 0 disables waiting)",
50
54
  " --trusted-proxy Trust X-Forwarded-Proto and X-Forwarded-Host",
@@ -194,6 +198,7 @@ function parseCliOptions(args) {
194
198
  "allowed-host": { type: "string", multiple: true },
195
199
  "allowed-origin": { type: "string", multiple: true },
196
200
  "max-request-bytes": { type: "string" },
201
+ "max-response-bytes": { type: "string" },
197
202
  "max-batch-size": { type: "string" },
198
203
  "max-sessions": { type: "string" },
199
204
  "max-sessions-per-subject": { type: "string" },
@@ -203,6 +208,7 @@ function parseCliOptions(args) {
203
208
  "max-sse-event-history": { type: "string" },
204
209
  "sse-keep-alive-ms": { type: "string" },
205
210
  "max-concurrent-tool-calls": { type: "string" },
211
+ "max-active-requests": { type: "string" },
206
212
  "max-queued-tool-calls": { type: "string" },
207
213
  "trusted-proxy": { type: "boolean" },
208
214
  "request-timeout-ms": { type: "string" },
@@ -222,6 +228,7 @@ function parseCliOptions(args) {
222
228
  });
223
229
  const maxRequestBytes = parseOptionalInteger(values["max-request-bytes"], "--max-request-bytes", 1);
224
230
  const maxBatchSize = parseOptionalInteger(values["max-batch-size"], "--max-batch-size", 1);
231
+ const maxResponseBytes = parseOptionalInteger(values["max-response-bytes"], "--max-response-bytes", 1);
225
232
  const maxSessions = parseOptionalInteger(values["max-sessions"], "--max-sessions", 1);
226
233
  const maxSessionsPerSubject = parseOptionalInteger(values["max-sessions-per-subject"], "--max-sessions-per-subject", 1);
227
234
  const sessionTtlMs = parseOptionalInteger(values["session-ttl-ms"], "--session-ttl-ms", 1);
@@ -231,6 +238,7 @@ function parseCliOptions(args) {
231
238
  const sseKeepAliveMs = parseOptionalInteger(values["sse-keep-alive-ms"], "--sse-keep-alive-ms", 0);
232
239
  const maxConcurrentToolCalls = parseOptionalInteger(values["max-concurrent-tool-calls"], "--max-concurrent-tool-calls", 1);
233
240
  const maxQueuedToolCalls = parseOptionalInteger(values["max-queued-tool-calls"], "--max-queued-tool-calls", 0);
241
+ const maxActiveRequests = parseOptionalInteger(values["max-active-requests"], "--max-active-requests", 1);
234
242
  const requestTimeoutMs = parseOptionalInteger(values["request-timeout-ms"], "--request-timeout-ms", 0);
235
243
  const headersTimeoutMs = parseOptionalInteger(values["headers-timeout-ms"], "--headers-timeout-ms", 0);
236
244
  const keepAliveTimeoutMs = parseOptionalInteger(values["keep-alive-timeout-ms"], "--keep-alive-timeout-ms", 0);
@@ -252,6 +260,7 @@ function parseCliOptions(args) {
252
260
  }
253
261
  : {}),
254
262
  ...(maxRequestBytes === undefined ? {} : { maxRequestBytes }),
263
+ ...(maxResponseBytes === undefined ? {} : { maxResponseBytes }),
255
264
  ...(maxBatchSize === undefined ? {} : { maxBatchSize }),
256
265
  ...(maxSessions === undefined ? {} : { maxSessions }),
257
266
  ...(maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject }),
@@ -261,6 +270,7 @@ function parseCliOptions(args) {
261
270
  ...(maxSseEventHistory === undefined ? {} : { maxSseEventHistory }),
262
271
  ...(sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs }),
263
272
  ...(maxConcurrentToolCalls === undefined ? {} : { maxConcurrentToolCalls }),
273
+ ...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
264
274
  ...(maxQueuedToolCalls === undefined ? {} : { maxQueuedToolCalls }),
265
275
  trustedProxy: values["trusted-proxy"] ?? false,
266
276
  ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
@@ -400,6 +410,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
400
410
  ...(options.maxRequestBytes === undefined
401
411
  ? {}
402
412
  : { maxRequestBytes: options.maxRequestBytes }),
413
+ ...(options.maxResponseBytes === undefined ? {} : { maxResponseBytes: options.maxResponseBytes }),
403
414
  ...(options.maxBatchSize === undefined ? {} : { maxBatchSize: options.maxBatchSize }),
404
415
  ...(options.maxSessions === undefined ? {} : { maxSessions: options.maxSessions }),
405
416
  ...(options.maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject: options.maxSessionsPerSubject }),
@@ -418,6 +429,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
418
429
  ? {}
419
430
  : { maxConcurrentToolCalls: options.maxConcurrentToolCalls }),
420
431
  ...(options.maxQueuedToolCalls === undefined ? {} : { maxQueuedToolCalls: options.maxQueuedToolCalls }),
432
+ ...(options.maxActiveRequests === undefined ? {} : { maxActiveRequests: options.maxActiveRequests }),
421
433
  ...(options.trustedProxy ? { trustedProxy: true } : {}),
422
434
  ...(oauth === undefined ? {} : { oauth })
423
435
  });
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.17",
21
+ "version": "0.1.19",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -1,5 +1,5 @@
1
1
  import { type IncomingMessage, type ServerResponse } from "node:http";
2
- import { type Server, type ServerOptions, type ToolDefinition, type CallToolResult, type ToolReturn, type TypedSchema } from "tiny-stdio-mcp-server";
2
+ import { type Server, type ServerOptions, type ToolDefinition, type CallToolResult, type InputRequiredResult, type HandlerRequestContext, type ToolReturn, type TypedSchema, type TypedOutputSchema } from "tiny-stdio-mcp-server";
3
3
  import { type AuthenticatedIncomingMessage, type TokenVerifier, type VerifiedAccessToken, type RequestAuthInfo } from "./auth.js";
4
4
  import { type HttpObservabilityEvent, type StreamableHttpTransportOptions } from "./http-transport.js";
5
5
  export interface ProtectedResourceMetadataOptions {
@@ -33,18 +33,20 @@ export interface HttpServerHandle {
33
33
  closeAllConnections(): void;
34
34
  }
35
35
  export interface HttpServer extends Omit<Server, "tool" | "registerTool"> {
36
- tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: HttpToolHandler<TIn, TOut>, outputSchema?: TypedSchema<TOut>): HttpServer;
36
+ tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: HttpToolHandler<TIn, TOut>, outputSchema?: TypedOutputSchema<TOut>): HttpServer;
37
37
  registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: HttpToolHandler<TIn, TOut>): HttpServer;
38
38
  listenHttp(options?: HttpListenOptions): Promise<HttpServerHandle>;
39
39
  handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
40
- getRequestContext(): HttpToolContext | undefined;
40
+ getRequestContext(): HttpRequestContext | undefined;
41
41
  }
42
- export interface HttpToolContext {
42
+ export interface HttpRequestContext {
43
43
  request: AuthenticatedIncomingMessage;
44
44
  sessionId?: string;
45
45
  auth?: RequestAuthInfo;
46
46
  }
47
- export type HttpToolHandler<T = Record<string, unknown>, TOut = ToolReturn> = (args: T, context: HttpToolContext) => Promise<TOut | CallToolResult> | TOut | CallToolResult;
47
+ export interface HttpToolContext extends HttpRequestContext, HandlerRequestContext {
48
+ }
49
+ export type HttpToolHandler<T = Record<string, unknown>, TOut = ToolReturn> = (args: T, context: HttpToolContext) => Promise<TOut | CallToolResult | InputRequiredResult> | TOut | CallToolResult | InputRequiredResult;
48
50
  export declare function createProtectedResourceMetadataDocument(options: ProtectedResourceMetadataOptions): Record<string, unknown>;
49
51
  export declare function createHttpServer(options: HttpTransportOptions): HttpServer;
50
52
  export type { RequestAuthInfo, TokenVerifier, VerifiedAccessToken };
@@ -87,12 +87,7 @@ export function createProtectedResourceMetadataDocument(options) {
87
87
  }
88
88
  export function createHttpServer(options) {
89
89
  const requestContextStorage = new AsyncLocalStorage();
90
- const supportsSessions = !hasOwnProperty(options, "sessionIdGenerator") || options.sessionIdGenerator !== undefined;
91
- const server = createServer({
92
- ...options,
93
- supportNotifications: supportsSessions,
94
- supportResourceSubscriptions: supportsSessions
95
- });
90
+ const server = createServer(options);
96
91
  const transport = new StreamableHttpTransport(server, options, async (req, callback) => requestContextStorage.run({
97
92
  request: req,
98
93
  sessionId: Array.isArray(req.headers["mcp-session-id"])
@@ -144,11 +139,11 @@ export function createHttpServer(options) {
144
139
  return true;
145
140
  };
146
141
  httpServer.tool = (name, description, inputSchema, handler, outputSchema) => {
147
- registerTool(name, description, inputSchema, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext), outputSchema);
142
+ registerTool(name, description, inputSchema, (args, context) => handler(args, { ...(requestContextStorage.getStore() ?? defaultContext), ...context }), outputSchema);
148
143
  return httpServer;
149
144
  };
150
145
  httpServer.registerTool = (definition, handler) => {
151
- registerRichTool(definition, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext));
146
+ registerRichTool(definition, (args, context) => handler(args, { ...(requestContextStorage.getStore() ?? defaultContext), ...context }));
152
147
  return httpServer;
153
148
  };
154
149
  httpServer.listenHttp = async (listenOptions = {}) => {
@@ -265,6 +260,3 @@ export function createHttpServer(options) {
265
260
  httpServer.getRequestContext = () => requestContextStorage.getStore();
266
261
  return httpServer;
267
262
  }
268
- function hasOwnProperty(value, key) {
269
- return Object.prototype.hasOwnProperty.call(value, key);
270
- }
@@ -64,6 +64,7 @@ export interface StreamableHttpTransportOptions {
64
64
  allowedOrigins?: readonly string[];
65
65
  allowedHosts?: readonly string[];
66
66
  maxRequestBytes?: number;
67
+ maxResponseBytes?: number;
67
68
  maxBatchSize?: number;
68
69
  maxSessions?: number;
69
70
  /** Maximum sessions per authenticated subject or client ID; defaults to 16. */
@@ -88,6 +89,7 @@ export declare class StreamableHttpTransport {
88
89
  private readonly allowedOrigins;
89
90
  private readonly allowedHosts;
90
91
  private readonly maxRequestBytes;
92
+ private readonly maxResponseBytes;
91
93
  private readonly maxBatchSize;
92
94
  private readonly maxSessions;
93
95
  private readonly maxSessionsPerSubject;
@@ -108,6 +110,8 @@ export declare class StreamableHttpTransport {
108
110
  private readonly responseRequestIds;
109
111
  private readonly responseOrigins;
110
112
  private readonly responseRejectionReasons;
113
+ private readonly modernRequests;
114
+ private readonly modernStreams;
111
115
  private nextNotificationEventId;
112
116
  private nextRequestId;
113
117
  private activeToolCalls;
@@ -118,6 +122,7 @@ export declare class StreamableHttpTransport {
118
122
  handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
119
123
  close(): Promise<void>;
120
124
  private handlePost;
125
+ private handleModernPost;
121
126
  private handleGet;
122
127
  private handleDelete;
123
128
  private handleOptions;
@@ -143,7 +148,7 @@ export declare class StreamableHttpTransport {
143
148
  private stopSseKeepAliveIfIdle;
144
149
  private stopSseKeepAlive;
145
150
  private sendNotificationToSession;
146
- private writeToLiveGetStream;
151
+ private writeToLiveStream;
147
152
  private recordSseEvent;
148
153
  private replaySseEvents;
149
154
  private isJsonRequest;
@@ -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
  }
package/dist/index.d.ts CHANGED
@@ -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
  }