staff-mcp 1.0.29 → 1.0.30
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/src/server.js +3 -5
- package/dist/src/server.js.map +1 -1
- package/dist/src/transports/fte-lifecycle.d.ts +13 -0
- package/dist/src/transports/fte-lifecycle.js +76 -0
- package/dist/src/transports/fte-lifecycle.js.map +1 -0
- package/dist/src/transports/reverse.d.ts +14 -8
- package/dist/src/transports/reverse.js +36 -17
- package/dist/src/transports/reverse.js.map +1 -1
- package/dist/tests/fte-lifecycle.test.d.ts +1 -0
- package/dist/tests/fte-lifecycle.test.js +118 -0
- package/dist/tests/fte-lifecycle.test.js.map +1 -0
- package/dist/tests/reverse-reconnect.test.d.ts +1 -0
- package/dist/tests/reverse-reconnect.test.js +89 -0
- package/dist/tests/reverse-reconnect.test.js.map +1 -0
- package/package.json +5 -2
package/dist/src/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerSkillTools } from "./tools/skills.js";
|
|
|
7
7
|
import { registerMcpClientTools } from "./tools/mcp-client-tools.js";
|
|
8
8
|
import { getMcpInstructions } from "./tools/system-tools.js";
|
|
9
9
|
import { FteServer } from "mcp-fte";
|
|
10
|
+
import { installFteTransportLifecycle } from "./transports/fte-lifecycle.js";
|
|
10
11
|
/**
|
|
11
12
|
* Creates and initializes a new McpServer with all functional tools.
|
|
12
13
|
* @param name The server's identification name.
|
|
@@ -44,11 +45,8 @@ export function createServerFactory(name, version, workingDir, allowedDirs, prof
|
|
|
44
45
|
registerSkillTools(server, workingDir, security, profile);
|
|
45
46
|
registerMcpClientTools(server, { maxSessions: maxMcpSessions });
|
|
46
47
|
// Intercept connect to wrap transport with FTE server-side processing
|
|
47
|
-
|
|
48
|
-
server
|
|
49
|
-
const wrapped = FteServer.wrapTransport(transport, { sandbox });
|
|
50
|
-
return originalConnect(wrapped);
|
|
51
|
-
};
|
|
48
|
+
// while ensuring each reconnect also disposes the previous FTE wrapper.
|
|
49
|
+
installFteTransportLifecycle(server, sandbox);
|
|
52
50
|
return server;
|
|
53
51
|
};
|
|
54
52
|
}
|
package/dist/src/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,OAAe,EACf,UAAkB,EAClB,WAAqB,EACrB,UAAkB,SAAS,EAC3B,iBAAyB,CAAC,EAC1B,YAAqB,KAAK;IAE1B,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,EAAE,CAAC;IAE1C,uEAAuE;IACvE,MAAM,YAAY,GAAG,kBAAkB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAE9D,OAAO,GAAG,EAAE;QACV,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B;YACE,IAAI;YACJ,OAAO;SACR,EACD;YACE,yDAAyD;YACzD,YAAY;YACZ,6BAA6B;YAC7B,YAAY,EAAE;gBACZ,GAAG,SAAS,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;aACvC;SACF,CACF,CAAC;QAEF,uCAAuC;QACvC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACpC,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAErC,IAAI,SAAS,EAAE,CAAC;YACd,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACrC,CAAC;QAED,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC1D,sBAAsB,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;QAEhE,sEAAsE;QACtE,wEAAwE;QACxE,4BAA4B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAE9C,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import { type FteServerConfig } from "mcp-fte";
|
|
4
|
+
export type FteTransportFactory = (transport: Transport, config: FteServerConfig) => Transport;
|
|
5
|
+
/**
|
|
6
|
+
* Install FTE wrapping while preserving transport lifecycle semantics.
|
|
7
|
+
*
|
|
8
|
+
* mcp-reverse transports can close themselves before the MCP SDK calls the
|
|
9
|
+
* wrapper's close() method. Without this bridge, FteServer's timer and transfer
|
|
10
|
+
* sessions would survive every reconnect. The composed onclose handler clears
|
|
11
|
+
* the SDK binding first, then closes the FTE wrapper exactly once.
|
|
12
|
+
*/
|
|
13
|
+
export declare function installFteTransportLifecycle(server: McpServer, sandbox: string[], wrapTransport?: FteTransportFactory): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FteServer } from "mcp-fte";
|
|
2
|
+
/**
|
|
3
|
+
* Install FTE wrapping while preserving transport lifecycle semantics.
|
|
4
|
+
*
|
|
5
|
+
* mcp-reverse transports can close themselves before the MCP SDK calls the
|
|
6
|
+
* wrapper's close() method. Without this bridge, FteServer's timer and transfer
|
|
7
|
+
* sessions would survive every reconnect. The composed onclose handler clears
|
|
8
|
+
* the SDK binding first, then closes the FTE wrapper exactly once.
|
|
9
|
+
*/
|
|
10
|
+
export function installFteTransportLifecycle(server, sandbox, wrapTransport = (transport, config) => FteServer.wrapTransport(transport, config)) {
|
|
11
|
+
const originalConnect = server.connect.bind(server);
|
|
12
|
+
server.connect = async (transport) => {
|
|
13
|
+
const wrapped = wrapTransport(transport, { sandbox });
|
|
14
|
+
const rawWrappedClose = wrapped.close.bind(wrapped);
|
|
15
|
+
let closing = false;
|
|
16
|
+
let closePromise = Promise.resolve();
|
|
17
|
+
const closeWrappedOnce = () => {
|
|
18
|
+
if (closing)
|
|
19
|
+
return closePromise;
|
|
20
|
+
closing = true;
|
|
21
|
+
try {
|
|
22
|
+
closePromise = Promise.resolve(rawWrappedClose());
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
closePromise = Promise.reject(error);
|
|
26
|
+
}
|
|
27
|
+
return closePromise;
|
|
28
|
+
};
|
|
29
|
+
// Any SDK-driven close must use the same idempotent cleanup path.
|
|
30
|
+
wrapped.close = closeWrappedOnce;
|
|
31
|
+
try {
|
|
32
|
+
await originalConnect(wrapped);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
await closeWrappedOnce().catch((cleanupError) => {
|
|
36
|
+
console.error("[staff-mcp] Failed to clean up FTE transport after connect failure:", cleanupError);
|
|
37
|
+
});
|
|
38
|
+
// Some transports reject start() without firing onclose. Release only if
|
|
39
|
+
// this exact wrapper is still bound: an asynchronous stale cleanup must
|
|
40
|
+
// never clear a newer reconnect that has already succeeded.
|
|
41
|
+
if (server.server.transport === wrapped) {
|
|
42
|
+
try {
|
|
43
|
+
wrapped.onclose?.();
|
|
44
|
+
}
|
|
45
|
+
catch (cleanupError) {
|
|
46
|
+
console.error("[staff-mcp] Failed to release MCP transport after connect failure:", cleanupError);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
// Protocol.connect() installs its callbacks during originalConnect().
|
|
52
|
+
// Compose only afterwards so both the SDK and FTE cleanup always run.
|
|
53
|
+
const protocolOnClose = wrapped.onclose;
|
|
54
|
+
let closeHandled = false;
|
|
55
|
+
wrapped.onclose = () => {
|
|
56
|
+
if (closeHandled)
|
|
57
|
+
return;
|
|
58
|
+
closeHandled = true;
|
|
59
|
+
try {
|
|
60
|
+
protocolOnClose?.();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
void closeWrappedOnce().catch((cleanupError) => {
|
|
64
|
+
console.error("[staff-mcp] Failed to clean up closed FTE transport:", cleanupError);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Cover the narrow race where this delegate closes after connect resolves
|
|
69
|
+
// but before the composed callback above is installed. Transport identity,
|
|
70
|
+
// rather than global connected state, protects a newer concurrent binding.
|
|
71
|
+
if (server.server.transport !== wrapped) {
|
|
72
|
+
await closeWrappedOnce();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=fte-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fte-lifecycle.js","sourceRoot":"","sources":["../../../src/transports/fte-lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAwB,MAAM,SAAS,CAAC;AAO1D;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAC1C,MAAiB,EACjB,OAAiB,EACjB,gBAAqC,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CACzD,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;IAE5C,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEpD,MAAM,CAAC,OAAO,GAAG,KAAK,EAAE,SAAoB,EAAiB,EAAE;QAC7D,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACtD,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEpD,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,YAAY,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;QACpD,MAAM,gBAAgB,GAAG,GAAkB,EAAE;YAC3C,IAAI,OAAO;gBAAE,OAAO,YAAY,CAAC;YACjC,OAAO,GAAG,IAAI,CAAC;YAEf,IAAI,CAAC;gBACH,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,YAAY,CAAC;QACtB,CAAC,CAAC;QAEF,kEAAkE;QAClE,OAAO,CAAC,KAAK,GAAG,gBAAgB,CAAC;QAEjC,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,EAAE;gBAC9C,OAAO,CAAC,KAAK,CAAC,qEAAqE,EAAE,YAAY,CAAC,CAAC;YACrG,CAAC,CAAC,CAAC;YAEH,yEAAyE;YACzE,wEAAwE;YACxE,4DAA4D;YAC5D,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;gBACxC,IAAI,CAAC;oBACH,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;gBACtB,CAAC;gBAAC,OAAO,YAAY,EAAE,CAAC;oBACtB,OAAO,CAAC,KAAK,CAAC,oEAAoE,EAAE,YAAY,CAAC,CAAC;gBACpG,CAAC;YACH,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QAED,sEAAsE;QACtE,sEAAsE;QACtE,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;QACxC,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE;YACrB,IAAI,YAAY;gBAAE,OAAO;YACzB,YAAY,GAAG,IAAI,CAAC;YAEpB,IAAI,CAAC;gBACH,eAAe,EAAE,EAAE,CAAC;YACtB,CAAC;oBAAS,CAAC;gBACT,KAAK,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,EAAE;oBAC7C,OAAO,CAAC,KAAK,CAAC,sDAAsD,EAAE,YAAY,CAAC,CAAC;gBACtF,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC;QAEF,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;YACxC,MAAM,gBAAgB,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Logger, ReconnectOptions } from "mcp-reverse";
|
|
3
|
+
import { ReverseMCPClient } from "mcp-reverse/client";
|
|
4
|
+
export interface ReverseServerOptions {
|
|
5
|
+
/** Timeout for establishing each SSE connection. 0 disables the timeout. */
|
|
6
|
+
connectTimeout?: number;
|
|
7
|
+
/** Override the automatic reconnect policy. */
|
|
8
|
+
reconnect?: ReconnectOptions;
|
|
9
|
+
/** Override reverse transport and lifecycle logging. */
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Start a reverse MCP connection to a public gateway.
|
|
4
14
|
*
|
|
5
|
-
* The ReverseMCPClient
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param server The McpServer instance (with all tools registered)
|
|
9
|
-
* @param url Base URL of the reverse MCP endpoint
|
|
10
|
-
* @param token Authentication token
|
|
11
|
-
* @param name Server name for identification
|
|
15
|
+
* The ReverseMCPClient owns transport lifecycle, reconnection, and MCP
|
|
16
|
+
* protocol re-binding. start() launches the connection loop; readiness is
|
|
17
|
+
* reported asynchronously through the connected event/log.
|
|
12
18
|
*/
|
|
13
|
-
export declare function startReverseServer(server: McpServer, url: string, token: string, name: string): Promise<
|
|
19
|
+
export declare function startReverseServer(server: McpServer, url: string, token: string, name: string, options?: ReverseServerOptions): Promise<ReverseMCPClient>;
|
|
@@ -1,43 +1,62 @@
|
|
|
1
1
|
import { ReverseMCPClient } from "mcp-reverse/client";
|
|
2
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
|
|
3
|
+
function createReverseLogger() {
|
|
4
|
+
const debugEnabled = process.env.STAFF_MCP_REVERSE_DEBUG === "1";
|
|
5
|
+
return {
|
|
6
|
+
debug: debugEnabled
|
|
7
|
+
? (message, ...args) => console.error(`[staff-mcp][reverse][debug] ${message}`, ...args)
|
|
8
|
+
: () => { },
|
|
9
|
+
info: (message, ...args) => console.error(`[staff-mcp][reverse][info] ${message}`, ...args),
|
|
10
|
+
warn: (message, ...args) => console.error(`[staff-mcp][reverse][warn] ${message}`, ...args),
|
|
11
|
+
error: (message, ...args) => console.error(`[staff-mcp][reverse][error] ${message}`, ...args),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* Start a reverse MCP connection to a public gateway.
|
|
4
16
|
*
|
|
5
|
-
* The ReverseMCPClient
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param server The McpServer instance (with all tools registered)
|
|
9
|
-
* @param url Base URL of the reverse MCP endpoint
|
|
10
|
-
* @param token Authentication token
|
|
11
|
-
* @param name Server name for identification
|
|
17
|
+
* The ReverseMCPClient owns transport lifecycle, reconnection, and MCP
|
|
18
|
+
* protocol re-binding. start() launches the connection loop; readiness is
|
|
19
|
+
* reported asynchronously through the connected event/log.
|
|
12
20
|
*/
|
|
13
|
-
export async function startReverseServer(server, url, token, name) {
|
|
21
|
+
export async function startReverseServer(server, url, token, name, options = {}) {
|
|
22
|
+
const reconnectEnabled = options.reconnect?.enabled !== false;
|
|
23
|
+
const reverseLogger = options.logger ?? createReverseLogger();
|
|
14
24
|
const client = await ReverseMCPClient.createSSE(server, {
|
|
15
25
|
url,
|
|
16
26
|
serverName: name,
|
|
17
27
|
authToken: token,
|
|
28
|
+
connectTimeout: options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
18
29
|
reconnect: {
|
|
19
30
|
initialDelay: 1000,
|
|
20
31
|
maxDelay: 30000,
|
|
21
32
|
multiplier: 2,
|
|
22
33
|
maxRetries: 0, // 0 = infinite
|
|
34
|
+
...options.reconnect,
|
|
23
35
|
},
|
|
24
|
-
});
|
|
36
|
+
}, reverseLogger);
|
|
25
37
|
client.on("connected", () => {
|
|
26
|
-
|
|
38
|
+
reverseLogger.info(`SSE transport connected as "${name}"`);
|
|
27
39
|
});
|
|
28
40
|
client.on("disconnected", () => {
|
|
29
|
-
|
|
41
|
+
reverseLogger.warn(reconnectEnabled
|
|
42
|
+
? "Gateway connection closed; automatic reconnect is enabled"
|
|
43
|
+
: "Gateway connection closed; automatic reconnect is disabled");
|
|
30
44
|
});
|
|
31
|
-
client.on("reconnecting", (
|
|
32
|
-
|
|
45
|
+
client.on("reconnecting", (completedAttempts) => {
|
|
46
|
+
if (completedAttempts === 0) {
|
|
47
|
+
reverseLogger.info("Reconnect cycle started");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
reverseLogger.info(`${completedAttempts} reconnect attempt(s) have failed`);
|
|
51
|
+
}
|
|
33
52
|
});
|
|
34
53
|
client.on("error", (error) => {
|
|
35
|
-
|
|
54
|
+
reverseLogger.error(`Transport reported an error: ${error.message}`);
|
|
36
55
|
});
|
|
37
|
-
client.on("failed", () => {
|
|
38
|
-
|
|
39
|
-
process.exit(1);
|
|
56
|
+
client.on("failed", (error) => {
|
|
57
|
+
reverseLogger.error(`Reconnect budget exhausted: ${error.message}`);
|
|
40
58
|
});
|
|
41
59
|
await client.start();
|
|
60
|
+
return client;
|
|
42
61
|
}
|
|
43
62
|
//# sourceMappingURL=reverse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reverse.js","sourceRoot":"","sources":["../../../src/transports/reverse.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reverse.js","sourceRoot":"","sources":["../../../src/transports/reverse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { FteServer } from "mcp-fte";
|
|
5
|
+
import { installFteTransportLifecycle } from "../src/transports/fte-lifecycle.js";
|
|
6
|
+
class TestTransport {
|
|
7
|
+
startFailure;
|
|
8
|
+
closeBeforeReject;
|
|
9
|
+
closeGate;
|
|
10
|
+
onclose;
|
|
11
|
+
onerror;
|
|
12
|
+
onmessage;
|
|
13
|
+
closeCalls = 0;
|
|
14
|
+
constructor(startFailure, closeBeforeReject = false, closeGate) {
|
|
15
|
+
this.startFailure = startFailure;
|
|
16
|
+
this.closeBeforeReject = closeBeforeReject;
|
|
17
|
+
this.closeGate = closeGate;
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
if (this.startFailure) {
|
|
21
|
+
if (this.closeBeforeReject)
|
|
22
|
+
this.onclose?.();
|
|
23
|
+
throw this.startFailure;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async send() { }
|
|
27
|
+
async close() {
|
|
28
|
+
this.closeCalls++;
|
|
29
|
+
this.onclose?.();
|
|
30
|
+
this.closeGate?.started();
|
|
31
|
+
await this.closeGate?.wait;
|
|
32
|
+
}
|
|
33
|
+
disconnect() {
|
|
34
|
+
this.onclose?.();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function createServer(cleanupCounter) {
|
|
38
|
+
const server = new McpServer({ name: "fte-lifecycle-test", version: "1.0.0" });
|
|
39
|
+
installFteTransportLifecycle(server, [], (transport, config) => {
|
|
40
|
+
const wrapped = FteServer.wrapTransport(transport, config);
|
|
41
|
+
const close = wrapped.close.bind(wrapped);
|
|
42
|
+
wrapped.close = async () => {
|
|
43
|
+
cleanupCounter.value++;
|
|
44
|
+
await close();
|
|
45
|
+
};
|
|
46
|
+
return wrapped;
|
|
47
|
+
});
|
|
48
|
+
return server;
|
|
49
|
+
}
|
|
50
|
+
async function flushCleanup() {
|
|
51
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
52
|
+
}
|
|
53
|
+
test("self-closing reverse transports dispose the FTE wrapper exactly once and allow reconnect", async () => {
|
|
54
|
+
const cleanupCounter = { value: 0 };
|
|
55
|
+
const server = createServer(cleanupCounter);
|
|
56
|
+
const first = new TestTransport();
|
|
57
|
+
await server.connect(first);
|
|
58
|
+
assert.equal(server.isConnected(), true);
|
|
59
|
+
first.disconnect();
|
|
60
|
+
await flushCleanup();
|
|
61
|
+
assert.equal(server.isConnected(), false);
|
|
62
|
+
assert.equal(cleanupCounter.value, 1);
|
|
63
|
+
const second = new TestTransport();
|
|
64
|
+
await server.connect(second);
|
|
65
|
+
assert.equal(server.isConnected(), true);
|
|
66
|
+
await server.close();
|
|
67
|
+
await flushCleanup();
|
|
68
|
+
assert.equal(server.isConnected(), false);
|
|
69
|
+
assert.equal(cleanupCounter.value, 2);
|
|
70
|
+
});
|
|
71
|
+
test("failed starts clean up FTE state even when the transport does not emit onclose", async () => {
|
|
72
|
+
const cleanupCounter = { value: 0 };
|
|
73
|
+
const server = createServer(cleanupCounter);
|
|
74
|
+
const failed = new TestTransport(new Error("connect failed"), false);
|
|
75
|
+
await assert.rejects(server.connect(failed), /connect failed/);
|
|
76
|
+
await flushCleanup();
|
|
77
|
+
assert.equal(server.isConnected(), false);
|
|
78
|
+
assert.equal(cleanupCounter.value, 1);
|
|
79
|
+
const next = new TestTransport();
|
|
80
|
+
await server.connect(next);
|
|
81
|
+
assert.equal(server.isConnected(), true);
|
|
82
|
+
await server.close();
|
|
83
|
+
});
|
|
84
|
+
test("failed starts that already emitted onclose still clean up only once", async () => {
|
|
85
|
+
const cleanupCounter = { value: 0 };
|
|
86
|
+
const server = createServer(cleanupCounter);
|
|
87
|
+
const failed = new TestTransport(new Error("closed while connecting"), true);
|
|
88
|
+
await assert.rejects(server.connect(failed), /closed while connecting/);
|
|
89
|
+
await flushCleanup();
|
|
90
|
+
assert.equal(server.isConnected(), false);
|
|
91
|
+
assert.equal(cleanupCounter.value, 1);
|
|
92
|
+
});
|
|
93
|
+
test("stale failed-transport cleanup cannot detach a newer connection", async () => {
|
|
94
|
+
const cleanupCounter = { value: 0 };
|
|
95
|
+
const server = createServer(cleanupCounter);
|
|
96
|
+
let markCloseStarted;
|
|
97
|
+
const closeStarted = new Promise((resolve) => {
|
|
98
|
+
markCloseStarted = resolve;
|
|
99
|
+
});
|
|
100
|
+
let releaseClose;
|
|
101
|
+
const closeWait = new Promise((resolve) => {
|
|
102
|
+
releaseClose = resolve;
|
|
103
|
+
});
|
|
104
|
+
const failed = new TestTransport(new Error("first connect failed"), false, { started: markCloseStarted, wait: closeWait });
|
|
105
|
+
const firstConnect = server.connect(failed);
|
|
106
|
+
await closeStarted;
|
|
107
|
+
// The first delegate has synchronously fired onclose, so Protocol is free to
|
|
108
|
+
// bind a replacement while the old FTE close remains asynchronously pending.
|
|
109
|
+
const replacement = new TestTransport();
|
|
110
|
+
await server.connect(replacement);
|
|
111
|
+
assert.equal(server.isConnected(), true);
|
|
112
|
+
releaseClose();
|
|
113
|
+
await assert.rejects(firstConnect, /first connect failed/);
|
|
114
|
+
assert.equal(server.isConnected(), true);
|
|
115
|
+
assert.equal(server.server.transport !== undefined, true);
|
|
116
|
+
await server.close();
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=fte-lifecycle.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fte-lifecycle.test.js","sourceRoot":"","sources":["../../tests/fte-lifecycle.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer as createNetServer } from "node:net";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { SSEAcceptor } from "mcp-reverse";
|
|
7
|
+
import { installFteTransportLifecycle } from "../src/transports/fte-lifecycle.js";
|
|
8
|
+
import { startReverseServer } from "../src/transports/reverse.js";
|
|
9
|
+
const noopLogger = {
|
|
10
|
+
debug: () => { },
|
|
11
|
+
info: () => { },
|
|
12
|
+
warn: () => { },
|
|
13
|
+
error: () => { },
|
|
14
|
+
};
|
|
15
|
+
async function freePort() {
|
|
16
|
+
const server = createNetServer();
|
|
17
|
+
await new Promise((resolve, reject) => {
|
|
18
|
+
server.once("error", reject);
|
|
19
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
20
|
+
});
|
|
21
|
+
const address = server.address();
|
|
22
|
+
if (!address || typeof address === "string")
|
|
23
|
+
throw new Error("Could not allocate port");
|
|
24
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
25
|
+
return address.port;
|
|
26
|
+
}
|
|
27
|
+
async function waitFor(predicate, timeout = 10_000) {
|
|
28
|
+
const deadline = Date.now() + timeout;
|
|
29
|
+
while (!predicate()) {
|
|
30
|
+
if (Date.now() >= deadline)
|
|
31
|
+
throw new Error("Timed out waiting for condition");
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
test("staff reverse mode reconnects through a gateway restart with FTE wrapping enabled", async () => {
|
|
36
|
+
const port = await freePort();
|
|
37
|
+
const serverName = "staff-restart-test";
|
|
38
|
+
const token = "staff-restart-token";
|
|
39
|
+
const gatewayClients = [];
|
|
40
|
+
const createAcceptor = () => {
|
|
41
|
+
const acceptor = new SSEAcceptor({
|
|
42
|
+
port,
|
|
43
|
+
host: "127.0.0.1",
|
|
44
|
+
authTokens: { [serverName]: token },
|
|
45
|
+
heartbeat: { enabled: false },
|
|
46
|
+
sessionTimeout: 5_000,
|
|
47
|
+
}, noopLogger);
|
|
48
|
+
acceptor.onConnection(async (connection) => {
|
|
49
|
+
const gatewayClient = new Client({ name: "gateway-test", version: "1.0.0" });
|
|
50
|
+
gatewayClients.push(gatewayClient);
|
|
51
|
+
await gatewayClient.connect(connection.transport);
|
|
52
|
+
});
|
|
53
|
+
return acceptor;
|
|
54
|
+
};
|
|
55
|
+
let acceptor = createAcceptor();
|
|
56
|
+
await acceptor.start();
|
|
57
|
+
const mcpServer = new McpServer({ name: serverName, version: "1.0.0" });
|
|
58
|
+
installFteTransportLifecycle(mcpServer, []);
|
|
59
|
+
const reverseClient = await startReverseServer(mcpServer, `http://127.0.0.1:${port}/mcp-reverse`, token, serverName, {
|
|
60
|
+
connectTimeout: 500,
|
|
61
|
+
reconnect: {
|
|
62
|
+
initialDelay: 10,
|
|
63
|
+
maxDelay: 20,
|
|
64
|
+
multiplier: 1,
|
|
65
|
+
jitter: false,
|
|
66
|
+
maxRetries: 0,
|
|
67
|
+
},
|
|
68
|
+
logger: noopLogger,
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
await waitFor(() => gatewayClients.length === 1 && gatewayClients[0].getServerVersion() !== undefined);
|
|
72
|
+
assert.equal(acceptor.sessionCount, 1);
|
|
73
|
+
await acceptor.close();
|
|
74
|
+
await waitFor(() => !mcpServer.isConnected());
|
|
75
|
+
// Leave the gateway down long enough for at least one failed attempt.
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
77
|
+
acceptor = createAcceptor();
|
|
78
|
+
await acceptor.start();
|
|
79
|
+
await waitFor(() => gatewayClients.length >= 2 && gatewayClients[1].getServerVersion() !== undefined);
|
|
80
|
+
assert.equal(acceptor.sessionCount, 1);
|
|
81
|
+
assert.equal(mcpServer.isConnected(), true);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await reverseClient.stop();
|
|
85
|
+
await Promise.allSettled(gatewayClients.map((client) => client.close()));
|
|
86
|
+
await acceptor.close();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=reverse-reconnect.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse-reconnect.test.js","sourceRoot":"","sources":["../../tests/reverse-reconnect.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "staff-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/ChineseAStar/staff-mcp.git"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"express": "^4.21.1",
|
|
33
33
|
"js-yaml": "^4.1.1",
|
|
34
34
|
"mcp-fte": "^1.0.2",
|
|
35
|
-
"mcp-reverse": "
|
|
35
|
+
"mcp-reverse": "1.3.0",
|
|
36
36
|
"zod": "^3.23.8"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"@types/node": "^22.9.0",
|
|
43
43
|
"tsx": "^4.19.2",
|
|
44
44
|
"typescript": "^5.6.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
45
48
|
}
|
|
46
49
|
}
|