mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-13 01:50:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eed27c4fd |
No files matched your search
@@ -0,0 +1,346 @@
|
||||
"use strict";
|
||||
|
||||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const net = require("node:net");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
|
||||
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
|
||||
|
||||
const EXTERNAL_SERVER = path.join(__dirname, "netcatty-external-mcp-server.cjs");
|
||||
const MANAGED_SERVER = path.join(__dirname, "netcatty-mcp-server.cjs");
|
||||
|
||||
function errorText(result) {
|
||||
return (result?.content || []).map((part) => part?.text || "").join("\n");
|
||||
}
|
||||
|
||||
function writeDiscovery(filePath, { port, token }) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify({
|
||||
version: 1,
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
token,
|
||||
permissionMode: "confirm",
|
||||
chatSessionId: "__external_mcp__",
|
||||
updatedAt: new Date().toISOString(),
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
function startBridge({ token, authDelayMs = 0, onGetContext = null }) {
|
||||
const calls = [];
|
||||
const sockets = new Set();
|
||||
let connections = 0;
|
||||
const server = net.createServer((socket) => {
|
||||
connections += 1;
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
let buffer = "";
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk;
|
||||
let newlineIdx;
|
||||
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
||||
const line = buffer.slice(0, newlineIdx);
|
||||
buffer = buffer.slice(newlineIdx + 1);
|
||||
if (!line.trim()) continue;
|
||||
const msg = JSON.parse(line);
|
||||
calls.push({
|
||||
id: msg.id,
|
||||
method: msg.method,
|
||||
token: msg.params?.token,
|
||||
});
|
||||
const respond = () => {
|
||||
if (msg.method === "auth/verify") {
|
||||
if (msg.params?.token !== token) {
|
||||
socket.write(`${JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: msg.id,
|
||||
error: { code: -32001, message: "Authentication required. Send auth/verify with valid token first." },
|
||||
})}\n`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
socket.write(`${JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: { ok: true } })}\n`);
|
||||
return;
|
||||
}
|
||||
if (msg.method === "netcatty/getContext" && onGetContext) {
|
||||
const action = onGetContext(msg, socket);
|
||||
if (action === "hold-and-close") {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
socket.write(`${JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: msg.id,
|
||||
result: { ok: true, marker: token },
|
||||
})}\n`);
|
||||
};
|
||||
if (msg.method === "auth/verify" && authDelayMs > 0) {
|
||||
setTimeout(respond, authDelayMs);
|
||||
} else {
|
||||
respond();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve(server.address().port));
|
||||
});
|
||||
|
||||
return {
|
||||
calls,
|
||||
ready,
|
||||
get connections() {
|
||||
return connections;
|
||||
},
|
||||
destroySockets() {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
},
|
||||
close() {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
if (!server.listening) return Promise.resolve();
|
||||
return new Promise((resolve) => server.close(() => resolve()));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function connectClient(entry, env) {
|
||||
const client = new Client({ name: "external-mcp-reconnect-test", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entry],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const stderr = [];
|
||||
transport.stderr.on("data", (chunk) => stderr.push(String(chunk)));
|
||||
try {
|
||||
await client.connect(transport);
|
||||
} catch (error) {
|
||||
error.message = `${error.message}\nstderr:\n${stderr.join("")}`;
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
client,
|
||||
stderr,
|
||||
async close() {
|
||||
await client.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function childEnv(overrides) {
|
||||
const env = { ...process.env };
|
||||
delete env.NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE;
|
||||
delete env.NETCATTY_MCP_PORT;
|
||||
delete env.NETCATTY_MCP_TOKEN;
|
||||
delete env.NETCATTY_MCP_PERMISSION_MODE;
|
||||
delete env.NETCATTY_MCP_CHAT_SESSION_ID;
|
||||
delete env.NETCATTY_MCP_SESSION_IDS;
|
||||
return { ...env, ...overrides };
|
||||
}
|
||||
|
||||
async function callEnvironment(client) {
|
||||
return client.callTool({ name: "get_environment", arguments: {} });
|
||||
}
|
||||
|
||||
test("external stdio bridge reconnects with refreshed discovery after the app endpoint changes", async (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mcp-reconnect-"));
|
||||
const discoveryPath = path.join(tmp, "discovery.json");
|
||||
const first = startBridge({ token: "token-first" });
|
||||
const firstPort = await first.ready;
|
||||
writeDiscovery(discoveryPath, { port: firstPort, token: "token-first" });
|
||||
const child = await connectClient(EXTERNAL_SERVER, childEnv({
|
||||
NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE: discoveryPath,
|
||||
}));
|
||||
t.after(async () => {
|
||||
await child.close().catch(() => {});
|
||||
await first.close().catch(() => {});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const live = await callEnvironment(child.client);
|
||||
assert.equal(live.isError, undefined);
|
||||
assert.match(errorText(live), /token-first/);
|
||||
|
||||
await first.close();
|
||||
const second = startBridge({ token: "token-restarted" });
|
||||
t.after(() => second.close().catch(() => {}));
|
||||
const secondPort = await second.ready;
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "token-restarted" });
|
||||
|
||||
const recovered = await callEnvironment(child.client);
|
||||
assert.notEqual(recovered.isError, true, errorText(recovered));
|
||||
assert.match(errorText(recovered), /token-restarted/);
|
||||
assert.deepEqual(
|
||||
second.calls.map((call) => call.method),
|
||||
["auth/verify", "netcatty/getContext"],
|
||||
);
|
||||
assert.equal(second.calls[0].token, "token-restarted");
|
||||
assert.equal(second.connections, 1);
|
||||
});
|
||||
|
||||
test("concurrent external calls share one reconnect and one re-auth", async (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mcp-reconnect-"));
|
||||
const discoveryPath = path.join(tmp, "discovery.json");
|
||||
const first = startBridge({ token: "token-first" });
|
||||
const firstPort = await first.ready;
|
||||
writeDiscovery(discoveryPath, { port: firstPort, token: "token-first" });
|
||||
const child = await connectClient(EXTERNAL_SERVER, childEnv({
|
||||
NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE: discoveryPath,
|
||||
}));
|
||||
let second = null;
|
||||
t.after(async () => {
|
||||
await child.close().catch(() => {});
|
||||
await first.close().catch(() => {});
|
||||
if (second) await second.close().catch(() => {});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.notEqual((await callEnvironment(child.client)).isError, true);
|
||||
await first.close();
|
||||
// Hold auth open so both tool calls enter recovery before the socket exists.
|
||||
second = startBridge({ token: "token-restarted", authDelayMs: 300 });
|
||||
const secondPort = await second.ready;
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "token-restarted" });
|
||||
|
||||
const [left, right] = await Promise.all([
|
||||
callEnvironment(child.client),
|
||||
callEnvironment(child.client),
|
||||
]);
|
||||
assert.notEqual(left.isError, true, errorText(left));
|
||||
assert.notEqual(right.isError, true, errorText(right));
|
||||
assert.match(errorText(left), /token-restarted/);
|
||||
assert.match(errorText(right), /token-restarted/);
|
||||
assert.equal(second.connections, 1);
|
||||
assert.equal(second.calls.filter((call) => call.method === "auth/verify").length, 1);
|
||||
assert.equal(second.calls.filter((call) => call.method === "netcatty/getContext").length, 2);
|
||||
assert.equal(second.calls[0].token, "token-restarted");
|
||||
});
|
||||
|
||||
test("in-flight external calls are rejected on disconnect and are not replayed", async (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mcp-reconnect-"));
|
||||
const discoveryPath = path.join(tmp, "discovery.json");
|
||||
let holdNext = false;
|
||||
const first = startBridge({
|
||||
token: "token-first",
|
||||
onGetContext: () => (holdNext ? "hold-and-close" : null),
|
||||
});
|
||||
const firstPort = await first.ready;
|
||||
writeDiscovery(discoveryPath, { port: firstPort, token: "token-first" });
|
||||
const child = await connectClient(EXTERNAL_SERVER, childEnv({
|
||||
NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE: discoveryPath,
|
||||
}));
|
||||
let second = null;
|
||||
t.after(async () => {
|
||||
await child.close().catch(() => {});
|
||||
await first.close().catch(() => {});
|
||||
if (second) await second.close().catch(() => {});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.notEqual((await callEnvironment(child.client)).isError, true);
|
||||
holdNext = true;
|
||||
const dropped = await callEnvironment(child.client);
|
||||
assert.equal(dropped.isError, true);
|
||||
assert.match(errorText(dropped), /TCP connection (closed|lost)/);
|
||||
const droppedCall = first.calls.filter((call) => call.method === "netcatty/getContext").at(-1);
|
||||
assert.ok(droppedCall);
|
||||
|
||||
await first.close();
|
||||
second = startBridge({ token: "token-restarted" });
|
||||
const secondPort = await second.ready;
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "token-restarted" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
assert.equal(second.connections, 0);
|
||||
|
||||
const recovered = await callEnvironment(child.client);
|
||||
assert.notEqual(recovered.isError, true, errorText(recovered));
|
||||
assert.match(errorText(recovered), /token-restarted/);
|
||||
assert.deepEqual(
|
||||
second.calls.map((call) => call.method),
|
||||
["auth/verify", "netcatty/getContext"],
|
||||
);
|
||||
assert.equal(second.calls.some((call) => call.id === droppedCall.id), false);
|
||||
});
|
||||
|
||||
test("missing or rejected discovery stays recoverable without killing the external process", async (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mcp-reconnect-"));
|
||||
const discoveryPath = path.join(tmp, "discovery.json");
|
||||
const first = startBridge({ token: "token-first" });
|
||||
const firstPort = await first.ready;
|
||||
writeDiscovery(discoveryPath, { port: firstPort, token: "token-first" });
|
||||
const child = await connectClient(EXTERNAL_SERVER, childEnv({
|
||||
NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE: discoveryPath,
|
||||
}));
|
||||
let second = null;
|
||||
t.after(async () => {
|
||||
await child.close().catch(() => {});
|
||||
await first.close().catch(() => {});
|
||||
if (second) await second.close().catch(() => {});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.notEqual((await callEnvironment(child.client)).isError, true);
|
||||
await first.close();
|
||||
fs.rmSync(discoveryPath, { force: true });
|
||||
|
||||
const missing = await callEnvironment(child.client);
|
||||
assert.equal(missing.isError, true);
|
||||
assert.match(errorText(missing), /temporarily unavailable|unavailable or invalid/i);
|
||||
|
||||
second = startBridge({ token: "token-restarted" });
|
||||
const secondPort = await second.ready;
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "wrong-token" });
|
||||
const rejected = await callEnvironment(child.client);
|
||||
assert.equal(rejected.isError, true);
|
||||
assert.match(errorText(rejected), /temporarily unavailable/i);
|
||||
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "token-restarted" });
|
||||
const recovered = await callEnvironment(child.client);
|
||||
assert.notEqual(recovered.isError, true, `${errorText(recovered)}\n${child.stderr.join("")}`);
|
||||
assert.match(errorText(recovered), /token-restarted/);
|
||||
});
|
||||
|
||||
test("managed stdio children fail fast without rereading discovery", async (t) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mcp-reconnect-"));
|
||||
const discoveryPath = path.join(tmp, "discovery.json");
|
||||
const first = startBridge({ token: "managed-token" });
|
||||
const firstPort = await first.ready;
|
||||
const child = await connectClient(MANAGED_SERVER, childEnv({
|
||||
NETCATTY_MCP_PORT: String(firstPort),
|
||||
NETCATTY_MCP_TOKEN: "managed-token",
|
||||
}));
|
||||
let second = null;
|
||||
t.after(async () => {
|
||||
await child.close().catch(() => {});
|
||||
await first.close().catch(() => {});
|
||||
if (second) await second.close().catch(() => {});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const live = await callEnvironment(child.client);
|
||||
assert.notEqual(live.isError, true, errorText(live));
|
||||
await first.close();
|
||||
|
||||
second = startBridge({ token: "token-restarted" });
|
||||
const secondPort = await second.ready;
|
||||
writeDiscovery(discoveryPath, { port: secondPort, token: "token-restarted" });
|
||||
|
||||
const started = Date.now();
|
||||
const failed = await callEnvironment(child.client);
|
||||
assert.ok(Date.now() - started < 1000, "managed disconnect must not wait on discovery refresh");
|
||||
assert.equal(failed.isError, true);
|
||||
assert.match(errorText(failed), /Not connected to Netcatty/);
|
||||
assert.equal(second.connections, 0);
|
||||
|
||||
const stillAlive = await callEnvironment(child.client);
|
||||
assert.equal(stillAlive.isError, true);
|
||||
assert.match(errorText(stillAlive), /Not connected to Netcatty/);
|
||||
assert.equal(second.connections, 0);
|
||||
});
|
||||
@@ -38,17 +38,21 @@ function debugLog(...args) {
|
||||
|
||||
// ── TCP Bridge to Netcatty main process ──
|
||||
|
||||
const NETCATTY_MCP_PORT = parseInt(process.env.NETCATTY_MCP_PORT, 10);
|
||||
if (!NETCATTY_MCP_PORT) {
|
||||
process.stderr.write("[netcatty-mcp] NETCATTY_MCP_PORT not set\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Auth token for TCP bridge authentication
|
||||
const NETCATTY_MCP_TOKEN = process.env.NETCATTY_MCP_TOKEN || "";
|
||||
if (!NETCATTY_MCP_TOKEN) {
|
||||
process.stderr.write("[netcatty-mcp] NETCATTY_MCP_TOKEN not set\n");
|
||||
process.exit(1);
|
||||
// External stdio clients outlive Netcatty. Their port and token stay mutable so
|
||||
// a later call can refresh discovery. Managed children receive a fixed env and
|
||||
// keep the original fail-fast exit.
|
||||
const externalDiscoveryPath = process.env.NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE;
|
||||
let NETCATTY_MCP_PORT = parseInt(process.env.NETCATTY_MCP_PORT, 10);
|
||||
let NETCATTY_MCP_TOKEN = process.env.NETCATTY_MCP_TOKEN || "";
|
||||
if (!externalDiscoveryPath) {
|
||||
if (!NETCATTY_MCP_PORT) {
|
||||
process.stderr.write("[netcatty-mcp] NETCATTY_MCP_PORT not set\n");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!NETCATTY_MCP_TOKEN) {
|
||||
process.stderr.write("[netcatty-mcp] NETCATTY_MCP_TOKEN not set\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Scoped session IDs (comma-separated). When set (even if empty), only listed
|
||||
@@ -132,6 +136,7 @@ function connectTcp() {
|
||||
sock.setEncoding("utf-8");
|
||||
const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
sock.on("data", (chunk) => {
|
||||
if (tcpSocket !== sock) return;
|
||||
tcpBuffer += chunk;
|
||||
if (tcpBuffer.length > MAX_BUFFER_SIZE) {
|
||||
process.stderr.write(`[netcatty-mcp] TCP buffer exceeded ${MAX_BUFFER_SIZE} bytes, clearing buffer\n`);
|
||||
@@ -167,7 +172,8 @@ function connectTcp() {
|
||||
sock.on("error", (err) => {
|
||||
debugLog("TCP socket error", { message: err?.message || String(err) });
|
||||
reject(err);
|
||||
// Reject all pending
|
||||
if (tcpSocket && tcpSocket !== sock) return;
|
||||
// Reject in-flight calls only. They are not replayed after reconnect.
|
||||
for (const { reject: rej } of pendingRequests.values()) {
|
||||
rej(new Error("TCP connection lost"));
|
||||
}
|
||||
@@ -175,20 +181,21 @@ function connectTcp() {
|
||||
});
|
||||
sock.on("close", () => {
|
||||
debugLog("TCP socket closed");
|
||||
// Reject all pending requests on clean close
|
||||
if (tcpSocket && tcpSocket !== sock) return;
|
||||
for (const { reject: rej } of pendingRequests.values()) {
|
||||
rej(new Error("TCP connection closed"));
|
||||
}
|
||||
pendingRequests.clear();
|
||||
tcpSocket = null;
|
||||
if (tcpSocket === sock) tcpSocket = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rpcCall(method, params) {
|
||||
function sendRpc(method, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!tcpSocket || tcpSocket.destroyed) {
|
||||
return reject(new Error("Not connected to Netcatty"));
|
||||
reject(new Error("Not connected to Netcatty"));
|
||||
return;
|
||||
}
|
||||
const id = nextRpcId++;
|
||||
pendingRequests.set(id, { resolve, reject });
|
||||
@@ -198,8 +205,73 @@ function rpcCall(method, params) {
|
||||
});
|
||||
}
|
||||
|
||||
function temporaryUnavailable(error) {
|
||||
const message = error?.message || String(error || "connection failed");
|
||||
if (message.startsWith("Netcatty is temporarily unavailable")) {
|
||||
return error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
return new Error(`Netcatty is temporarily unavailable: ${message}`);
|
||||
}
|
||||
|
||||
function refreshExternalEndpoint() {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(externalDiscoveryPath, "utf8"));
|
||||
} catch {
|
||||
throw new Error("Netcatty is temporarily unavailable: discovery file is missing or unreadable");
|
||||
}
|
||||
const port = parseInt(parsed?.port, 10);
|
||||
const token = typeof parsed?.token === "string" ? parsed.token.trim() : "";
|
||||
if (!port || !token) {
|
||||
throw new Error("Netcatty is temporarily unavailable: discovery endpoint is incomplete");
|
||||
}
|
||||
NETCATTY_MCP_PORT = port;
|
||||
NETCATTY_MCP_TOKEN = token;
|
||||
}
|
||||
|
||||
function dropActiveSocket() {
|
||||
const sock = tcpSocket;
|
||||
tcpSocket = null;
|
||||
tcpBuffer = "";
|
||||
if (sock && !sock.destroyed) sock.destroy();
|
||||
}
|
||||
|
||||
// One reconnect at a time. Auth uses sendRpc so callers waiting on this promise
|
||||
// cannot deadlock, and calls already rejected by close/error stay rejected.
|
||||
let connecting = null;
|
||||
|
||||
async function connectAndAuth() {
|
||||
if (tcpSocket?.destroyed) tcpSocket = null;
|
||||
refreshExternalEndpoint();
|
||||
tcpBuffer = "";
|
||||
try {
|
||||
await connectTcp();
|
||||
const authResult = await sendRpc("auth/verify", { token: NETCATTY_MCP_TOKEN });
|
||||
if (!authResult?.ok) {
|
||||
throw new Error("Netcatty is temporarily unavailable: TCP bridge authentication failed");
|
||||
}
|
||||
} catch (error) {
|
||||
dropActiveSocket();
|
||||
throw temporaryUnavailable(error);
|
||||
}
|
||||
process.stderr.write("[netcatty-mcp] Authenticated with TCP bridge\n");
|
||||
}
|
||||
|
||||
function ensureConnected() {
|
||||
if (tcpSocket && !tcpSocket.destroyed) return Promise.resolve();
|
||||
if (connecting) return connecting;
|
||||
connecting = connectAndAuth().finally(() => {
|
||||
connecting = null;
|
||||
});
|
||||
return connecting;
|
||||
}
|
||||
|
||||
function rpcCall(method, params) {
|
||||
if (!externalDiscoveryPath) return sendRpc(method, params);
|
||||
return ensureConnected().then(() => sendRpc(method, params));
|
||||
}
|
||||
|
||||
// External clients own connection maps here; managed agents use the main-process chat boundary.
|
||||
const externalDiscoveryPath = process.env.NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE;
|
||||
const exposedRpcCall = externalDiscoveryPath
|
||||
? createPrivacyBoundary(rpcCall, () => {
|
||||
const discovery = JSON.parse(fs.readFileSync(externalDiscoveryPath, "utf8"));
|
||||
@@ -258,16 +330,20 @@ async function main() {
|
||||
scopedSessionIds: SCOPED_SESSION_IDS,
|
||||
chatSessionId: CHAT_SESSION_ID,
|
||||
permissionMode: readPermissionMode(),
|
||||
externalDiscovery: Boolean(externalDiscoveryPath),
|
||||
});
|
||||
await connectTcp();
|
||||
|
||||
// Authenticate with the TCP bridge before accepting any tool calls
|
||||
const authResult = await rpcCall("auth/verify", { token: NETCATTY_MCP_TOKEN });
|
||||
debugLog("auth/verify result", authResult);
|
||||
if (!authResult?.ok) {
|
||||
throw new Error("TCP bridge authentication failed");
|
||||
if (externalDiscoveryPath) {
|
||||
await ensureConnected();
|
||||
} else {
|
||||
await connectTcp();
|
||||
// Authenticate with the TCP bridge before accepting any tool calls.
|
||||
const authResult = await rpcCall("auth/verify", { token: NETCATTY_MCP_TOKEN });
|
||||
debugLog("auth/verify result", authResult);
|
||||
if (!authResult?.ok) {
|
||||
throw new Error("TCP bridge authentication failed");
|
||||
}
|
||||
process.stderr.write("[netcatty-mcp] Authenticated with TCP bridge\n");
|
||||
}
|
||||
process.stderr.write("[netcatty-mcp] Authenticated with TCP bridge\n");
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
// Protocol.connect keeps this callback and runs it before request dispatch.
|
||||
|
||||
Reference in new issue
Block a user