toolcraft 0.0.24 → 0.0.26
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 +11 -9
- package/dist/error-report.js +14 -11
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.js +70 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +33 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +2 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +36 -9
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +170 -36
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +66 -9
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +5 -4
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +5 -1
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +29 -10
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +1 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +36 -2
- package/node_modules/auth-store/dist/keychain-store.js +20 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -3
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +3 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +39 -14
- package/node_modules/tiny-mcp-client/src/internal.ts +45 -17
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/node_modules/tiny-mcp-client/src/transports.test.ts +68 -0
- package/package.json +2 -2
|
@@ -13,6 +13,7 @@ let temporaryFileSequence = 0;
|
|
|
13
13
|
export class EncryptedFileStore {
|
|
14
14
|
fs;
|
|
15
15
|
filePath;
|
|
16
|
+
symbolicLinkCheckStartPath;
|
|
16
17
|
salt;
|
|
17
18
|
getMachineIdentity;
|
|
18
19
|
getRandomBytes;
|
|
@@ -20,7 +21,16 @@ export class EncryptedFileStore {
|
|
|
20
21
|
constructor(input) {
|
|
21
22
|
this.fs = input.fs ?? fs;
|
|
22
23
|
this.salt = input.salt;
|
|
23
|
-
|
|
24
|
+
if (input.filePath === undefined) {
|
|
25
|
+
const homeDirectory = (input.getHomeDirectory ?? homedir)();
|
|
26
|
+
const defaultDirectory = input.defaultDirectory ?? ".auth-store";
|
|
27
|
+
this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
|
|
28
|
+
this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.filePath = input.filePath;
|
|
32
|
+
this.symbolicLinkCheckStartPath = null;
|
|
33
|
+
}
|
|
24
34
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
25
35
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
26
36
|
}
|
|
@@ -106,7 +116,7 @@ export class EncryptedFileStore {
|
|
|
106
116
|
}
|
|
107
117
|
async assertRegularCredentialPath() {
|
|
108
118
|
const resolvedPath = path.resolve(this.filePath);
|
|
109
|
-
const protectedPaths =
|
|
119
|
+
const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
|
|
110
120
|
for (const currentPath of protectedPaths) {
|
|
111
121
|
try {
|
|
112
122
|
const stats = await this.fs.lstat(currentPath);
|
|
@@ -135,6 +145,30 @@ export class EncryptedFileStore {
|
|
|
135
145
|
return this.keyPromise;
|
|
136
146
|
}
|
|
137
147
|
}
|
|
148
|
+
function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
|
|
149
|
+
const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
|
|
150
|
+
return path.resolve(homeDirectory, firstSegment ?? ".");
|
|
151
|
+
}
|
|
152
|
+
function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
153
|
+
if (symbolicLinkCheckStartPath === null) {
|
|
154
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
155
|
+
}
|
|
156
|
+
const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
|
|
157
|
+
if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
|
|
158
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
159
|
+
}
|
|
160
|
+
const protectedPaths = [resolvedStartPath];
|
|
161
|
+
let currentPath = resolvedStartPath;
|
|
162
|
+
for (const segment of path.relative(resolvedStartPath, resolvedPath).split(path.sep).filter(Boolean)) {
|
|
163
|
+
currentPath = path.join(currentPath, segment);
|
|
164
|
+
protectedPaths.push(currentPath);
|
|
165
|
+
}
|
|
166
|
+
return protectedPaths;
|
|
167
|
+
}
|
|
168
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
169
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
170
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
171
|
+
}
|
|
138
172
|
async function removeIfPresent(fileSystem, filePath) {
|
|
139
173
|
try {
|
|
140
174
|
await fileSystem.unlink(filePath);
|
|
@@ -64,6 +64,19 @@ function runSecurityCommand(command, args, options) {
|
|
|
64
64
|
});
|
|
65
65
|
let stdout = "";
|
|
66
66
|
let stderr = "";
|
|
67
|
+
let stdinErrorMessage;
|
|
68
|
+
const appendStderr = (message) => {
|
|
69
|
+
stderr = stderr.length === 0
|
|
70
|
+
? message
|
|
71
|
+
: `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
|
|
72
|
+
};
|
|
73
|
+
const appendStdinError = () => {
|
|
74
|
+
if (stdinErrorMessage === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
appendStderr(stdinErrorMessage);
|
|
78
|
+
stdinErrorMessage = undefined;
|
|
79
|
+
};
|
|
67
80
|
child.stdout?.setEncoding("utf8");
|
|
68
81
|
child.stdout?.on("data", (chunk) => {
|
|
69
82
|
stdout += chunk.toString();
|
|
@@ -73,17 +86,23 @@ function runSecurityCommand(command, args, options) {
|
|
|
73
86
|
stderr += chunk.toString();
|
|
74
87
|
});
|
|
75
88
|
if (options?.stdin !== undefined) {
|
|
89
|
+
child.stdin?.once("error", (error) => {
|
|
90
|
+
stdinErrorMessage = error instanceof Error ? error.message : String(error);
|
|
91
|
+
});
|
|
76
92
|
child.stdin?.end(options.stdin);
|
|
77
93
|
}
|
|
78
94
|
child.on("error", (error) => {
|
|
79
95
|
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
96
|
+
appendStdinError();
|
|
97
|
+
appendStderr(message);
|
|
80
98
|
resolve({
|
|
81
99
|
stdout,
|
|
82
|
-
stderr
|
|
100
|
+
stderr,
|
|
83
101
|
exitCode: 127
|
|
84
102
|
});
|
|
85
103
|
});
|
|
86
104
|
child.on("close", (code) => {
|
|
105
|
+
appendStdinError();
|
|
87
106
|
resolve({
|
|
88
107
|
stdout,
|
|
89
108
|
stderr,
|
|
@@ -61,12 +61,15 @@ export function createAuthStoreClientStore(options) {
|
|
|
61
61
|
}
|
|
62
62
|
function createNamedSecretStore(key, options, defaults) {
|
|
63
63
|
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
|
64
|
-
const
|
|
64
|
+
const configuredFilePath = options.fileStore?.filePath;
|
|
65
|
+
const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
|
|
65
66
|
const fileStore = {
|
|
66
67
|
...options.fileStore,
|
|
68
|
+
filePath: parsedFilePath === null
|
|
69
|
+
? undefined
|
|
70
|
+
: path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
|
|
67
71
|
salt: options.fileStore?.salt ?? defaults.salt,
|
|
68
|
-
defaultDirectory:
|
|
69
|
-
options.fileStore?.defaultDirectory ||
|
|
72
|
+
defaultDirectory: options.fileStore?.defaultDirectory ||
|
|
70
73
|
defaults.directory,
|
|
71
74
|
defaultFileName: parsedFilePath === null
|
|
72
75
|
? `${hash}.enc`
|
|
@@ -400,6 +400,7 @@ export declare class StdioTransport implements McpTransport {
|
|
|
400
400
|
private static readonly STDERR_MAX_LENGTH;
|
|
401
401
|
constructor({ command, args, cwd, env, spawn: spawnProcess, }: StdioTransportOptions);
|
|
402
402
|
getStderrOutput(): string;
|
|
403
|
+
private appendStderrOutput;
|
|
403
404
|
dispose(reason?: Error): void;
|
|
404
405
|
}
|
|
405
406
|
export declare class HttpTransport implements McpTransport {
|
|
@@ -515,6 +516,7 @@ export declare class SseParser {
|
|
|
515
516
|
export interface JsonRpcRequestOptions {
|
|
516
517
|
timeoutMs?: number;
|
|
517
518
|
onRequestId?: (requestId: RequestId) => void;
|
|
519
|
+
onTimeout?: (requestId: RequestId) => void;
|
|
518
520
|
}
|
|
519
521
|
interface JsonRpcRequestContext {
|
|
520
522
|
id: RequestId;
|
|
@@ -541,6 +543,7 @@ export declare class JsonRpcMessageLayer {
|
|
|
541
543
|
onRequest(method: string, handler: JsonRpcRequestHandler): void;
|
|
542
544
|
onNotification(method: string, handler: JsonRpcNotificationHandler): void;
|
|
543
545
|
sendRequest(method: string, params?: unknown, options?: JsonRpcRequestOptions): Promise<unknown>;
|
|
546
|
+
cancelRequest(requestId: RequestId, reason: unknown): boolean;
|
|
544
547
|
dispose(reason?: Error): void;
|
|
545
548
|
private consumeInput;
|
|
546
549
|
private resolveInputStreamClosedReason;
|
|
@@ -258,10 +258,19 @@ export class McpClient {
|
|
|
258
258
|
}
|
|
259
259
|
try {
|
|
260
260
|
let requestId;
|
|
261
|
+
let cancellationSent = false;
|
|
262
|
+
const sendCancellationNotification = () => {
|
|
263
|
+
if (requestId === undefined || cancellationSent) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
cancellationSent = true;
|
|
267
|
+
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
268
|
+
};
|
|
261
269
|
const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
|
|
262
270
|
onRequestId: (nextRequestId) => {
|
|
263
271
|
requestId = nextRequestId;
|
|
264
272
|
},
|
|
273
|
+
onTimeout: sendCancellationNotification,
|
|
265
274
|
}).then((result) => {
|
|
266
275
|
if (!isCallToolResult(result)) {
|
|
267
276
|
throw new McpError(ERROR_INVALID_REQUEST, "Invalid tool result");
|
|
@@ -275,8 +284,9 @@ export class McpClient {
|
|
|
275
284
|
let abortListener;
|
|
276
285
|
const abortPromise = new Promise((_, reject) => {
|
|
277
286
|
const rejectWithAbortReason = () => {
|
|
287
|
+
sendCancellationNotification();
|
|
278
288
|
if (requestId !== undefined) {
|
|
279
|
-
messageLayer.
|
|
289
|
+
messageLayer.cancelRequest(requestId, signal.reason);
|
|
280
290
|
}
|
|
281
291
|
reject(signal.reason);
|
|
282
292
|
};
|
|
@@ -1504,11 +1514,15 @@ export class StdioTransport {
|
|
|
1504
1514
|
const child = this.child;
|
|
1505
1515
|
this.readable = child.stdout;
|
|
1506
1516
|
this.writable = child.stdin;
|
|
1517
|
+
const stderrDecoder = new TextDecoder();
|
|
1507
1518
|
child.stderr.on("data", (chunk) => {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1519
|
+
const decoded = chunk instanceof Uint8Array
|
|
1520
|
+
? stderrDecoder.decode(chunk, { stream: true })
|
|
1521
|
+
: `${stderrDecoder.decode()}${String(chunk)}`;
|
|
1522
|
+
this.appendStderrOutput(decoded);
|
|
1523
|
+
});
|
|
1524
|
+
child.stderr.once("end", () => {
|
|
1525
|
+
this.appendStderrOutput(stderrDecoder.decode());
|
|
1512
1526
|
});
|
|
1513
1527
|
this.closed = new Promise((resolve) => {
|
|
1514
1528
|
let settled = false;
|
|
@@ -1548,6 +1562,15 @@ export class StdioTransport {
|
|
|
1548
1562
|
getStderrOutput() {
|
|
1549
1563
|
return this.stderrOutput;
|
|
1550
1564
|
}
|
|
1565
|
+
appendStderrOutput(chunk) {
|
|
1566
|
+
if (chunk.length === 0) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
this.stderrOutput += chunk;
|
|
1570
|
+
if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
|
|
1571
|
+
this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1551
1574
|
dispose(reason = new Error("Stdio transport disposed")) {
|
|
1552
1575
|
void reason;
|
|
1553
1576
|
if (this.disposed) {
|
|
@@ -1959,15 +1982,6 @@ export class McpError extends Error {
|
|
|
1959
1982
|
export function serializeJsonRpcMessage(message) {
|
|
1960
1983
|
return `${JSON.stringify(message)}\n`;
|
|
1961
1984
|
}
|
|
1962
|
-
function chunkToString(chunk) {
|
|
1963
|
-
if (typeof chunk === "string") {
|
|
1964
|
-
return chunk;
|
|
1965
|
-
}
|
|
1966
|
-
if (chunk instanceof Uint8Array) {
|
|
1967
|
-
return Buffer.from(chunk).toString("utf8");
|
|
1968
|
-
}
|
|
1969
|
-
return String(chunk);
|
|
1970
|
-
}
|
|
1971
1985
|
function normalizeLine(line) {
|
|
1972
1986
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1973
1987
|
}
|
|
@@ -2147,6 +2161,7 @@ export class JsonRpcMessageLayer {
|
|
|
2147
2161
|
return new Promise((resolve, reject) => {
|
|
2148
2162
|
const timeout = setTimeout(() => {
|
|
2149
2163
|
this.pendingRequests.delete(id);
|
|
2164
|
+
options.onTimeout?.(id);
|
|
2150
2165
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
2151
2166
|
}, timeoutMs);
|
|
2152
2167
|
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
@@ -2160,6 +2175,16 @@ export class JsonRpcMessageLayer {
|
|
|
2160
2175
|
}
|
|
2161
2176
|
});
|
|
2162
2177
|
}
|
|
2178
|
+
cancelRequest(requestId, reason) {
|
|
2179
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2180
|
+
if (pending === undefined) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
this.pendingRequests.delete(requestId);
|
|
2184
|
+
clearTimeout(pending.timeout);
|
|
2185
|
+
pending.reject(reason);
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2163
2188
|
dispose(reason = new Error("JSON-RPC message layer disposed")) {
|
|
2164
2189
|
if (this.disposedError !== undefined) {
|
|
2165
2190
|
return;
|
|
@@ -449,10 +449,19 @@ export class McpClient {
|
|
|
449
449
|
|
|
450
450
|
try {
|
|
451
451
|
let requestId: RequestId | undefined;
|
|
452
|
+
let cancellationSent = false;
|
|
453
|
+
const sendCancellationNotification = () => {
|
|
454
|
+
if (requestId === undefined || cancellationSent) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
cancellationSent = true;
|
|
458
|
+
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
459
|
+
};
|
|
452
460
|
const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
|
|
453
461
|
onRequestId: (nextRequestId) => {
|
|
454
462
|
requestId = nextRequestId;
|
|
455
463
|
},
|
|
464
|
+
onTimeout: sendCancellationNotification,
|
|
456
465
|
}).then((result) => {
|
|
457
466
|
if (!isCallToolResult(result)) {
|
|
458
467
|
throw new McpError(ERROR_INVALID_REQUEST, "Invalid tool result");
|
|
@@ -468,8 +477,9 @@ export class McpClient {
|
|
|
468
477
|
let abortListener: (() => void) | undefined;
|
|
469
478
|
const abortPromise = new Promise<CallToolResult>((_, reject) => {
|
|
470
479
|
const rejectWithAbortReason = () => {
|
|
480
|
+
sendCancellationNotification();
|
|
471
481
|
if (requestId !== undefined) {
|
|
472
|
-
messageLayer.
|
|
482
|
+
messageLayer.cancelRequest(requestId, signal.reason);
|
|
473
483
|
}
|
|
474
484
|
reject(signal.reason);
|
|
475
485
|
};
|
|
@@ -2343,11 +2353,16 @@ export class StdioTransport implements McpTransport {
|
|
|
2343
2353
|
|
|
2344
2354
|
this.readable = child.stdout;
|
|
2345
2355
|
this.writable = child.stdin;
|
|
2356
|
+
const stderrDecoder = new TextDecoder();
|
|
2346
2357
|
child.stderr.on("data", (chunk: unknown) => {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2358
|
+
const decoded =
|
|
2359
|
+
chunk instanceof Uint8Array
|
|
2360
|
+
? stderrDecoder.decode(chunk, { stream: true })
|
|
2361
|
+
: `${stderrDecoder.decode()}${String(chunk)}`;
|
|
2362
|
+
this.appendStderrOutput(decoded);
|
|
2363
|
+
});
|
|
2364
|
+
child.stderr.once("end", () => {
|
|
2365
|
+
this.appendStderrOutput(stderrDecoder.decode());
|
|
2351
2366
|
});
|
|
2352
2367
|
this.closed = new Promise((resolve) => {
|
|
2353
2368
|
let settled = false;
|
|
@@ -2398,6 +2413,17 @@ export class StdioTransport implements McpTransport {
|
|
|
2398
2413
|
return this.stderrOutput;
|
|
2399
2414
|
}
|
|
2400
2415
|
|
|
2416
|
+
private appendStderrOutput(chunk: string): void {
|
|
2417
|
+
if (chunk.length === 0) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
this.stderrOutput += chunk;
|
|
2422
|
+
if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
|
|
2423
|
+
this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2401
2427
|
dispose(reason = new Error("Stdio transport disposed")): void {
|
|
2402
2428
|
void reason;
|
|
2403
2429
|
|
|
@@ -2958,18 +2984,6 @@ export function serializeJsonRpcMessage(message: JsonRpcMessage): string {
|
|
|
2958
2984
|
return `${JSON.stringify(message)}\n`;
|
|
2959
2985
|
}
|
|
2960
2986
|
|
|
2961
|
-
function chunkToString(chunk: unknown): string {
|
|
2962
|
-
if (typeof chunk === "string") {
|
|
2963
|
-
return chunk;
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
if (chunk instanceof Uint8Array) {
|
|
2967
|
-
return Buffer.from(chunk).toString("utf8");
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
return String(chunk);
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
2987
|
function normalizeLine(line: string): string {
|
|
2974
2988
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
2975
2989
|
}
|
|
@@ -3134,6 +3148,7 @@ interface ActiveIncomingRequest {
|
|
|
3134
3148
|
export interface JsonRpcRequestOptions {
|
|
3135
3149
|
timeoutMs?: number;
|
|
3136
3150
|
onRequestId?: (requestId: RequestId) => void;
|
|
3151
|
+
onTimeout?: (requestId: RequestId) => void;
|
|
3137
3152
|
}
|
|
3138
3153
|
|
|
3139
3154
|
interface JsonRpcRequestContext {
|
|
@@ -3242,6 +3257,7 @@ export class JsonRpcMessageLayer {
|
|
|
3242
3257
|
return new Promise((resolve, reject) => {
|
|
3243
3258
|
const timeout = setTimeout(() => {
|
|
3244
3259
|
this.pendingRequests.delete(id);
|
|
3260
|
+
options.onTimeout?.(id);
|
|
3245
3261
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
3246
3262
|
}, timeoutMs);
|
|
3247
3263
|
|
|
@@ -3257,6 +3273,18 @@ export class JsonRpcMessageLayer {
|
|
|
3257
3273
|
});
|
|
3258
3274
|
}
|
|
3259
3275
|
|
|
3276
|
+
cancelRequest(requestId: RequestId, reason: unknown): boolean {
|
|
3277
|
+
const pending = this.pendingRequests.get(requestId);
|
|
3278
|
+
if (pending === undefined) {
|
|
3279
|
+
return false;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
this.pendingRequests.delete(requestId);
|
|
3283
|
+
clearTimeout(pending.timeout);
|
|
3284
|
+
pending.reject(reason);
|
|
3285
|
+
return true;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3260
3288
|
dispose(reason = new Error("JSON-RPC message layer disposed")): void {
|
|
3261
3289
|
if (this.disposedError !== undefined) {
|
|
3262
3290
|
return;
|
|
@@ -293,6 +293,38 @@ describe("McpClient SDK integration callTool", () => {
|
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
it("cancels an in-flight slow tool call when the request timeout elapses", async () => {
|
|
297
|
+
const server = await createMockSlowToolServer({ delayMs: 1_000, pollIntervalMs: 5 });
|
|
298
|
+
const { client, cleanup } = await createSdkTestPair(server, () =>
|
|
299
|
+
new McpClient({
|
|
300
|
+
clientInfo: {
|
|
301
|
+
name: "test-client",
|
|
302
|
+
version: "1.0.0",
|
|
303
|
+
},
|
|
304
|
+
requestTimeoutMs: 100,
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const callPromise = client.callTool({
|
|
310
|
+
name: "slow",
|
|
311
|
+
arguments: {
|
|
312
|
+
delayMs: 500,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await waitFor(() => server.wasStarted(), "Timed out waiting for slow tool to start");
|
|
317
|
+
|
|
318
|
+
await expect(callPromise).rejects.toThrow(
|
|
319
|
+
'JSON-RPC request "tools/call" timed out after 100ms'
|
|
320
|
+
);
|
|
321
|
+
await waitFor(() => server.wasCancelled(), "Timed out waiting for slow tool cancellation");
|
|
322
|
+
expect(server.getCancelledRequestIds()).toEqual(server.getStartedRequestIds());
|
|
323
|
+
} finally {
|
|
324
|
+
await cleanup();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
296
328
|
it("rejects with JSON-RPC error code and message for unknown tool names", async () => {
|
|
297
329
|
const server = await createMockErrorServer();
|
|
298
330
|
const { client, cleanup } = await createSdkTestPair(server, () =>
|
|
@@ -1471,6 +1471,40 @@ describe("JsonRpcMessageLayer sendRequest", () => {
|
|
|
1471
1471
|
vi.useRealTimers();
|
|
1472
1472
|
}
|
|
1473
1473
|
});
|
|
1474
|
+
|
|
1475
|
+
it("clears pending request state and timeout when a request is cancelled", async () => {
|
|
1476
|
+
vi.useFakeTimers();
|
|
1477
|
+
try {
|
|
1478
|
+
const input = new PassThrough();
|
|
1479
|
+
const output = new PassThrough();
|
|
1480
|
+
trackForCleanup(input, output);
|
|
1481
|
+
const layer = new JsonRpcMessageLayer(input, output, 25);
|
|
1482
|
+
const onTimeout = vi.fn();
|
|
1483
|
+
const pendingCount = () =>
|
|
1484
|
+
(
|
|
1485
|
+
layer as unknown as {
|
|
1486
|
+
pendingRequests: Map<unknown, unknown>;
|
|
1487
|
+
}
|
|
1488
|
+
).pendingRequests.size;
|
|
1489
|
+
|
|
1490
|
+
const responsePromise = layer.sendRequest("slow/method", undefined, {
|
|
1491
|
+
onTimeout,
|
|
1492
|
+
});
|
|
1493
|
+
expect(pendingCount()).toBe(1);
|
|
1494
|
+
|
|
1495
|
+
expect(layer.cancelRequest(1, "user cancelled")).toBe(true);
|
|
1496
|
+
|
|
1497
|
+
await expect(responsePromise).rejects.toBe("user cancelled");
|
|
1498
|
+
expect(pendingCount()).toBe(0);
|
|
1499
|
+
|
|
1500
|
+
await vi.advanceTimersByTimeAsync(25);
|
|
1501
|
+
|
|
1502
|
+
expect(onTimeout).not.toHaveBeenCalled();
|
|
1503
|
+
expect(layer.cancelRequest(1, "already gone")).toBe(false);
|
|
1504
|
+
} finally {
|
|
1505
|
+
vi.useRealTimers();
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1474
1508
|
});
|
|
1475
1509
|
|
|
1476
1510
|
describe("JsonRpcMessageLayer UTF-8 input", () => {
|
|
@@ -2643,6 +2677,22 @@ describe("StdioTransport stderr capture", () => {
|
|
|
2643
2677
|
expect(transport.getStderrOutput()).toBe("first second");
|
|
2644
2678
|
});
|
|
2645
2679
|
|
|
2680
|
+
it("preserves UTF-8 characters split across stderr chunks", () => {
|
|
2681
|
+
const child = createMockChildProcess();
|
|
2682
|
+
const spawn = vi.fn<StdioSpawn>(() => child);
|
|
2683
|
+
|
|
2684
|
+
const transport = new StdioTransport({
|
|
2685
|
+
command: "node",
|
|
2686
|
+
spawn,
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
const encoded = Buffer.from("é", "utf8");
|
|
2690
|
+
child.stderr.write(encoded.subarray(0, 1));
|
|
2691
|
+
child.stderr.write(encoded.subarray(1));
|
|
2692
|
+
|
|
2693
|
+
expect(transport.getStderrOutput()).toBe("é");
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2646
2696
|
it("caps stderr at 64KB keeping the tail", () => {
|
|
2647
2697
|
const child = createMockChildProcess();
|
|
2648
2698
|
const spawn = vi.fn<StdioSpawn>(() => child);
|
|
@@ -7123,6 +7173,21 @@ describe("McpClient callTool", () => {
|
|
|
7123
7173
|
throw new Error("Expected initialized notification line to be written");
|
|
7124
7174
|
}
|
|
7125
7175
|
|
|
7176
|
+
const activeMessageLayer = (
|
|
7177
|
+
client as unknown as {
|
|
7178
|
+
messageLayer: JsonRpcMessageLayer | null;
|
|
7179
|
+
}
|
|
7180
|
+
).messageLayer;
|
|
7181
|
+
if (activeMessageLayer === null) {
|
|
7182
|
+
throw new Error("Expected message layer to exist after connect");
|
|
7183
|
+
}
|
|
7184
|
+
const pendingCount = () =>
|
|
7185
|
+
(
|
|
7186
|
+
activeMessageLayer as unknown as {
|
|
7187
|
+
pendingRequests: Map<unknown, unknown>;
|
|
7188
|
+
}
|
|
7189
|
+
).pendingRequests.size;
|
|
7190
|
+
|
|
7126
7191
|
const abortController = new AbortController();
|
|
7127
7192
|
const callToolPromise = client.callTool(
|
|
7128
7193
|
{
|
|
@@ -7142,6 +7207,8 @@ describe("McpClient callTool", () => {
|
|
|
7142
7207
|
const callToolRequest = JSON.parse(callToolLineResult.value) as {
|
|
7143
7208
|
id: number;
|
|
7144
7209
|
};
|
|
7210
|
+
expect(pendingCount()).toBe(1);
|
|
7211
|
+
|
|
7145
7212
|
abortController.abort("user cancelled");
|
|
7146
7213
|
|
|
7147
7214
|
const cancelledLineResult = await iterator.next();
|
|
@@ -7158,6 +7225,7 @@ describe("McpClient callTool", () => {
|
|
|
7158
7225
|
});
|
|
7159
7226
|
|
|
7160
7227
|
await callToolRejection;
|
|
7228
|
+
expect(pendingCount()).toBe(0);
|
|
7161
7229
|
await client.close();
|
|
7162
7230
|
});
|
|
7163
7231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@clack/core": "^1.0.0",
|
|
47
47
|
"@clack/prompts": "^1.0.0",
|
|
48
|
-
"toolcraft-schema": "0.0.
|
|
48
|
+
"toolcraft-schema": "0.0.26",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"jose": "^6.1.2",
|
|
51
51
|
"jsonc-parser": "^3.3.1",
|