@ai-ecoverse/mcpcat 0.4.0 → 0.5.0
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/host/mcpcat.mjs +242 -16
- package/dist/kernel/mcpcat.mjs +241 -17
- package/package.json +4 -3
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -1708,6 +1708,185 @@ var StreamableHttpTransport = class extends TransportEvents {
|
|
|
1708
1708
|
}
|
|
1709
1709
|
};
|
|
1710
1710
|
|
|
1711
|
+
// src/client/platform-stdio-transport.ts
|
|
1712
|
+
var DEFAULT_MAX_MESSAGE_BYTES2 = 4 * 1024 * 1024;
|
|
1713
|
+
var DEFAULT_MAX_STDERR_BYTES2 = 64 * 1024;
|
|
1714
|
+
var DEFAULT_CLOSE_TIMEOUT_MS2 = 2e3;
|
|
1715
|
+
var STDIN_CLOSE_GRACE_MS2 = 500;
|
|
1716
|
+
var PlatformStdioTransport = class {
|
|
1717
|
+
options;
|
|
1718
|
+
process;
|
|
1719
|
+
started = false;
|
|
1720
|
+
closed = false;
|
|
1721
|
+
closeEmitted = false;
|
|
1722
|
+
stderrText = "";
|
|
1723
|
+
decoder = new TextDecoder();
|
|
1724
|
+
encoder = new TextEncoder();
|
|
1725
|
+
messageListeners = [];
|
|
1726
|
+
errorListeners = [];
|
|
1727
|
+
closeListeners = [];
|
|
1728
|
+
constructor(options) {
|
|
1729
|
+
this.options = options;
|
|
1730
|
+
}
|
|
1731
|
+
get stderr() {
|
|
1732
|
+
return this.stderrText;
|
|
1733
|
+
}
|
|
1734
|
+
onMessage(listener) {
|
|
1735
|
+
this.messageListeners.push(listener);
|
|
1736
|
+
return () => {
|
|
1737
|
+
this.messageListeners = this.messageListeners.filter((l) => l !== listener);
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
onError(listener) {
|
|
1741
|
+
this.errorListeners.push(listener);
|
|
1742
|
+
return () => {
|
|
1743
|
+
this.errorListeners = this.errorListeners.filter((l) => l !== listener);
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
onClose(listener) {
|
|
1747
|
+
this.closeListeners.push(listener);
|
|
1748
|
+
return () => {
|
|
1749
|
+
this.closeListeners = this.closeListeners.filter((l) => l !== listener);
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
emitMessage(message) {
|
|
1753
|
+
for (const l of [...this.messageListeners]) l(message);
|
|
1754
|
+
}
|
|
1755
|
+
emitError(error) {
|
|
1756
|
+
for (const l of [...this.errorListeners]) l(error);
|
|
1757
|
+
}
|
|
1758
|
+
emitClose() {
|
|
1759
|
+
if (this.closeEmitted) return;
|
|
1760
|
+
this.closeEmitted = true;
|
|
1761
|
+
for (const l of [...this.closeListeners]) l();
|
|
1762
|
+
}
|
|
1763
|
+
async start() {
|
|
1764
|
+
if (this.started) throw new Error("MCP stdio transport already started");
|
|
1765
|
+
if (this.closed) throw new Error("MCP connection closed");
|
|
1766
|
+
this.started = true;
|
|
1767
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env ?? {} } : {
|
|
1768
|
+
...this.options.platform.env,
|
|
1769
|
+
...this.options.env
|
|
1770
|
+
};
|
|
1771
|
+
this.process = await this.options.platform.spawn({
|
|
1772
|
+
argv: [this.options.command, ...this.options.args ?? []],
|
|
1773
|
+
env,
|
|
1774
|
+
cwd: this.options.cwd
|
|
1775
|
+
});
|
|
1776
|
+
void this.readStdout();
|
|
1777
|
+
void this.readStderr();
|
|
1778
|
+
void this.waitForExit();
|
|
1779
|
+
}
|
|
1780
|
+
async send(message) {
|
|
1781
|
+
if (!this.started || this.closed || !this.process) throw new Error("MCP connection closed");
|
|
1782
|
+
const payload = `${JSON.stringify(message)}
|
|
1783
|
+
`;
|
|
1784
|
+
const writer = this.process.stdin.getWriter();
|
|
1785
|
+
try {
|
|
1786
|
+
await writer.write(this.encoder.encode(payload));
|
|
1787
|
+
} finally {
|
|
1788
|
+
writer.releaseLock();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
async close() {
|
|
1792
|
+
if (this.closed) return;
|
|
1793
|
+
this.closed = true;
|
|
1794
|
+
const proc = this.process;
|
|
1795
|
+
if (!proc) {
|
|
1796
|
+
this.emitClose();
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS2;
|
|
1800
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS2, closeTimeoutMs);
|
|
1801
|
+
const done = proc.wait().then(() => true);
|
|
1802
|
+
const delay = (ms) => new Promise((r) => setTimeout(() => r(false), ms));
|
|
1803
|
+
try {
|
|
1804
|
+
await proc.stdin.close();
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
const exited = await Promise.race([done, delay(grace)]);
|
|
1808
|
+
if (!exited) {
|
|
1809
|
+
proc.kill("SIGTERM");
|
|
1810
|
+
const exitedAfterTerm = await Promise.race([done, delay(closeTimeoutMs)]);
|
|
1811
|
+
if (!exitedAfterTerm) {
|
|
1812
|
+
proc.kill("SIGKILL");
|
|
1813
|
+
await done;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
proc.kill("SIGTERM");
|
|
1817
|
+
this.emitClose();
|
|
1818
|
+
}
|
|
1819
|
+
async readStdout() {
|
|
1820
|
+
const proc = this.process;
|
|
1821
|
+
if (!proc) return;
|
|
1822
|
+
const reader = proc.stdout.getReader();
|
|
1823
|
+
const maxBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES2;
|
|
1824
|
+
let buffer = "";
|
|
1825
|
+
try {
|
|
1826
|
+
for (; ; ) {
|
|
1827
|
+
const { done, value } = await reader.read();
|
|
1828
|
+
if (done) break;
|
|
1829
|
+
buffer += this.decoder.decode(value, { stream: true });
|
|
1830
|
+
let nlIdx = buffer.indexOf("\n");
|
|
1831
|
+
while (nlIdx >= 0) {
|
|
1832
|
+
const line = buffer.slice(0, nlIdx).replace(/\r$/, "");
|
|
1833
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
1834
|
+
if (line.trim()) {
|
|
1835
|
+
try {
|
|
1836
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(line)));
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
this.emitError(err);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
nlIdx = buffer.indexOf("\n");
|
|
1842
|
+
}
|
|
1843
|
+
if (this.encoder.encode(buffer).length > maxBytes) {
|
|
1844
|
+
buffer = "";
|
|
1845
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxBytes} bytes`));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
buffer += this.decoder.decode();
|
|
1849
|
+
if (buffer.trim()) {
|
|
1850
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
1851
|
+
}
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
if (!this.closed) this.emitError(err);
|
|
1854
|
+
} finally {
|
|
1855
|
+
reader.releaseLock();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async readStderr() {
|
|
1859
|
+
const proc = this.process;
|
|
1860
|
+
if (!proc) return;
|
|
1861
|
+
const reader = proc.stderr.getReader();
|
|
1862
|
+
const maxBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES2;
|
|
1863
|
+
try {
|
|
1864
|
+
for (; ; ) {
|
|
1865
|
+
const { done, value } = await reader.read();
|
|
1866
|
+
if (done) break;
|
|
1867
|
+
const chunk = this.decoder.decode(value, { stream: true });
|
|
1868
|
+
this.stderrText += chunk;
|
|
1869
|
+
if (this.encoder.encode(this.stderrText).length > maxBytes) {
|
|
1870
|
+
this.stderrText = this.stderrText.slice(-maxBytes);
|
|
1871
|
+
}
|
|
1872
|
+
this.options.onStderr?.(chunk);
|
|
1873
|
+
}
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async waitForExit() {
|
|
1878
|
+
const proc = this.process;
|
|
1879
|
+
if (!proc) return;
|
|
1880
|
+
try {
|
|
1881
|
+
await proc.wait();
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
if (!this.closed) {
|
|
1885
|
+
this.emitClose();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1711
1890
|
// src/client/pi-connection.ts
|
|
1712
1891
|
async function createPiConnection(options) {
|
|
1713
1892
|
const clientOptions = {
|
|
@@ -1719,17 +1898,29 @@ async function createPiConnection(options) {
|
|
|
1719
1898
|
if (options.url) {
|
|
1720
1899
|
transport = new StreamableHttpTransport({
|
|
1721
1900
|
url: options.url,
|
|
1722
|
-
headers: options.headers
|
|
1901
|
+
headers: options.headers,
|
|
1902
|
+
fetch: options.fetchImpl
|
|
1723
1903
|
});
|
|
1724
1904
|
} else if (options.command) {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1905
|
+
if (options.platform) {
|
|
1906
|
+
transport = new PlatformStdioTransport({
|
|
1907
|
+
command: options.command,
|
|
1908
|
+
args: options.args,
|
|
1909
|
+
env: options.env,
|
|
1910
|
+
cwd: options.cwd,
|
|
1911
|
+
inheritEnv: true,
|
|
1912
|
+
platform: options.platform
|
|
1913
|
+
});
|
|
1914
|
+
} else {
|
|
1915
|
+
transport = new StdioTransport({
|
|
1916
|
+
command: options.command,
|
|
1917
|
+
args: options.args,
|
|
1918
|
+
env: options.env,
|
|
1919
|
+
cwd: options.cwd,
|
|
1920
|
+
inheritEnv: true,
|
|
1921
|
+
stderr: "pipe"
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1733
1924
|
} else {
|
|
1734
1925
|
throw new Error("PiConnection requires either a url or a command");
|
|
1735
1926
|
}
|
|
@@ -1943,7 +2134,8 @@ async function connect(platform2, options) {
|
|
|
1943
2134
|
args: options.args,
|
|
1944
2135
|
env: options.env,
|
|
1945
2136
|
cwd: options.cwd,
|
|
1946
|
-
version: options.version
|
|
2137
|
+
version: options.version,
|
|
2138
|
+
platform: platform2
|
|
1947
2139
|
});
|
|
1948
2140
|
}
|
|
1949
2141
|
if (!options.url) {
|
|
@@ -1953,7 +2145,9 @@ async function connect(platform2, options) {
|
|
|
1953
2145
|
return createPiConnection({
|
|
1954
2146
|
url: options.url,
|
|
1955
2147
|
headers: options.headers,
|
|
1956
|
-
version: options.version
|
|
2148
|
+
version: options.version,
|
|
2149
|
+
fetchImpl: platform2.fetch,
|
|
2150
|
+
platform: platform2
|
|
1957
2151
|
});
|
|
1958
2152
|
}
|
|
1959
2153
|
try {
|
|
@@ -1972,7 +2166,9 @@ async function connect(platform2, options) {
|
|
|
1972
2166
|
return createPiConnection({
|
|
1973
2167
|
url: options.url,
|
|
1974
2168
|
headers: options.headers,
|
|
1975
|
-
version: options.version
|
|
2169
|
+
version: options.version,
|
|
2170
|
+
fetchImpl: platform2.fetch,
|
|
2171
|
+
platform: platform2
|
|
1976
2172
|
});
|
|
1977
2173
|
}
|
|
1978
2174
|
throw err;
|
|
@@ -4905,6 +5101,21 @@ function renderCallResult(result) {
|
|
|
4905
5101
|
}
|
|
4906
5102
|
return parts.join("\n");
|
|
4907
5103
|
}
|
|
5104
|
+
var NETWORK_HINT = "connection failed (use slicc-node, slicc-swift, or the browser extension for servers without CORS)";
|
|
5105
|
+
function transportHint(err) {
|
|
5106
|
+
if (err instanceof TypeError) {
|
|
5107
|
+
const cause = err.cause;
|
|
5108
|
+
if (cause?.code === "ECONNREFUSED" || cause?.code === "ENETUNREACH") {
|
|
5109
|
+
return `connection failed: ${err.message}`;
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
if (err instanceof Error && "status" in err && err.status === 502) {
|
|
5113
|
+
const msg = err.message;
|
|
5114
|
+
if (msg) return `${NETWORK_HINT}: ${msg}`;
|
|
5115
|
+
return NETWORK_HINT;
|
|
5116
|
+
}
|
|
5117
|
+
return void 0;
|
|
5118
|
+
}
|
|
4908
5119
|
async function readDataArg(platform2, dataArg) {
|
|
4909
5120
|
let raw;
|
|
4910
5121
|
if (dataArg.startsWith("@-")) {
|
|
@@ -5146,7 +5357,8 @@ async function run(platform2, version, args2) {
|
|
|
5146
5357
|
version
|
|
5147
5358
|
});
|
|
5148
5359
|
} catch (err) {
|
|
5149
|
-
|
|
5360
|
+
const msg = transportHint(err) ?? `connection failed: ${err.message}`;
|
|
5361
|
+
await platform2.write(2, `mcpcat: ${msg}
|
|
5150
5362
|
`);
|
|
5151
5363
|
return 3;
|
|
5152
5364
|
}
|
|
@@ -5176,11 +5388,13 @@ import { access, mkdir, readFile as readFile2, rename, rmdir, stat, writeFile }
|
|
|
5176
5388
|
import { createServer } from "node:net";
|
|
5177
5389
|
import { Readable, Writable } from "node:stream";
|
|
5178
5390
|
import * as tty from "node:tty";
|
|
5391
|
+
var USE_PROCESS_GROUPS2 = process.platform !== "win32";
|
|
5179
5392
|
function nodeSpawn(options) {
|
|
5180
5393
|
const child = cpSpawn(options.argv[0], options.argv.slice(1), {
|
|
5181
5394
|
env: options.env ?? process.env,
|
|
5182
5395
|
cwd: options.cwd,
|
|
5183
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5396
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5397
|
+
detached: USE_PROCESS_GROUPS2
|
|
5184
5398
|
});
|
|
5185
5399
|
const stdout = Readable.toWeb(child.stdout);
|
|
5186
5400
|
const stderr = Readable.toWeb(child.stderr);
|
|
@@ -5200,7 +5414,19 @@ function nodeSpawn(options) {
|
|
|
5200
5414
|
});
|
|
5201
5415
|
},
|
|
5202
5416
|
kill(signal) {
|
|
5203
|
-
|
|
5417
|
+
const sig = signal ?? "SIGTERM";
|
|
5418
|
+
const pid = child.pid;
|
|
5419
|
+
if (USE_PROCESS_GROUPS2 && pid !== void 0) {
|
|
5420
|
+
try {
|
|
5421
|
+
process.kill(-pid, sig);
|
|
5422
|
+
return;
|
|
5423
|
+
} catch {
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
try {
|
|
5427
|
+
child.kill(sig);
|
|
5428
|
+
} catch {
|
|
5429
|
+
}
|
|
5204
5430
|
}
|
|
5205
5431
|
});
|
|
5206
5432
|
}
|
|
@@ -5354,5 +5580,5 @@ function createHostPlatform() {
|
|
|
5354
5580
|
// src/host.ts
|
|
5355
5581
|
var platform = createHostPlatform();
|
|
5356
5582
|
var args = process.argv.slice(2);
|
|
5357
|
-
var code = await run(platform, "0.
|
|
5583
|
+
var code = await run(platform, "0.5.0", args);
|
|
5358
5584
|
process.exit(code);
|
package/dist/kernel/mcpcat.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
var __mcpcat_byteLength = (s) => new TextEncoder().encode(s).length;
|
|
2
|
+
|
|
1
3
|
// node_modules/@earendil-works/pi-mcp/dist/protocol/jsonrpc.js
|
|
2
4
|
var JSON_RPC_ERROR_CODES = {
|
|
3
5
|
parseError: -32700,
|
|
@@ -772,7 +774,7 @@ async function consumeSseStream(stream, options) {
|
|
|
772
774
|
if (value.startsWith(" "))
|
|
773
775
|
value = value.slice(1);
|
|
774
776
|
if (field === "data") {
|
|
775
|
-
dataBytes +=
|
|
777
|
+
dataBytes += __mcpcat_byteLength(value) + (dataLines.length > 0 ? 1 : 0);
|
|
776
778
|
if (dataBytes > maxEventBytes)
|
|
777
779
|
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
778
780
|
dataLines.push(value);
|
|
@@ -796,7 +798,7 @@ async function consumeSseStream(stream, options) {
|
|
|
796
798
|
buffered = buffered.slice(newline + 1);
|
|
797
799
|
newline = buffered.indexOf("\n");
|
|
798
800
|
}
|
|
799
|
-
if (
|
|
801
|
+
if (__mcpcat_byteLength(buffered) > maxEventBytes)
|
|
800
802
|
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
801
803
|
}
|
|
802
804
|
buffered += decoder2.decode();
|
|
@@ -1173,6 +1175,185 @@ var StreamableHttpTransport = class extends TransportEvents {
|
|
|
1173
1175
|
}
|
|
1174
1176
|
};
|
|
1175
1177
|
|
|
1178
|
+
// src/client/platform-stdio-transport.ts
|
|
1179
|
+
var DEFAULT_MAX_MESSAGE_BYTES2 = 4 * 1024 * 1024;
|
|
1180
|
+
var DEFAULT_MAX_STDERR_BYTES2 = 64 * 1024;
|
|
1181
|
+
var DEFAULT_CLOSE_TIMEOUT_MS2 = 2e3;
|
|
1182
|
+
var STDIN_CLOSE_GRACE_MS2 = 500;
|
|
1183
|
+
var PlatformStdioTransport = class {
|
|
1184
|
+
options;
|
|
1185
|
+
process;
|
|
1186
|
+
started = false;
|
|
1187
|
+
closed = false;
|
|
1188
|
+
closeEmitted = false;
|
|
1189
|
+
stderrText = "";
|
|
1190
|
+
decoder = new TextDecoder();
|
|
1191
|
+
encoder = new TextEncoder();
|
|
1192
|
+
messageListeners = [];
|
|
1193
|
+
errorListeners = [];
|
|
1194
|
+
closeListeners = [];
|
|
1195
|
+
constructor(options) {
|
|
1196
|
+
this.options = options;
|
|
1197
|
+
}
|
|
1198
|
+
get stderr() {
|
|
1199
|
+
return this.stderrText;
|
|
1200
|
+
}
|
|
1201
|
+
onMessage(listener) {
|
|
1202
|
+
this.messageListeners.push(listener);
|
|
1203
|
+
return () => {
|
|
1204
|
+
this.messageListeners = this.messageListeners.filter((l) => l !== listener);
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
onError(listener) {
|
|
1208
|
+
this.errorListeners.push(listener);
|
|
1209
|
+
return () => {
|
|
1210
|
+
this.errorListeners = this.errorListeners.filter((l) => l !== listener);
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
onClose(listener) {
|
|
1214
|
+
this.closeListeners.push(listener);
|
|
1215
|
+
return () => {
|
|
1216
|
+
this.closeListeners = this.closeListeners.filter((l) => l !== listener);
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
emitMessage(message) {
|
|
1220
|
+
for (const l of [...this.messageListeners]) l(message);
|
|
1221
|
+
}
|
|
1222
|
+
emitError(error) {
|
|
1223
|
+
for (const l of [...this.errorListeners]) l(error);
|
|
1224
|
+
}
|
|
1225
|
+
emitClose() {
|
|
1226
|
+
if (this.closeEmitted) return;
|
|
1227
|
+
this.closeEmitted = true;
|
|
1228
|
+
for (const l of [...this.closeListeners]) l();
|
|
1229
|
+
}
|
|
1230
|
+
async start() {
|
|
1231
|
+
if (this.started) throw new Error("MCP stdio transport already started");
|
|
1232
|
+
if (this.closed) throw new Error("MCP connection closed");
|
|
1233
|
+
this.started = true;
|
|
1234
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env ?? {} } : {
|
|
1235
|
+
...this.options.platform.env,
|
|
1236
|
+
...this.options.env
|
|
1237
|
+
};
|
|
1238
|
+
this.process = await this.options.platform.spawn({
|
|
1239
|
+
argv: [this.options.command, ...this.options.args ?? []],
|
|
1240
|
+
env,
|
|
1241
|
+
cwd: this.options.cwd
|
|
1242
|
+
});
|
|
1243
|
+
void this.readStdout();
|
|
1244
|
+
void this.readStderr();
|
|
1245
|
+
void this.waitForExit();
|
|
1246
|
+
}
|
|
1247
|
+
async send(message) {
|
|
1248
|
+
if (!this.started || this.closed || !this.process) throw new Error("MCP connection closed");
|
|
1249
|
+
const payload = `${JSON.stringify(message)}
|
|
1250
|
+
`;
|
|
1251
|
+
const writer = this.process.stdin.getWriter();
|
|
1252
|
+
try {
|
|
1253
|
+
await writer.write(this.encoder.encode(payload));
|
|
1254
|
+
} finally {
|
|
1255
|
+
writer.releaseLock();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async close() {
|
|
1259
|
+
if (this.closed) return;
|
|
1260
|
+
this.closed = true;
|
|
1261
|
+
const proc = this.process;
|
|
1262
|
+
if (!proc) {
|
|
1263
|
+
this.emitClose();
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS2;
|
|
1267
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS2, closeTimeoutMs);
|
|
1268
|
+
const done = proc.wait().then(() => true);
|
|
1269
|
+
const delay = (ms) => new Promise((r) => setTimeout(() => r(false), ms));
|
|
1270
|
+
try {
|
|
1271
|
+
await proc.stdin.close();
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
const exited = await Promise.race([done, delay(grace)]);
|
|
1275
|
+
if (!exited) {
|
|
1276
|
+
proc.kill("SIGTERM");
|
|
1277
|
+
const exitedAfterTerm = await Promise.race([done, delay(closeTimeoutMs)]);
|
|
1278
|
+
if (!exitedAfterTerm) {
|
|
1279
|
+
proc.kill("SIGKILL");
|
|
1280
|
+
await done;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
proc.kill("SIGTERM");
|
|
1284
|
+
this.emitClose();
|
|
1285
|
+
}
|
|
1286
|
+
async readStdout() {
|
|
1287
|
+
const proc = this.process;
|
|
1288
|
+
if (!proc) return;
|
|
1289
|
+
const reader = proc.stdout.getReader();
|
|
1290
|
+
const maxBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES2;
|
|
1291
|
+
let buffer = "";
|
|
1292
|
+
try {
|
|
1293
|
+
for (; ; ) {
|
|
1294
|
+
const { done, value } = await reader.read();
|
|
1295
|
+
if (done) break;
|
|
1296
|
+
buffer += this.decoder.decode(value, { stream: true });
|
|
1297
|
+
let nlIdx = buffer.indexOf("\n");
|
|
1298
|
+
while (nlIdx >= 0) {
|
|
1299
|
+
const line = buffer.slice(0, nlIdx).replace(/\r$/, "");
|
|
1300
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
1301
|
+
if (line.trim()) {
|
|
1302
|
+
try {
|
|
1303
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(line)));
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
this.emitError(err);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
nlIdx = buffer.indexOf("\n");
|
|
1309
|
+
}
|
|
1310
|
+
if (this.encoder.encode(buffer).length > maxBytes) {
|
|
1311
|
+
buffer = "";
|
|
1312
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxBytes} bytes`));
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
buffer += this.decoder.decode();
|
|
1316
|
+
if (buffer.trim()) {
|
|
1317
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
1318
|
+
}
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (!this.closed) this.emitError(err);
|
|
1321
|
+
} finally {
|
|
1322
|
+
reader.releaseLock();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async readStderr() {
|
|
1326
|
+
const proc = this.process;
|
|
1327
|
+
if (!proc) return;
|
|
1328
|
+
const reader = proc.stderr.getReader();
|
|
1329
|
+
const maxBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES2;
|
|
1330
|
+
try {
|
|
1331
|
+
for (; ; ) {
|
|
1332
|
+
const { done, value } = await reader.read();
|
|
1333
|
+
if (done) break;
|
|
1334
|
+
const chunk = this.decoder.decode(value, { stream: true });
|
|
1335
|
+
this.stderrText += chunk;
|
|
1336
|
+
if (this.encoder.encode(this.stderrText).length > maxBytes) {
|
|
1337
|
+
this.stderrText = this.stderrText.slice(-maxBytes);
|
|
1338
|
+
}
|
|
1339
|
+
this.options.onStderr?.(chunk);
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async waitForExit() {
|
|
1345
|
+
const proc = this.process;
|
|
1346
|
+
if (!proc) return;
|
|
1347
|
+
try {
|
|
1348
|
+
await proc.wait();
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
if (!this.closed) {
|
|
1352
|
+
this.emitClose();
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1176
1357
|
// src/client/pi-connection.ts
|
|
1177
1358
|
async function createPiConnection(options) {
|
|
1178
1359
|
const clientOptions = {
|
|
@@ -1184,17 +1365,29 @@ async function createPiConnection(options) {
|
|
|
1184
1365
|
if (options.url) {
|
|
1185
1366
|
transport = new StreamableHttpTransport({
|
|
1186
1367
|
url: options.url,
|
|
1187
|
-
headers: options.headers
|
|
1368
|
+
headers: options.headers,
|
|
1369
|
+
fetch: options.fetchImpl
|
|
1188
1370
|
});
|
|
1189
1371
|
} else if (options.command) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1372
|
+
if (options.platform) {
|
|
1373
|
+
transport = new PlatformStdioTransport({
|
|
1374
|
+
command: options.command,
|
|
1375
|
+
args: options.args,
|
|
1376
|
+
env: options.env,
|
|
1377
|
+
cwd: options.cwd,
|
|
1378
|
+
inheritEnv: true,
|
|
1379
|
+
platform: options.platform
|
|
1380
|
+
});
|
|
1381
|
+
} else {
|
|
1382
|
+
transport = new StdioTransport({
|
|
1383
|
+
command: options.command,
|
|
1384
|
+
args: options.args,
|
|
1385
|
+
env: options.env,
|
|
1386
|
+
cwd: options.cwd,
|
|
1387
|
+
inheritEnv: true,
|
|
1388
|
+
stderr: "pipe"
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1198
1391
|
} else {
|
|
1199
1392
|
throw new Error("PiConnection requires either a url or a command");
|
|
1200
1393
|
}
|
|
@@ -1408,7 +1601,8 @@ async function connect(platform, options) {
|
|
|
1408
1601
|
args: options.args,
|
|
1409
1602
|
env: options.env,
|
|
1410
1603
|
cwd: options.cwd,
|
|
1411
|
-
version: options.version
|
|
1604
|
+
version: options.version,
|
|
1605
|
+
platform
|
|
1412
1606
|
});
|
|
1413
1607
|
}
|
|
1414
1608
|
if (!options.url) {
|
|
@@ -1418,7 +1612,9 @@ async function connect(platform, options) {
|
|
|
1418
1612
|
return createPiConnection({
|
|
1419
1613
|
url: options.url,
|
|
1420
1614
|
headers: options.headers,
|
|
1421
|
-
version: options.version
|
|
1615
|
+
version: options.version,
|
|
1616
|
+
fetchImpl: platform.fetch,
|
|
1617
|
+
platform
|
|
1422
1618
|
});
|
|
1423
1619
|
}
|
|
1424
1620
|
try {
|
|
@@ -1437,7 +1633,9 @@ async function connect(platform, options) {
|
|
|
1437
1633
|
return createPiConnection({
|
|
1438
1634
|
url: options.url,
|
|
1439
1635
|
headers: options.headers,
|
|
1440
|
-
version: options.version
|
|
1636
|
+
version: options.version,
|
|
1637
|
+
fetchImpl: platform.fetch,
|
|
1638
|
+
platform
|
|
1441
1639
|
});
|
|
1442
1640
|
}
|
|
1443
1641
|
throw err;
|
|
@@ -4370,6 +4568,21 @@ function renderCallResult(result) {
|
|
|
4370
4568
|
}
|
|
4371
4569
|
return parts.join("\n");
|
|
4372
4570
|
}
|
|
4571
|
+
var NETWORK_HINT = "connection failed (use slicc-node, slicc-swift, or the browser extension for servers without CORS)";
|
|
4572
|
+
function transportHint(err) {
|
|
4573
|
+
if (err instanceof TypeError) {
|
|
4574
|
+
const cause = err.cause;
|
|
4575
|
+
if (cause?.code === "ECONNREFUSED" || cause?.code === "ENETUNREACH") {
|
|
4576
|
+
return `connection failed: ${err.message}`;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
if (err instanceof Error && "status" in err && err.status === 502) {
|
|
4580
|
+
const msg = err.message;
|
|
4581
|
+
if (msg) return `${NETWORK_HINT}: ${msg}`;
|
|
4582
|
+
return NETWORK_HINT;
|
|
4583
|
+
}
|
|
4584
|
+
return void 0;
|
|
4585
|
+
}
|
|
4373
4586
|
async function readDataArg(platform, dataArg) {
|
|
4374
4587
|
let raw;
|
|
4375
4588
|
if (dataArg.startsWith("@-")) {
|
|
@@ -4611,7 +4824,8 @@ async function run(platform, version, args) {
|
|
|
4611
4824
|
version
|
|
4612
4825
|
});
|
|
4613
4826
|
} catch (err) {
|
|
4614
|
-
|
|
4827
|
+
const msg = transportHint(err) ?? `connection failed: ${err.message}`;
|
|
4828
|
+
await platform.write(2, `mcpcat: ${msg}
|
|
4615
4829
|
`);
|
|
4616
4830
|
return 3;
|
|
4617
4831
|
}
|
|
@@ -4664,12 +4878,22 @@ async function kernelSpawn(ctx, options) {
|
|
|
4664
4878
|
stdout: "pipe",
|
|
4665
4879
|
stderr: "pipe"
|
|
4666
4880
|
});
|
|
4881
|
+
const childPid = result.pid;
|
|
4667
4882
|
return {
|
|
4668
4883
|
stdin: result.stdin,
|
|
4669
4884
|
stdout: result.stdout,
|
|
4670
4885
|
stderr: result.stderr,
|
|
4671
4886
|
wait: () => result.wait(),
|
|
4672
|
-
kill: (signal) =>
|
|
4887
|
+
kill: (signal) => {
|
|
4888
|
+
try {
|
|
4889
|
+
ctx.kill(-childPid, signal);
|
|
4890
|
+
} catch {
|
|
4891
|
+
}
|
|
4892
|
+
try {
|
|
4893
|
+
ctx.kill(childPid, signal);
|
|
4894
|
+
} catch {
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4673
4897
|
};
|
|
4674
4898
|
}
|
|
4675
4899
|
async function kernelListen(ctx, options) {
|
|
@@ -4735,7 +4959,7 @@ function createKernelPlatform(ctx) {
|
|
|
4735
4959
|
async function main(ctx) {
|
|
4736
4960
|
const platform = createKernelPlatform(ctx);
|
|
4737
4961
|
const args = ctx.argv.slice(1);
|
|
4738
|
-
return run(platform, "0.
|
|
4962
|
+
return run(platform, "0.5.0", args);
|
|
4739
4963
|
}
|
|
4740
4964
|
export {
|
|
4741
4965
|
main as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/mcpcat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A curl for the Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=24",
|
|
20
|
-
"slicc-kernel": ">=1.
|
|
20
|
+
"slicc-kernel": ">=1.65.2"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public",
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
"build": "node scripts/build.mjs",
|
|
47
47
|
"lint": "sh scripts/lint.sh",
|
|
48
48
|
"typecheck": "tsc",
|
|
49
|
-
"test": "npm run test:unit && npm run test:integration",
|
|
49
|
+
"test": "npm run test:unit && npm run test:integration && npm run test:kernel-min",
|
|
50
50
|
"test:unit": "mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
|
|
51
51
|
"test:integration": "node --test --test-timeout=30000 test/integration/*.test.mjs",
|
|
52
|
+
"test:kernel-min": "node scripts/test-kernel-min.mjs",
|
|
52
53
|
"cert": "node --test --test-timeout=120000 test/integration/cert.test.mjs test/integration/serve-soak.cert.mjs",
|
|
53
54
|
"release": "semantic-release",
|
|
54
55
|
"trust": "npx fledgling sync -y --skip-publish",
|