@ateam-ai/mcp 0.4.25 → 0.4.26

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.
Files changed (3) hide show
  1. package/package.json +3 -2
  2. package/src/api.js +29 -0
  3. package/src/http.js +29 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "mcpName": "io.github.ariekogan/ateam-mcp",
5
5
  "description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
6
6
  "type": "module",
@@ -12,7 +12,8 @@
12
12
  "start": "node src/index.js",
13
13
  "start:http": "node src/index.js --http",
14
14
  "dev": "node --watch src/index.js",
15
- "dev:http": "node --watch src/index.js --http"
15
+ "dev:http": "node --watch src/index.js --http",
16
+ "test": "node test/session-isolation.test.mjs"
16
17
  },
17
18
  "keywords": [
18
19
  "mcp",
package/src/api.js CHANGED
@@ -211,6 +211,35 @@ export function bindSessionBearer(sessionId, bearerToken) {
211
211
  console.log(`[Auth] Bearer bound for session ${sessionId}`);
212
212
  }
213
213
 
214
+ /**
215
+ * The bearer a session is bound to, or null if the session was never
216
+ * bearer-authenticated (e.g. a no-bearer ateam_auth flow). Used by the HTTP
217
+ * transport to enforce that a bearer-bound session can only be reused by a
218
+ * request presenting the SAME validated bearer — a client-supplied session-id
219
+ * alone must never grant access to another client's credentials.
220
+ */
221
+ export function getSessionBearer(sessionId) {
222
+ return sessionBearers.get(sessionId) || null;
223
+ }
224
+
225
+ /**
226
+ * May a request presenting `presentedToken` (its validated bearer, or
227
+ * null/undefined if none) reuse a session whose bound bearer is `boundBearer`?
228
+ *
229
+ * - No bound bearer → the session was never bearer-authenticated (a no-bearer
230
+ * ateam_auth flow); nothing to match against, allow (unchanged behavior).
231
+ * - Bound bearer → the request MUST present the exact same validated bearer.
232
+ * A missing or different bearer is denied — so a client that knows another
233
+ * client's (non-secret, logged/echoed) session-id cannot be served that
234
+ * client's credentials by sending the id with no/other Authorization.
235
+ *
236
+ * Pure + exported for unit testing.
237
+ */
238
+ export function bearerOwnershipOk(boundBearer, presentedToken) {
239
+ if (!boundBearer) return true;
240
+ return !!presentedToken && presentedToken === boundBearer;
241
+ }
242
+
214
243
  /** Store ateam_auth override for this user (by bearer). Called from tools.js. */
215
244
  export function setAuthOverride(sessionId, { tenant, apiKey, apiUrl }) {
216
245
  const bearer = sessionBearers.get(sessionId);
package/src/http.js CHANGED
@@ -26,7 +26,7 @@ import { createServer } from "./server.js";
26
26
  import {
27
27
  clearSession, setSessionCredentials, parseApiKey,
28
28
  startSessionSweeper, getSessionStats, sweepStaleSessions,
29
- bindSessionBearer, getAuthOverride,
29
+ bindSessionBearer, getAuthOverride, getSessionBearer, bearerOwnershipOk,
30
30
  } from "./api.js";
31
31
  import { mountOAuth } from "./oauth.js";
32
32
  import { connectGithubPage } from "./pages.js";
@@ -222,9 +222,10 @@ export function startHttpServer(port = 3100) {
222
222
  });
223
223
  });
224
224
 
225
- // ─── Get API Key — redirect to Skill Builder with auto-open ──
225
+ // ─── Get API Key — redirect to the main web app's Tenant Admin →
226
+ // Tokens & Keys (the key now lives in the main UI, not the builder). ──
226
227
  app.get("/get-api-key", (_req, res) => {
227
- res.redirect("https://app.ateam-ai.com/builder/?show=api-key");
228
+ res.redirect("https://app.ateam-ai.com/?admin=tokens");
228
229
  });
229
230
 
230
231
  // ─── Connect GitHub — user-facing guide an agent links to on
@@ -235,8 +236,31 @@ export function startHttpServer(port = 3100) {
235
236
 
236
237
  // ─── MCP POST — handle tool calls + initialize ───────────────
237
238
  // Mounted at both "/" and "/mcp" for Claude.ai compatibility
239
+ // SECURITY (multi-client isolation): a session-id is client-supplied and
240
+ // non-secret (logged + echoed in the mcp-session-id response header). If a
241
+ // session was authenticated with a Bearer, ONLY a request presenting that SAME
242
+ // validated Bearer may reuse it — for POST (tool calls), GET (SSE stream) and
243
+ // DELETE (terminate). Without this, a client could send another client's
244
+ // session-id on the optional-auth /mcp path with no/other Authorization and be
245
+ // served that client's tenant + api key (or read its stream / kill its
246
+ // session). A session with no bound bearer (the no-bearer ateam_auth flow) has
247
+ // nothing to match against, so this is a no-op there.
248
+ const denySessionReuse = (req, res, sessionId) => {
249
+ if (sessionId && !bearerOwnershipOk(getSessionBearer(sessionId), req.auth?.token)) {
250
+ console.warn(`[Auth] DENY session reuse: bearer mismatch for session ${sessionId} (presented=${req.auth?.token ? "other-bearer" : "none"})`);
251
+ res.status(401).json({
252
+ jsonrpc: "2.0",
253
+ error: { code: -32001, message: "Unauthorized: this session belongs to a different credential. Re-initialize with your own Authorization." },
254
+ id: req.body?.id ?? null,
255
+ });
256
+ return true;
257
+ }
258
+ return false;
259
+ };
260
+
238
261
  const mcpPost = async (req, res) => {
239
262
  const sessionId = req.headers["mcp-session-id"];
263
+ if (denySessionReuse(req, res, sessionId)) return;
240
264
 
241
265
  try {
242
266
  let transport;
@@ -345,6 +369,7 @@ export function startHttpServer(port = 3100) {
345
369
  res.json({ ok: true, service: "ateam-mcp", transport: "http" });
346
370
  return;
347
371
  }
372
+ if (denySessionReuse(req, res, sessionId)) return;
348
373
  await transports[sessionId].handleRequest(req, res);
349
374
  };
350
375
 
@@ -355,6 +380,7 @@ export function startHttpServer(port = 3100) {
355
380
  res.status(400).send("Invalid or missing session ID");
356
381
  return;
357
382
  }
383
+ if (denySessionReuse(req, res, sessionId)) return;
358
384
  await transports[sessionId].handleRequest(req, res);
359
385
  };
360
386