@ateam-ai/mcp 0.2.1 → 0.2.2
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 +1 -1
- package/src/api.js +95 -35
- package/src/http.js +15 -3
- package/src/tools.js +22 -1
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 1. Per-session override (set via ateam_auth tool — used by HTTP transport)
|
|
6
6
|
* 2. Environment variables (ADAS_API_KEY, ADAS_TENANT — used by stdio transport)
|
|
7
7
|
* 3. Defaults (no key, tenant "main")
|
|
8
|
+
*
|
|
9
|
+
* Sessions also track activity timestamps and optional context (active solution,
|
|
10
|
+
* last skill) to support TTL-based cleanup and smarter UX.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
const BASE_URL = process.env.ADAS_API_URL || "https://api.ateam-ai.com";
|
|
@@ -14,14 +17,15 @@ const ENV_API_KEY = process.env.ADAS_API_KEY || "";
|
|
|
14
17
|
// Request timeout (30 seconds)
|
|
15
18
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
16
19
|
|
|
17
|
-
//
|
|
18
|
-
const
|
|
20
|
+
// Session TTL — sessions idle longer than this are swept
|
|
21
|
+
const SESSION_TTL = 60 * 60 * 1000; // 60 minutes
|
|
19
22
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Sweep interval — how often we check for stale sessions
|
|
24
|
+
const SWEEP_INTERVAL = 5 * 60 * 1000; // every 5 minutes
|
|
25
|
+
|
|
26
|
+
// Per-session store (sessionId → { tenant, apiKey, lastActivity, context })
|
|
27
|
+
// context: { activeSolutionId, lastSkillId, lastToolName }
|
|
28
|
+
const sessions = new Map();
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Parse a tenant-embedded API key.
|
|
@@ -41,7 +45,6 @@ export function parseApiKey(key) {
|
|
|
41
45
|
/**
|
|
42
46
|
* Set credentials for a session (called by ateam_auth tool).
|
|
43
47
|
* If tenant is not provided, it's auto-extracted from the key.
|
|
44
|
-
* Also updates the global fallback so new sessions inherit credentials.
|
|
45
48
|
*/
|
|
46
49
|
export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
47
50
|
let resolvedTenant = tenant;
|
|
@@ -49,12 +52,14 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
|
49
52
|
const parsed = parseApiKey(apiKey);
|
|
50
53
|
if (parsed.tenant) resolvedTenant = parsed.tenant;
|
|
51
54
|
}
|
|
52
|
-
const
|
|
53
|
-
sessions.set(sessionId,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const existing = sessions.get(sessionId);
|
|
56
|
+
sessions.set(sessionId, {
|
|
57
|
+
tenant: resolvedTenant || "main",
|
|
58
|
+
apiKey,
|
|
59
|
+
lastActivity: Date.now(),
|
|
60
|
+
context: existing?.context || {},
|
|
61
|
+
});
|
|
62
|
+
console.log(`[Auth] Credentials set for session ${sessionId} (tenant: ${resolvedTenant || "main"})`);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
@@ -62,10 +67,6 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
|
62
67
|
* Resolution order:
|
|
63
68
|
* 1. Per-session (from ateam_auth or seedCredentials)
|
|
64
69
|
* 2. Environment variables (ADAS_API_KEY, ADAS_TENANT)
|
|
65
|
-
*
|
|
66
|
-
* Note: tenantFallbacks are NOT used in getCredentials() to prevent
|
|
67
|
-
* cross-user credential leaks. They are only used in seedFromFallback()
|
|
68
|
-
* which requires explicit tenant matching.
|
|
69
70
|
*/
|
|
70
71
|
export function getCredentials(sessionId) {
|
|
71
72
|
// 1. Per-session credentials
|
|
@@ -84,21 +85,6 @@ export function getCredentials(sessionId) {
|
|
|
84
85
|
return { tenant: tenant || "main", apiKey };
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
-
* Seed a session's credentials from a matching tenant fallback.
|
|
89
|
-
* Called by HTTP transport when a new session is created with a known tenant
|
|
90
|
-
* (e.g., from OAuth token). Only inherits from the SAME tenant.
|
|
91
|
-
*/
|
|
92
|
-
export function seedFromFallback(sessionId, tenant) {
|
|
93
|
-
const fallback = tenantFallbacks.get(tenant);
|
|
94
|
-
if (fallback && (Date.now() - fallback.createdAt < FALLBACK_TTL)) {
|
|
95
|
-
sessions.set(sessionId, { tenant: fallback.tenant, apiKey: fallback.apiKey });
|
|
96
|
-
console.log(`[Auth] Seeded session ${sessionId} from tenant fallback (tenant: ${tenant})`);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
88
|
/**
|
|
103
89
|
* Check if a session is authenticated (has an API key from any source).
|
|
104
90
|
*/
|
|
@@ -110,14 +96,40 @@ export function isAuthenticated(sessionId) {
|
|
|
110
96
|
/**
|
|
111
97
|
* Check if a session has been explicitly authenticated via ateam_auth.
|
|
112
98
|
* This checks ONLY per-session credentials, ignoring env vars.
|
|
113
|
-
* Used to gate
|
|
114
|
-
* to deploy, update, or
|
|
99
|
+
* Used to gate tenant-aware operations — env vars alone are not sufficient
|
|
100
|
+
* to deploy, update, or read solutions.
|
|
115
101
|
*/
|
|
116
102
|
export function isExplicitlyAuthenticated(sessionId) {
|
|
117
103
|
if (!sessionId) return false;
|
|
118
104
|
return sessions.has(sessionId);
|
|
119
105
|
}
|
|
120
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Record activity on a session — called on every tool call.
|
|
109
|
+
* Keeps the session alive and updates context for smarter UX.
|
|
110
|
+
*/
|
|
111
|
+
export function touchSession(sessionId, { toolName, solutionId, skillId } = {}) {
|
|
112
|
+
const session = sessions.get(sessionId);
|
|
113
|
+
if (!session) return;
|
|
114
|
+
|
|
115
|
+
session.lastActivity = Date.now();
|
|
116
|
+
|
|
117
|
+
// Update context — track what the user is working on
|
|
118
|
+
if (toolName) session.context.lastToolName = toolName;
|
|
119
|
+
if (solutionId) session.context.activeSolutionId = solutionId;
|
|
120
|
+
if (skillId) session.context.lastSkillId = skillId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get session context — what the user has been working on.
|
|
125
|
+
* Returns {} if no session or no context.
|
|
126
|
+
*/
|
|
127
|
+
export function getSessionContext(sessionId) {
|
|
128
|
+
const session = sessions.get(sessionId);
|
|
129
|
+
if (!session) return {};
|
|
130
|
+
return { ...session.context };
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
/**
|
|
122
134
|
* Remove session credentials (on disconnect).
|
|
123
135
|
*/
|
|
@@ -125,6 +137,54 @@ export function clearSession(sessionId) {
|
|
|
125
137
|
sessions.delete(sessionId);
|
|
126
138
|
}
|
|
127
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Sweep expired sessions — removes sessions idle longer than SESSION_TTL.
|
|
142
|
+
* Returns the number of sessions removed.
|
|
143
|
+
*/
|
|
144
|
+
export function sweepStaleSessions() {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
let swept = 0;
|
|
147
|
+
for (const [sid, session] of sessions) {
|
|
148
|
+
if (now - session.lastActivity > SESSION_TTL) {
|
|
149
|
+
sessions.delete(sid);
|
|
150
|
+
swept++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (swept > 0) {
|
|
154
|
+
console.log(`[Session] Swept ${swept} stale session(s). ${sessions.size} active.`);
|
|
155
|
+
}
|
|
156
|
+
return swept;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start the periodic session sweep timer.
|
|
161
|
+
* Called once from HTTP transport on startup.
|
|
162
|
+
*/
|
|
163
|
+
export function startSessionSweeper() {
|
|
164
|
+
const timer = setInterval(sweepStaleSessions, SWEEP_INTERVAL);
|
|
165
|
+
timer.unref(); // don't prevent process exit
|
|
166
|
+
console.log(`[Session] Sweep timer started (interval: ${SWEEP_INTERVAL / 1000}s, TTL: ${SESSION_TTL / 1000}s)`);
|
|
167
|
+
return timer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get session stats — for health checks and debugging.
|
|
172
|
+
*/
|
|
173
|
+
export function getSessionStats() {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
let oldest = Infinity;
|
|
176
|
+
let newest = 0;
|
|
177
|
+
for (const [, session] of sessions) {
|
|
178
|
+
if (session.lastActivity < oldest) oldest = session.lastActivity;
|
|
179
|
+
if (session.lastActivity > newest) newest = session.lastActivity;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
active: sessions.size,
|
|
183
|
+
oldestAge: sessions.size > 0 ? Math.round((now - oldest) / 1000) : 0,
|
|
184
|
+
newestAge: sessions.size > 0 ? Math.round((now - newest) / 1000) : 0,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
128
188
|
function headers(sessionId) {
|
|
129
189
|
const { tenant, apiKey } = getCredentials(sessionId);
|
|
130
190
|
const h = { "Content-Type": "application/json" };
|
package/src/http.js
CHANGED
|
@@ -23,7 +23,10 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
23
23
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
24
24
|
import express from "express";
|
|
25
25
|
import { createServer } from "./server.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
clearSession, setSessionCredentials, parseApiKey,
|
|
28
|
+
startSessionSweeper, getSessionStats, sweepStaleSessions,
|
|
29
|
+
} from "./api.js";
|
|
27
30
|
import { mountOAuth } from "./oauth.js";
|
|
28
31
|
|
|
29
32
|
// Active sessions
|
|
@@ -169,7 +172,12 @@ export function startHttpServer(port = 3100) {
|
|
|
169
172
|
|
|
170
173
|
// ─── Health check ─────────────────────────────────────────────
|
|
171
174
|
app.get("/health", (_req, res) => {
|
|
172
|
-
res.json({
|
|
175
|
+
res.json({
|
|
176
|
+
ok: true,
|
|
177
|
+
service: "ateam-mcp",
|
|
178
|
+
transport: "http",
|
|
179
|
+
sessions: getSessionStats(),
|
|
180
|
+
});
|
|
173
181
|
});
|
|
174
182
|
|
|
175
183
|
// ─── Get API Key — redirect to Skill Builder with auto-open ──
|
|
@@ -290,8 +298,12 @@ export function startHttpServer(port = 3100) {
|
|
|
290
298
|
console.log(` Health check: http://localhost:${port}/health`);
|
|
291
299
|
});
|
|
292
300
|
|
|
293
|
-
//
|
|
301
|
+
// Start periodic session cleanup (sweeps stale sessions every 5 min)
|
|
302
|
+
startSessionSweeper();
|
|
303
|
+
|
|
304
|
+
// Graceful shutdown — close all transports and clear sessions
|
|
294
305
|
process.on("SIGINT", async () => {
|
|
306
|
+
console.log(`[HTTP] Shutting down — closing ${Object.keys(transports).length} transport(s)...`);
|
|
295
307
|
for (const sid of Object.keys(transports)) {
|
|
296
308
|
try {
|
|
297
309
|
await transports[sid].close();
|
package/src/tools.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import {
|
|
12
12
|
get, post, patch, del,
|
|
13
13
|
setSessionCredentials, isAuthenticated, isExplicitlyAuthenticated,
|
|
14
|
-
getCredentials, parseApiKey,
|
|
14
|
+
getCredentials, parseApiKey, touchSession, getSessionContext,
|
|
15
15
|
} from "./api.js";
|
|
16
16
|
|
|
17
17
|
// ─── Tool definitions ───────────────────────────────────────────────
|
|
@@ -1137,6 +1137,13 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
1137
1137
|
};
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
+
// Track activity + context on every tool call (keeps session alive, records what user is working on)
|
|
1141
|
+
touchSession(sessionId, {
|
|
1142
|
+
toolName: name,
|
|
1143
|
+
solutionId: args?.solution_id,
|
|
1144
|
+
skillId: args?.skill_id,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1140
1147
|
// Check auth for tenant-aware operations — requires explicit ateam_auth call.
|
|
1141
1148
|
// Env vars (ADAS_API_KEY / ADAS_TENANT) are NOT sufficient — they may be
|
|
1142
1149
|
// baked into MCP config and silently target the wrong tenant.
|
|
@@ -1167,6 +1174,20 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
1167
1174
|
|
|
1168
1175
|
try {
|
|
1169
1176
|
const result = await handler(args, sessionId);
|
|
1177
|
+
|
|
1178
|
+
// For ateam_bootstrap, inject session context so the LLM knows what the user was working on
|
|
1179
|
+
if (name === "ateam_bootstrap") {
|
|
1180
|
+
const ctx = getSessionContext(sessionId);
|
|
1181
|
+
if (ctx.activeSolutionId || ctx.lastSkillId) {
|
|
1182
|
+
result.session_context = {
|
|
1183
|
+
_note: "This user has an active session. You can reference their previous work.",
|
|
1184
|
+
active_solution_id: ctx.activeSolutionId || null,
|
|
1185
|
+
last_skill_id: ctx.lastSkillId || null,
|
|
1186
|
+
last_tool_used: ctx.lastToolName || null,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1170
1191
|
return {
|
|
1171
1192
|
content: [{ type: "text", text: formatResult(result, name) }],
|
|
1172
1193
|
};
|