@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",
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
- // Per-session credential store (sessionId { tenant, apiKey })
18
- const sessions = new Map();
20
+ // Session TTL sessions idle longer than this are swept
21
+ const SESSION_TTL = 60 * 60 * 1000; // 60 minutes
19
22
 
20
- // Per-tenant credential fallback for MCP clients that don't persist sessions
21
- // (e.g., ChatGPT's bridge creates a new session per tool call).
22
- // Keyed by tenant to prevent cross-user credential leaks in shared MCP servers.
23
- const tenantFallbacks = new Map(); // tenant → { tenant, apiKey, createdAt }
24
- const FALLBACK_TTL = 60 * 60 * 1000; // 60 minutes
23
+ // Sweep intervalhow 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 creds = { tenant: resolvedTenant || "main", apiKey };
53
- sessions.set(sessionId, creds);
54
-
55
- // Update per-tenant fallback — only sessions for the SAME tenant will inherit this
56
- tenantFallbacks.set(creds.tenant, { ...creds, createdAt: Date.now() });
57
- console.log(`[Auth] Credentials set for session ${sessionId}, tenant fallback updated (tenant: ${creds.tenant})`);
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 mutating operations — env vars alone are not sufficient
114
- * to deploy, update, or delete solutions.
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 { clearSession, setSessionCredentials, parseApiKey } from "./api.js";
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({ ok: true, service: "ateam-mcp", transport: "http" });
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
- // Graceful shutdown
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
  };