toolcraft 0.0.199 → 0.0.201

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/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.13",
101
+ "version": "0.1.15",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.199",
116
+ "version": "0.0.201",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.199",
126
+ "version": "0.0.201",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.13",
101
+ "version": "0.1.15",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.199",
116
+ "version": "0.0.201",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.199",
126
+ "version": "0.0.201",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.13",
21
+ "version": "0.1.15",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -754,7 +754,7 @@ export class StreamableHttpTransport {
754
754
  const history = this.sseEventHistory.get(sessionId) ?? [];
755
755
  for (const event of history) {
756
756
  if (event.id > lastEventId && !res.writableEnded) {
757
- res.write(formatSseEvent({ id: String(event.id), data: event.data }));
757
+ this.writeToLiveGetStream(res, formatSseEvent({ id: String(event.id), data: event.data }));
758
758
  }
759
759
  }
760
760
  }
@@ -64,12 +64,19 @@ async function readStreamBody(req, maxBytes) {
64
64
  return Buffer.concat(chunks).toString("utf8");
65
65
  }
66
66
  async function readRawBody(req, preParsed, options) {
67
- if (preParsed !== undefined) {
68
- return preParsed;
69
- }
70
67
  const reqWithBody = req;
71
- if (reqWithBody.body !== undefined) {
72
- return reqWithBody.body;
68
+ const body = preParsed !== undefined ? preParsed : reqWithBody.body;
69
+ if (body !== undefined) {
70
+ if (options.maxBytes !== undefined) {
71
+ const serialized = JSON.stringify(body);
72
+ if (serialized === undefined) {
73
+ throw new Error("Invalid Request");
74
+ }
75
+ if (Buffer.byteLength(serialized, "utf8") > options.maxBytes) {
76
+ throw new Error("Payload too large");
77
+ }
78
+ }
79
+ return body;
73
80
  }
74
81
  const raw = await readStreamBody(req, options.maxBytes);
75
82
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.199",
11
+ "version": "0.0.201",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -5,6 +5,7 @@ import { parseMessage, formatSuccessResponse, formatErrorResponse } from "./json
5
5
  import { parseUriTemplate } from "./uri-template.js";
6
6
  import { toContentBlocks } from "./content/convert.js";
7
7
  import { ToolCallAdmission } from "./tool-call-admission.js";
8
+ import { StdioOutput } from "./stdio-output.js";
8
9
  const PROTOCOL_VERSION = "2025-11-25";
9
10
  const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-03-26", "2025-06-18", PROTOCOL_VERSION]);
10
11
  function compileToolSchema(schema) {
@@ -23,9 +24,13 @@ export function createServer(options) {
23
24
  }
24
25
  const maxConcurrentToolCalls = options.maxConcurrentToolCalls ?? 4;
25
26
  const maxQueuedToolCalls = options.maxQueuedToolCalls ?? 64;
27
+ const maxStdioOutputBytes = options.maxStdioOutputBytes ?? 1024 * 1024;
28
+ const maxPendingStdioMessages = options.maxPendingStdioMessages ?? 128;
26
29
  for (const [name, value, minimum] of [
27
30
  ["maxConcurrentToolCalls", maxConcurrentToolCalls, 1],
28
- ["maxQueuedToolCalls", maxQueuedToolCalls, 0]
31
+ ["maxQueuedToolCalls", maxQueuedToolCalls, 0],
32
+ ["maxStdioOutputBytes", maxStdioOutputBytes, 1],
33
+ ["maxPendingStdioMessages", maxPendingStdioMessages, 1]
29
34
  ]) {
30
35
  if (!Number.isSafeInteger(value) || value < minimum) {
31
36
  throw new Error(`${name} must be a safe integer greater than or equal to ${minimum}.`);
@@ -361,7 +366,7 @@ export function createServer(options) {
361
366
  const processLine = async (line, write, messageHandler) => {
362
367
  const parsed = parseMessage(line);
363
368
  if (!parsed.success) {
364
- write(formatErrorResponse(parsed.id, parsed.error) + "\n");
369
+ await write(formatErrorResponse(parsed.id, parsed.error) + "\n");
365
370
  return;
366
371
  }
367
372
  const { request, isNotification } = parsed;
@@ -370,7 +375,7 @@ export function createServer(options) {
370
375
  }
371
376
  if (!isNotification && request.method === "notifications/initialized") {
372
377
  const requestWithId = request;
373
- write(formatErrorResponse(requestWithId.id, {
378
+ await write(formatErrorResponse(requestWithId.id, {
374
379
  code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
375
380
  message: "Invalid Request"
376
381
  }) + "\n");
@@ -383,7 +388,7 @@ export function createServer(options) {
383
388
  catch {
384
389
  if (!isNotification) {
385
390
  const requestWithId = request;
386
- write(formatErrorResponse(requestWithId.id, {
391
+ await write(formatErrorResponse(requestWithId.id, {
387
392
  code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
388
393
  message: "Internal error"
389
394
  }) + "\n");
@@ -396,10 +401,10 @@ export function createServer(options) {
396
401
  }
397
402
  const requestWithId = request;
398
403
  if (error) {
399
- write(formatErrorResponse(requestWithId.id, error) + "\n");
404
+ await write(formatErrorResponse(requestWithId.id, error) + "\n");
400
405
  }
401
406
  else if (result !== undefined) {
402
- write(formatSuccessResponse(requestWithId.id, result) + "\n");
407
+ await write(formatSuccessResponse(requestWithId.id, result) + "\n");
403
408
  }
404
409
  };
405
410
  const broadcastNotification = async (method, params, canSend = () => true) => {
@@ -541,27 +546,58 @@ export function createServer(options) {
541
546
  });
542
547
  },
543
548
  async connect(transport) {
544
- return new Promise((resolve) => {
545
- const listener = (notification) => {
546
- transport.writable.write(`${JSON.stringify(notification)}\n`);
547
- };
549
+ return new Promise((resolve, reject) => {
550
+ let inputClosed = false;
551
+ let settled = false;
552
+ const pendingMessages = new Set();
553
+ const output = new StdioOutput(transport.writable, maxStdioOutputBytes, fail, finish);
554
+ const listener = (notification) => output.write(`${JSON.stringify(notification)}\n`);
548
555
  const session = server.createMessageSession(listener);
549
556
  const rl = readline.createInterface({
550
557
  input: transport.readable,
551
558
  crlfDelay: Infinity
552
559
  });
553
- const pendingMessages = new Set();
560
+ function fail(error) {
561
+ if (settled)
562
+ return;
563
+ settled = true;
564
+ session.close();
565
+ rl.close();
566
+ transport.readable.pause();
567
+ transport.readable.off("error", fail);
568
+ output.abort(error);
569
+ pendingMessages.clear();
570
+ reject(error);
571
+ }
572
+ function finish() {
573
+ if (settled || !inputClosed || pendingMessages.size > 0 || output.pending > 0)
574
+ return;
575
+ settled = true;
576
+ session.close();
577
+ transport.readable.off("error", fail);
578
+ output.close();
579
+ resolve();
580
+ }
581
+ transport.readable.on("error", fail);
582
+ rl.on("error", fail);
554
583
  rl.on("line", (line) => {
555
- const message = processLine(line, (data) => transport.writable.write(data), session.handleMessage);
556
- pendingMessages.add(message);
557
- void message.finally(() => {
584
+ if (settled)
585
+ return;
586
+ if (pendingMessages.size >= maxPendingStdioMessages) {
587
+ fail(new Error("Stdio pending message limit exceeded"));
588
+ return;
589
+ }
590
+ const message = processLine(line, data => output.write(data), session.handleMessage);
591
+ if (!settled)
592
+ pendingMessages.add(message);
593
+ void message.then(() => {
558
594
  pendingMessages.delete(message);
559
- });
595
+ finish();
596
+ }, fail);
560
597
  });
561
- rl.on("close", async () => {
562
- await Promise.all([...pendingMessages]);
563
- session.close();
564
- resolve();
598
+ rl.on("close", () => {
599
+ inputClosed = true;
600
+ finish();
565
601
  });
566
602
  });
567
603
  },
@@ -0,0 +1,24 @@
1
+ /** One connection's admitted frames, including its submitted, unsettled write. */
2
+ export declare class StdioOutput {
3
+ private readonly writable;
4
+ private readonly maxBytes;
5
+ private readonly onFailure;
6
+ private readonly onIdle;
7
+ private readonly queue;
8
+ private active;
9
+ private bytes;
10
+ private stopped;
11
+ private failure;
12
+ private awaitingError;
13
+ constructor(writable: NodeJS.WritableStream, maxBytes: number, onFailure: (error: unknown) => void, onIdle: () => void);
14
+ get pending(): number;
15
+ write(data: string): Promise<void>;
16
+ abort(error: unknown): void;
17
+ close(): void;
18
+ private pump;
19
+ private advance;
20
+ private readonly onDrain;
21
+ private readonly onError;
22
+ private readonly onClose;
23
+ private detach;
24
+ }
@@ -0,0 +1,131 @@
1
+ /** One connection's admitted frames, including its submitted, unsettled write. */
2
+ export class StdioOutput {
3
+ writable;
4
+ maxBytes;
5
+ onFailure;
6
+ onIdle;
7
+ queue = [];
8
+ active;
9
+ bytes = 0;
10
+ stopped = false;
11
+ failure;
12
+ awaitingError = false;
13
+ constructor(writable, maxBytes, onFailure, onIdle) {
14
+ this.writable = writable;
15
+ this.maxBytes = maxBytes;
16
+ this.onFailure = onFailure;
17
+ this.onIdle = onIdle;
18
+ writable.on("drain", this.onDrain);
19
+ writable.on("error", this.onError);
20
+ writable.on("close", this.onClose);
21
+ }
22
+ get pending() { return this.queue.length + (this.active === undefined ? 0 : 1); }
23
+ write(data) {
24
+ if (this.stopped)
25
+ return Promise.reject(this.failure);
26
+ const bytes = Buffer.byteLength(data);
27
+ if (bytes > this.maxBytes - this.bytes) {
28
+ const error = new Error("Stdio output byte limit exceeded");
29
+ this.abort(error);
30
+ return Promise.reject(error);
31
+ }
32
+ this.bytes += bytes;
33
+ const promise = new Promise((resolve, reject) => {
34
+ this.queue.push({ data, bytes, resolve, reject });
35
+ });
36
+ this.pump();
37
+ return promise;
38
+ }
39
+ abort(error) {
40
+ if (!this.stopped) {
41
+ this.stopped = true;
42
+ this.failure = error;
43
+ this.active?.frame.reject(error);
44
+ if (this.active?.completed)
45
+ this.active = undefined;
46
+ for (const frame of this.queue.splice(0))
47
+ frame.reject(error);
48
+ this.bytes = 0;
49
+ this.onFailure(error);
50
+ }
51
+ // A submitted write can still fail after connection settlement. Keep its
52
+ // observation until callback/error/close; never destroy a caller's stream.
53
+ if (this.active === undefined && !this.awaitingError)
54
+ this.detach();
55
+ }
56
+ close() {
57
+ this.stopped = true;
58
+ this.failure = new Error("Stdio output is closed");
59
+ this.detach();
60
+ }
61
+ pump() {
62
+ if (this.stopped || this.active !== undefined)
63
+ return;
64
+ const frame = this.queue.shift();
65
+ if (frame === undefined) {
66
+ this.onIdle();
67
+ return;
68
+ }
69
+ const active = { frame, completed: false, returned: false, drained: false, needsDrain: false };
70
+ this.active = active;
71
+ try {
72
+ const accepted = this.writable.write(frame.data, "utf8", (error) => {
73
+ if (error != null) {
74
+ this.awaitingError = true;
75
+ this.active = undefined;
76
+ frame.reject(error);
77
+ this.abort(error);
78
+ return;
79
+ }
80
+ active.completed = true;
81
+ if (this.stopped) {
82
+ this.active = undefined;
83
+ this.detach();
84
+ }
85
+ else {
86
+ this.advance();
87
+ }
88
+ });
89
+ active.returned = true;
90
+ active.needsDrain = !accepted;
91
+ this.advance();
92
+ }
93
+ catch (error) {
94
+ this.active = undefined;
95
+ frame.reject(error);
96
+ this.abort(error);
97
+ }
98
+ }
99
+ advance() {
100
+ const active = this.active;
101
+ if (this.stopped || active === undefined || !active.returned || !active.completed ||
102
+ (active.needsDrain && !active.drained))
103
+ return;
104
+ this.active = undefined;
105
+ this.bytes -= active.frame.bytes;
106
+ active.frame.resolve();
107
+ this.pump();
108
+ }
109
+ onDrain = () => {
110
+ if (this.active !== undefined)
111
+ this.active.drained = true;
112
+ this.advance();
113
+ };
114
+ onError = (error) => {
115
+ this.awaitingError = false;
116
+ this.abort(error);
117
+ this.active = undefined;
118
+ this.detach();
119
+ };
120
+ onClose = () => {
121
+ this.awaitingError = false;
122
+ this.abort(new Error("Stdio output closed before connection completion"));
123
+ this.active = undefined;
124
+ this.detach();
125
+ };
126
+ detach() {
127
+ this.writable.off("drain", this.onDrain);
128
+ this.writable.off("error", this.onError);
129
+ this.writable.off("close", this.onClose);
130
+ }
131
+ }
@@ -213,6 +213,10 @@ export interface ServerOptions {
213
213
  maxConcurrentToolCalls?: number;
214
214
  /** Maximum tool calls waiting for capacity; defaults to 64. Zero disables waiting. */
215
215
  maxQueuedToolCalls?: number;
216
+ /** Per-stdio-connection UTF-8 bytes queued or submitted but unsettled; defaults to 1 MiB. */
217
+ maxStdioOutputBytes?: number;
218
+ /** Per-stdio-connection messages awaiting handler/output settlement; defaults to 128. */
219
+ maxPendingStdioMessages?: number;
216
220
  validateToolArguments?: boolean;
217
221
  supportNotifications?: boolean;
218
222
  supportResourceSubscriptions?: boolean;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.199",
3
+ "version": "0.0.201",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.199",
3
+ "version": "0.0.201",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.199",
161
+ "toolcraft-schema": "0.0.201",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",