@chrysb/alphaclaw 0.9.0-beta.0 → 0.9.0-beta.1

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.
@@ -26,17 +26,20 @@ const resolveGatewayWsUrl = ({ openclawDir, gatewayPort }) => {
26
26
  return `${scheme}://127.0.0.1:${gatewayPort}`;
27
27
  };
28
28
 
29
+ const sessions = new Map();
29
30
  let activeTransport = null;
30
31
 
31
- const closeActiveTransport = async () => {
32
- if (!activeTransport) return;
33
- const t = activeTransport;
32
+ const closeSession = (sessionId) => {
33
+ const t = sessions.get(sessionId);
34
+ if (!t) return;
35
+ sessions.delete(sessionId);
36
+ if (activeTransport === t) activeTransport = null;
37
+ t.close().catch(() => {});
38
+ };
39
+
40
+ const closeAllSessions = () => {
41
+ for (const [id] of sessions) closeSession(id);
34
42
  activeTransport = null;
35
- try {
36
- await t.close();
37
- } catch {
38
- /* already closed */
39
- }
40
43
  };
41
44
 
42
45
  const registerMcpRoutes = ({
@@ -46,7 +49,6 @@ const registerMcpRoutes = ({
46
49
  gatewayEnv,
47
50
  openclawDir,
48
51
  }) => {
49
- // Wire bridge stdout messages → active transport
50
52
  setOnMcpMessage((message) => {
51
53
  if (!activeTransport) return;
52
54
  activeTransport.send(message).catch((err) => {
@@ -86,7 +88,7 @@ const registerMcpRoutes = ({
86
88
  });
87
89
 
88
90
  app.post("/api/mcp/stop", requireAuth, async (_req, res) => {
89
- await closeActiveTransport();
91
+ closeAllSessions();
90
92
  const result = stopMcpBridge();
91
93
  res.json(result);
92
94
  });
@@ -125,38 +127,52 @@ const registerMcpRoutes = ({
125
127
  const sessionId = req.headers["mcp-session-id"];
126
128
 
127
129
  // ── Existing session ───────────────────────────────────────
128
- if (sessionId && activeTransport) {
129
- try {
130
- await activeTransport.handleRequest(req, res, req.body);
131
- } catch (err) {
132
- console.error("[mcp] handleRequest error (existing session):", err?.message);
133
- if (!res.headersSent) {
134
- res.status(500).json({ error: "Internal transport error" });
130
+ if (sessionId) {
131
+ const transport = sessions.get(sessionId);
132
+ if (transport) {
133
+ console.log(
134
+ `[mcp] ${req.method} sessionId=${sessionId} routed to transport (sessions=${sessions.size})`,
135
+ );
136
+ try {
137
+ await transport.handleRequest(req, res, req.body);
138
+ } catch (err) {
139
+ console.error(
140
+ "[mcp] handleRequest error (existing session):",
141
+ err?.message,
142
+ );
143
+ if (!res.headersSent) {
144
+ res.status(500).json({ error: "Internal transport error" });
145
+ }
135
146
  }
147
+ } else {
148
+ console.log(
149
+ `[mcp] ${req.method} sessionId=${sessionId} → NOT FOUND (known=[${[...sessions.keys()].join(", ")}])`,
150
+ );
151
+ res.status(404).json({
152
+ jsonrpc: "2.0",
153
+ error: {
154
+ code: -32001,
155
+ message: "Session not found. The server may have been restarted.",
156
+ },
157
+ id: null,
158
+ });
136
159
  }
137
160
  return;
138
161
  }
139
162
 
140
- // ── Stale / unknown session ────────────────────────────────
141
- if (sessionId && !activeTransport) {
142
- res.status(404).json({
143
- jsonrpc: "2.0",
144
- error: {
145
- code: -32000,
146
- message: "Session not found. The server may have been restarted.",
147
- },
148
- id: null,
149
- });
150
- return;
151
- }
152
-
153
163
  // ── New session (POST without session ID) ────────────────
154
- // Let the transport validate the body internally — it will reject
155
- // non-initialize requests and return the proper JSON-RPC error.
156
- if (!sessionId && req.method === "POST") {
164
+ if (req.method === "POST") {
157
165
  const transport = new StreamableHTTPServerTransport({
158
166
  sessionIdGenerator: () => randomUUID(),
159
167
  enableJsonResponse: true,
168
+ onsessioninitialized: (newSessionId) => {
169
+ closeAllSessions();
170
+ sessions.set(newSessionId, transport);
171
+ activeTransport = transport;
172
+ console.log(
173
+ `[mcp] Session registered: ${newSessionId} (sessions=${sessions.size})`,
174
+ );
175
+ },
160
176
  });
161
177
 
162
178
  transport.onmessage = (message) => {
@@ -164,10 +180,14 @@ const registerMcpRoutes = ({
164
180
  };
165
181
 
166
182
  transport.onclose = () => {
167
- if (activeTransport === transport) {
168
- activeTransport = null;
169
- console.log("[mcp] Transport closed");
183
+ for (const [id, t] of sessions) {
184
+ if (t === transport) {
185
+ sessions.delete(id);
186
+ break;
187
+ }
170
188
  }
189
+ if (activeTransport === transport) activeTransport = null;
190
+ console.log(`[mcp] Transport closed (sessions=${sessions.size})`);
171
191
  };
172
192
 
173
193
  transport.onerror = (err) => {
@@ -176,27 +196,17 @@ const registerMcpRoutes = ({
176
196
 
177
197
  await transport.start();
178
198
 
179
- // Set activeTransport BEFORE handleRequest so the onMcpMessage
180
- // callback can forward the child's response to this transport.
181
- const prev = activeTransport;
182
- activeTransport = transport;
183
-
184
199
  try {
185
200
  await transport.handleRequest(req, res, req.body);
186
201
  } catch (err) {
187
- console.error("[mcp] handleRequest error (new session):", err?.message);
202
+ console.error(
203
+ "[mcp] handleRequest error (new session):",
204
+ err?.message,
205
+ );
188
206
  if (!res.headersSent) {
189
207
  res.status(500).json({ error: "Failed to initialize MCP session" });
190
208
  }
191
209
  }
192
-
193
- if (transport.sessionId) {
194
- if (prev) prev.close().catch(() => {});
195
- console.log(`[mcp] Session established: ${transport.sessionId}`);
196
- } else {
197
- // Session was not established — roll back
198
- if (activeTransport === transport) activeTransport = prev;
199
- }
200
210
  return;
201
211
  }
202
212
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.0-beta.0",
3
+ "version": "0.9.0-beta.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },