@zswarm/mcp 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -41,7 +41,9 @@ the Zellij environment. Give the interpreter an absolute path and set
41
41
  `ZSWARM_SESSION`. Forward `ZELLIJ_PANE_ID` (or set `ZSWARM_FROM`) so send
42
42
  labels `[zswarm from=…]` with the sending pane instead of `swarm`.
43
43
  `ZSWARM_SSH` or `ZSWARM_SERVE` routes the same tool at a remote crew.
44
- Details: the [zswarm README](../../README.md).
44
+ `ZSWARM_SERVE` may be `host:port` or `ssh://user@host?servePort=9419` (MCP reuses
45
+ a healthy owned tunnel until stdin EOF, then cancels in-flight work and reaps
46
+ the SSH child). Details: the [zswarm README](../../README.md).
45
47
 
46
48
  ## License
47
49
 
@@ -3,9 +3,58 @@ import { createRequire } from "node:module";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
- import { dispatchZswarm, mcpInputSchema, MCP_TOOL_DESCRIPTION, } from "@zswarm/core";
6
+ import { createServeTunnelManager, dispatchZswarm, mcpInputSchema, MCP_TOOL_DESCRIPTION, } from "@zswarm/core";
7
7
  const { version } = createRequire(import.meta.url)("../package.json");
8
+ const SHUTDOWN_BOUND_MS = 5_000;
9
+ const serveTunnels = createServeTunnelManager({ persistIdle: true });
10
+ const requestAborts = new Set();
11
+ let inflight = 0;
12
+ let shuttingDown = false;
13
+ let shutdownPromise;
8
14
  const server = new Server({ name: "zswarm", version }, { capabilities: { tools: {} } });
15
+ const transport = new StdioServerTransport();
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+ async function shutdown() {
20
+ if (shutdownPromise)
21
+ return shutdownPromise;
22
+ shuttingDown = true;
23
+ shutdownPromise = (async () => {
24
+ for (const ac of requestAborts)
25
+ ac.abort();
26
+ const bound = Date.now() + SHUTDOWN_BOUND_MS;
27
+ while (inflight > 0 && Date.now() < bound) {
28
+ await sleep(15);
29
+ }
30
+ await Promise.race([
31
+ serveTunnels.closeAll(),
32
+ sleep(2_000),
33
+ ]);
34
+ await Promise.race([
35
+ server.close().catch(() => undefined),
36
+ sleep(500),
37
+ ]);
38
+ try {
39
+ await Promise.race([transport.close(), sleep(500)]);
40
+ }
41
+ catch {
42
+ /* already closed */
43
+ }
44
+ })();
45
+ await shutdownPromise;
46
+ }
47
+ function requestShutdown(exitAfter) {
48
+ const force = setTimeout(() => {
49
+ if (exitAfter)
50
+ process.exit(0);
51
+ }, SHUTDOWN_BOUND_MS);
52
+ void shutdown().finally(() => {
53
+ clearTimeout(force);
54
+ if (exitAfter)
55
+ process.exit(0);
56
+ });
57
+ }
9
58
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
10
59
  tools: [
11
60
  {
@@ -32,13 +81,59 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
32
81
  isError: true,
33
82
  };
34
83
  }
35
- const result = await dispatchZswarm(args, undefined, { signal: extra.signal });
36
- return {
37
- content: [
38
- { type: "text", text: JSON.stringify(result, null, 2) },
39
- ],
40
- isError: !result.ok,
41
- };
84
+ if (shuttingDown) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: JSON.stringify({
90
+ ok: false,
91
+ error: { code: "cancelled", message: "serve tunnel manager is closed" },
92
+ }),
93
+ },
94
+ ],
95
+ isError: true,
96
+ };
97
+ }
98
+ const ac = new AbortController();
99
+ const onExtraAbort = () => ac.abort();
100
+ extra.signal?.addEventListener("abort", onExtraAbort, { once: true });
101
+ requestAborts.add(ac);
102
+ inflight += 1;
103
+ try {
104
+ const result = await dispatchZswarm(args, undefined, {
105
+ signal: ac.signal,
106
+ serveTunnels,
107
+ });
108
+ return {
109
+ content: [
110
+ { type: "text", text: JSON.stringify(result, null, 2) },
111
+ ],
112
+ isError: !result.ok,
113
+ };
114
+ }
115
+ finally {
116
+ extra.signal?.removeEventListener("abort", onExtraAbort);
117
+ requestAborts.delete(ac);
118
+ inflight -= 1;
119
+ }
120
+ });
121
+ transport.onclose = () => {
122
+ requestShutdown(true);
123
+ };
124
+ transport.onerror = () => {
125
+ requestShutdown(true);
126
+ };
127
+ process.stdin.on("end", () => {
128
+ requestShutdown(true);
129
+ });
130
+ process.stdin.on("close", () => {
131
+ requestShutdown(true);
132
+ });
133
+ process.once("SIGINT", () => {
134
+ requestShutdown(true);
135
+ });
136
+ process.once("SIGTERM", () => {
137
+ requestShutdown(true);
42
138
  });
43
- const transport = new StdioServerTransport();
44
139
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "zSwarm MCP server — Zellij pane coordination",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@modelcontextprotocol/sdk": "1.30.0",
11
- "@zswarm/core": "0.1.7"
11
+ "@zswarm/core": "0.1.8"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@types/node": "22.14.1",