@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.
- package/package.json +3 -2
- package/src/api.js +29 -0
- 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.
|
|
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
|
|
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
|
|
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
|
|