@ateam-ai/mcp 0.2.1 → 0.2.3
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 +261 -44
- package/src/http.js +49 -7
- package/src/tools.js +542 -23
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -5,23 +5,38 @@
|
|
|
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
|
+
const CORE_URL = process.env.ADAS_CORE_URL || ""; // Direct Core access (for tenant list, etc.)
|
|
11
15
|
const ENV_TENANT = process.env.ADAS_TENANT || "";
|
|
12
16
|
const ENV_API_KEY = process.env.ADAS_API_KEY || "";
|
|
13
17
|
|
|
14
|
-
// Request timeout (
|
|
15
|
-
const REQUEST_TIMEOUT_MS =
|
|
18
|
+
// Request timeout (120 seconds — deploys can take 60-90s)
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
21
|
+
// Session TTL — sessions idle longer than this are swept
|
|
22
|
+
const SESSION_TTL = 60 * 60 * 1000; // 60 minutes
|
|
23
|
+
|
|
24
|
+
// Sweep interval — how often we check for stale sessions
|
|
25
|
+
const SWEEP_INTERVAL = 5 * 60 * 1000; // every 5 minutes
|
|
16
26
|
|
|
17
|
-
// Per-session
|
|
27
|
+
// Per-session store (sessionId → { tenant, apiKey, lastActivity, context })
|
|
28
|
+
// context: { activeSolutionId, lastSkillId, lastToolName }
|
|
18
29
|
const sessions = new Map();
|
|
19
30
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// ── Bearer-based auth (persistent across sessions) ──────────────
|
|
32
|
+
// The OAuth bearer token IS the user's API key (oauth.js exchangeAuthorizationCode).
|
|
33
|
+
// Each user has a unique bearer. MCP clients create new sessions per tool call,
|
|
34
|
+
// so we use the bearer as the persistent actor identity.
|
|
35
|
+
//
|
|
36
|
+
// When a user calls ateam_auth to override (e.g., switch tenants), the override
|
|
37
|
+
// is stored per bearer and applied to all future sessions from that user.
|
|
38
|
+
const authOverrides = new Map(); // bearerToken → { tenant, apiKey, updatedAt }
|
|
39
|
+
const sessionBearers = new Map(); // sessionId → bearerToken
|
|
25
40
|
|
|
26
41
|
/**
|
|
27
42
|
* Parse a tenant-embedded API key.
|
|
@@ -39,22 +54,51 @@ export function parseApiKey(key) {
|
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
/**
|
|
42
|
-
* Set credentials for a session
|
|
57
|
+
* Set credentials for a session.
|
|
43
58
|
* If tenant is not provided, it's auto-extracted from the key.
|
|
44
|
-
*
|
|
59
|
+
* Set explicit=true when called from ateam_auth (not from seedCredentials).
|
|
60
|
+
* Set masterKey for cross-tenant master mode (uses shared secret auth).
|
|
45
61
|
*/
|
|
46
|
-
export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
62
|
+
export function setSessionCredentials(sessionId, { tenant, apiKey, apiUrl, explicit = false, masterKey = null }) {
|
|
47
63
|
let resolvedTenant = tenant;
|
|
48
64
|
if (!resolvedTenant && apiKey) {
|
|
49
65
|
const parsed = parseApiKey(apiKey);
|
|
50
66
|
if (parsed.tenant) resolvedTenant = parsed.tenant;
|
|
51
67
|
}
|
|
52
|
-
const
|
|
53
|
-
sessions.set(sessionId,
|
|
68
|
+
const existing = sessions.get(sessionId);
|
|
69
|
+
sessions.set(sessionId, {
|
|
70
|
+
tenant: resolvedTenant || "main",
|
|
71
|
+
apiKey,
|
|
72
|
+
apiUrl: apiUrl || existing?.apiUrl || null,
|
|
73
|
+
authExplicit: explicit || existing?.authExplicit || false,
|
|
74
|
+
masterKey: masterKey || existing?.masterKey || null,
|
|
75
|
+
lastActivity: Date.now(),
|
|
76
|
+
context: existing?.context || {},
|
|
77
|
+
});
|
|
78
|
+
const urlNote = apiUrl ? `, url: ${apiUrl}` : "";
|
|
79
|
+
const masterNote = masterKey ? ", MASTER MODE" : "";
|
|
80
|
+
console.log(`[Auth] Credentials set for session ${sessionId} (tenant: ${resolvedTenant || "main"}${explicit ? ", explicit" : ""}${urlNote}${masterNote})`);
|
|
81
|
+
}
|
|
54
82
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Switch the active tenant for a master-key session (no re-auth needed).
|
|
85
|
+
* Returns true if switched, false if not in master mode.
|
|
86
|
+
*/
|
|
87
|
+
export function switchTenant(sessionId, newTenant) {
|
|
88
|
+
const session = sessions.get(sessionId);
|
|
89
|
+
if (!session?.masterKey) return false;
|
|
90
|
+
session.tenant = newTenant;
|
|
91
|
+
session.lastActivity = Date.now();
|
|
92
|
+
console.log(`[Auth] Master mode tenant switch: ${newTenant} (session ${sessionId})`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a session is in master key mode.
|
|
98
|
+
*/
|
|
99
|
+
export function isMasterMode(sessionId) {
|
|
100
|
+
const session = sessions.get(sessionId);
|
|
101
|
+
return !!(session?.masterKey);
|
|
58
102
|
}
|
|
59
103
|
|
|
60
104
|
/**
|
|
@@ -62,10 +106,6 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
|
62
106
|
* Resolution order:
|
|
63
107
|
* 1. Per-session (from ateam_auth or seedCredentials)
|
|
64
108
|
* 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
109
|
*/
|
|
70
110
|
export function getCredentials(sessionId) {
|
|
71
111
|
// 1. Per-session credentials
|
|
@@ -84,21 +124,6 @@ export function getCredentials(sessionId) {
|
|
|
84
124
|
return { tenant: tenant || "main", apiKey };
|
|
85
125
|
}
|
|
86
126
|
|
|
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
127
|
/**
|
|
103
128
|
* Check if a session is authenticated (has an API key from any source).
|
|
104
129
|
*/
|
|
@@ -109,23 +134,181 @@ export function isAuthenticated(sessionId) {
|
|
|
109
134
|
|
|
110
135
|
/**
|
|
111
136
|
* Check if a session has been explicitly authenticated via ateam_auth.
|
|
112
|
-
*
|
|
113
|
-
* Used to gate
|
|
114
|
-
* to deploy, update, or
|
|
137
|
+
* Checks per-session credentials AND bearer auth overrides.
|
|
138
|
+
* Used to gate tenant-aware operations — env vars alone are not sufficient
|
|
139
|
+
* to deploy, update, or read solutions.
|
|
115
140
|
*/
|
|
116
141
|
export function isExplicitlyAuthenticated(sessionId) {
|
|
117
142
|
if (!sessionId) return false;
|
|
118
|
-
|
|
143
|
+
// Session has credentials AND they came from ateam_auth (not just seedCredentials)
|
|
144
|
+
const session = sessions.get(sessionId);
|
|
145
|
+
if (session?.authExplicit) return true;
|
|
146
|
+
// Bearer has an active auth override from a previous session's ateam_auth
|
|
147
|
+
return hasBearerAuth(sessionId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Record activity on a session — called on every tool call.
|
|
152
|
+
* Keeps the session alive and updates context for smarter UX.
|
|
153
|
+
*/
|
|
154
|
+
export function touchSession(sessionId, { toolName, solutionId, skillId } = {}) {
|
|
155
|
+
const session = sessions.get(sessionId);
|
|
156
|
+
if (!session) return;
|
|
157
|
+
|
|
158
|
+
session.lastActivity = Date.now();
|
|
159
|
+
|
|
160
|
+
// Update context — track what the user is working on
|
|
161
|
+
if (toolName) session.context.lastToolName = toolName;
|
|
162
|
+
if (solutionId) session.context.activeSolutionId = solutionId;
|
|
163
|
+
if (skillId) session.context.lastSkillId = skillId;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get session context — what the user has been working on.
|
|
168
|
+
* Returns {} if no session or no context.
|
|
169
|
+
*/
|
|
170
|
+
export function getSessionContext(sessionId) {
|
|
171
|
+
const session = sessions.get(sessionId);
|
|
172
|
+
if (!session) return {};
|
|
173
|
+
return { ...session.context };
|
|
119
174
|
}
|
|
120
175
|
|
|
121
176
|
/**
|
|
122
177
|
* Remove session credentials (on disconnect).
|
|
123
178
|
*/
|
|
124
179
|
export function clearSession(sessionId) {
|
|
180
|
+
sessionBearers.delete(sessionId);
|
|
125
181
|
sessions.delete(sessionId);
|
|
126
182
|
}
|
|
127
183
|
|
|
184
|
+
// ── Bearer identity functions ──────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/** Bind a session to its OAuth bearer token. Called from seedCredentials. */
|
|
187
|
+
export function bindSessionBearer(sessionId, bearerToken) {
|
|
188
|
+
sessionBearers.set(sessionId, bearerToken);
|
|
189
|
+
console.log(`[Auth] Bearer bound for session ${sessionId} (bearer: ${bearerToken.substring(0, 25)}...)`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Store ateam_auth override for this user (by bearer). Called from tools.js. */
|
|
193
|
+
export function setAuthOverride(sessionId, { tenant, apiKey, apiUrl }) {
|
|
194
|
+
const bearer = sessionBearers.get(sessionId);
|
|
195
|
+
if (!bearer) {
|
|
196
|
+
console.log(`[Auth] WARNING: No bearer bound for session ${sessionId} — override NOT stored. sessionBearers has ${sessionBearers.size} entries.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
authOverrides.set(bearer, { tenant, apiKey, apiUrl: apiUrl || null, updatedAt: Date.now() });
|
|
200
|
+
console.log(`[Auth] Override stored for bearer (tenant: ${tenant}${apiUrl ? ", url: " + apiUrl : ""})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Get ateam_auth override for a bearer token. Returns null if none/expired. */
|
|
204
|
+
export function getAuthOverride(bearerToken) {
|
|
205
|
+
const entry = authOverrides.get(bearerToken);
|
|
206
|
+
if (!entry) return null;
|
|
207
|
+
if (Date.now() - entry.updatedAt > SESSION_TTL) {
|
|
208
|
+
authOverrides.delete(bearerToken);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return { tenant: entry.tenant, apiKey: entry.apiKey, apiUrl: entry.apiUrl || null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get the base URL for a session. Resolution order:
|
|
216
|
+
* 1. Per-session apiUrl (set via ateam_auth url parameter)
|
|
217
|
+
* 2. Bearer auth override apiUrl
|
|
218
|
+
* 3. Environment variable ADAS_API_URL
|
|
219
|
+
* 4. Default: https://api.ateam-ai.com
|
|
220
|
+
*/
|
|
221
|
+
export function getBaseUrl(sessionId) {
|
|
222
|
+
// 1. Per-session
|
|
223
|
+
const session = sessionId ? sessions.get(sessionId) : null;
|
|
224
|
+
if (session?.apiUrl) return session.apiUrl;
|
|
225
|
+
// 2. Bearer override
|
|
226
|
+
if (sessionId) {
|
|
227
|
+
const bearer = sessionBearers.get(sessionId);
|
|
228
|
+
if (bearer) {
|
|
229
|
+
const override = getAuthOverride(bearer);
|
|
230
|
+
if (override?.apiUrl) return override.apiUrl;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// 3/4. Env or default
|
|
234
|
+
return BASE_URL;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Check if a bearer has an active auth override. */
|
|
238
|
+
export function hasBearerAuth(sessionId) {
|
|
239
|
+
const bearer = sessionBearers.get(sessionId);
|
|
240
|
+
return bearer ? authOverrides.has(bearer) : false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sweep expired sessions — removes sessions idle longer than SESSION_TTL.
|
|
245
|
+
* Returns the number of sessions removed.
|
|
246
|
+
*/
|
|
247
|
+
export function sweepStaleSessions() {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
let swept = 0;
|
|
250
|
+
for (const [sid, session] of sessions) {
|
|
251
|
+
if (now - session.lastActivity > SESSION_TTL) {
|
|
252
|
+
sessionBearers.delete(sid);
|
|
253
|
+
sessions.delete(sid);
|
|
254
|
+
swept++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Also sweep expired auth overrides
|
|
258
|
+
let overridesSwept = 0;
|
|
259
|
+
for (const [bearer, entry] of authOverrides) {
|
|
260
|
+
if (now - entry.updatedAt > SESSION_TTL) {
|
|
261
|
+
authOverrides.delete(bearer);
|
|
262
|
+
overridesSwept++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (swept > 0 || overridesSwept > 0) {
|
|
266
|
+
console.log(`[Session] Swept ${swept} session(s), ${overridesSwept} override(s). ${sessions.size} active, ${authOverrides.size} overrides.`);
|
|
267
|
+
}
|
|
268
|
+
return swept;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Start the periodic session sweep timer.
|
|
273
|
+
* Called once from HTTP transport on startup.
|
|
274
|
+
*/
|
|
275
|
+
export function startSessionSweeper() {
|
|
276
|
+
const timer = setInterval(sweepStaleSessions, SWEEP_INTERVAL);
|
|
277
|
+
timer.unref(); // don't prevent process exit
|
|
278
|
+
console.log(`[Session] Sweep timer started (interval: ${SWEEP_INTERVAL / 1000}s, TTL: ${SESSION_TTL / 1000}s)`);
|
|
279
|
+
return timer;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get session stats — for health checks and debugging.
|
|
284
|
+
*/
|
|
285
|
+
export function getSessionStats() {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
let oldest = Infinity;
|
|
288
|
+
let newest = 0;
|
|
289
|
+
for (const [, session] of sessions) {
|
|
290
|
+
if (session.lastActivity < oldest) oldest = session.lastActivity;
|
|
291
|
+
if (session.lastActivity > newest) newest = session.lastActivity;
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
active: sessions.size,
|
|
295
|
+
oldestAge: sessions.size > 0 ? Math.round((now - oldest) / 1000) : 0,
|
|
296
|
+
newestAge: sessions.size > 0 ? Math.round((now - newest) / 1000) : 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
128
300
|
function headers(sessionId) {
|
|
301
|
+
const session = sessionId ? sessions.get(sessionId) : null;
|
|
302
|
+
|
|
303
|
+
// Master mode: use shared secret auth (x-adas-token) instead of API key
|
|
304
|
+
if (session?.masterKey) {
|
|
305
|
+
const h = { "Content-Type": "application/json" };
|
|
306
|
+
h["x-adas-token"] = session.masterKey;
|
|
307
|
+
h["X-ADAS-TENANT"] = session.tenant || "main";
|
|
308
|
+
return h;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Normal mode: API key auth
|
|
129
312
|
const { tenant, apiKey } = getCredentials(sessionId);
|
|
130
313
|
const h = { "Content-Type": "application/json" };
|
|
131
314
|
if (tenant) h["X-ADAS-TENANT"] = tenant;
|
|
@@ -172,6 +355,7 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
172
355
|
const timeoutMs = opts.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
173
356
|
const controller = new AbortController();
|
|
174
357
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
358
|
+
const baseUrl = getBaseUrl(sessionId);
|
|
175
359
|
|
|
176
360
|
try {
|
|
177
361
|
const fetchOpts = {
|
|
@@ -183,7 +367,7 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
183
367
|
fetchOpts.body = JSON.stringify(body);
|
|
184
368
|
}
|
|
185
369
|
|
|
186
|
-
const res = await fetch(`${
|
|
370
|
+
const res = await fetch(`${baseUrl}${path}`, fetchOpts);
|
|
187
371
|
|
|
188
372
|
if (!res.ok) {
|
|
189
373
|
const text = await res.text().catch(() => "");
|
|
@@ -195,18 +379,18 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
195
379
|
if (err.name === "AbortError") {
|
|
196
380
|
throw new Error(
|
|
197
381
|
`A-Team API timeout: ${method} ${path} did not respond within ${timeoutMs / 1000}s.\n` +
|
|
198
|
-
`Hint: The A-Team API at ${
|
|
382
|
+
`Hint: The A-Team API at ${baseUrl} may be down. Check ${baseUrl}/health`
|
|
199
383
|
);
|
|
200
384
|
}
|
|
201
385
|
if (err.cause?.code === "ECONNREFUSED") {
|
|
202
386
|
throw new Error(
|
|
203
|
-
`Cannot connect to A-Team API at ${
|
|
204
|
-
`Hint: The service may be down. Check
|
|
387
|
+
`Cannot connect to A-Team API at ${baseUrl}.\n` +
|
|
388
|
+
`Hint: The service may be down. Check ${baseUrl}/health`
|
|
205
389
|
);
|
|
206
390
|
}
|
|
207
391
|
if (err.cause?.code === "ENOTFOUND") {
|
|
208
392
|
throw new Error(
|
|
209
|
-
`Cannot resolve A-Team API host: ${
|
|
393
|
+
`Cannot resolve A-Team API host: ${baseUrl}.\n` +
|
|
210
394
|
`Hint: Check your internet connection and ADAS_API_URL setting.`
|
|
211
395
|
);
|
|
212
396
|
}
|
|
@@ -231,3 +415,36 @@ export async function patch(path, body, sessionId, opts) {
|
|
|
231
415
|
export async function del(path, sessionId, opts) {
|
|
232
416
|
return request("DELETE", path, undefined, sessionId, opts);
|
|
233
417
|
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* List all active tenants from Core API (requires master key).
|
|
421
|
+
* Calls Core directly (not through Builder) using shared secret auth.
|
|
422
|
+
*/
|
|
423
|
+
export async function listTenants(sessionId) {
|
|
424
|
+
const session = sessionId ? sessions.get(sessionId) : null;
|
|
425
|
+
if (!session?.masterKey) throw new Error("listTenants requires master key auth");
|
|
426
|
+
|
|
427
|
+
// Resolve Core URL: env var > derive from Builder URL > fallback
|
|
428
|
+
const coreUrl = CORE_URL || BASE_URL.replace(/:\d+$/, ":4000");
|
|
429
|
+
const controller = new AbortController();
|
|
430
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const res = await fetch(`${coreUrl}/api/tenants/list`, {
|
|
434
|
+
method: "GET",
|
|
435
|
+
headers: {
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
"x-adas-token": session.masterKey,
|
|
438
|
+
},
|
|
439
|
+
signal: controller.signal,
|
|
440
|
+
});
|
|
441
|
+
if (!res.ok) {
|
|
442
|
+
const text = await res.text().catch(() => "");
|
|
443
|
+
throw new Error(`Core API error: GET /api/tenants/list returned ${res.status} — ${text}`);
|
|
444
|
+
}
|
|
445
|
+
const data = await res.json();
|
|
446
|
+
return data.tenants || [];
|
|
447
|
+
} finally {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
}
|
|
450
|
+
}
|
package/src/http.js
CHANGED
|
@@ -23,7 +23,11 @@ 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
|
+
bindSessionBearer, getAuthOverride,
|
|
30
|
+
} from "./api.js";
|
|
27
31
|
import { mountOAuth } from "./oauth.js";
|
|
28
32
|
|
|
29
33
|
// Active sessions
|
|
@@ -169,7 +173,12 @@ export function startHttpServer(port = 3100) {
|
|
|
169
173
|
|
|
170
174
|
// ─── Health check ─────────────────────────────────────────────
|
|
171
175
|
app.get("/health", (_req, res) => {
|
|
172
|
-
res.json({
|
|
176
|
+
res.json({
|
|
177
|
+
ok: true,
|
|
178
|
+
service: "ateam-mcp",
|
|
179
|
+
transport: "http",
|
|
180
|
+
sessions: getSessionStats(),
|
|
181
|
+
});
|
|
173
182
|
});
|
|
174
183
|
|
|
175
184
|
// ─── Get API Key — redirect to Skill Builder with auto-open ──
|
|
@@ -189,8 +198,13 @@ export function startHttpServer(port = 3100) {
|
|
|
189
198
|
// Reuse existing session — seed credentials if Bearer token present
|
|
190
199
|
transport = transports[sessionId];
|
|
191
200
|
seedCredentials(req, sessionId);
|
|
192
|
-
} else if (
|
|
193
|
-
// New session
|
|
201
|
+
} else if (isInitializeRequest(req.body)) {
|
|
202
|
+
// New session (or stale session after server restart) — create fresh session.
|
|
203
|
+
// Accept initialize requests even if they carry a stale mcp-session-id.
|
|
204
|
+
if (sessionId) {
|
|
205
|
+
console.log(`[HTTP] Stale session ${sessionId} — creating new session (server was restarted)`);
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
const newSessionId = randomUUID();
|
|
195
209
|
|
|
196
210
|
// Seed credentials from OAuth Bearer token before server starts
|
|
@@ -216,6 +230,16 @@ export function startHttpServer(port = 3100) {
|
|
|
216
230
|
await server.connect(transport);
|
|
217
231
|
await transport.handleRequest(req, res, req.body);
|
|
218
232
|
return;
|
|
233
|
+
} else if (sessionId && !transports[sessionId]) {
|
|
234
|
+
// Stale session (non-initialize request) — tell client to re-initialize.
|
|
235
|
+
// This happens after server restarts when the client still has the old session ID.
|
|
236
|
+
console.log(`[HTTP] Stale session ${sessionId} — returning 400 to trigger re-init`);
|
|
237
|
+
res.status(400).json({
|
|
238
|
+
jsonrpc: "2.0",
|
|
239
|
+
error: { code: -32600, message: "Session expired. Please re-initialize." },
|
|
240
|
+
id: req.body?.id || null,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
219
243
|
} else {
|
|
220
244
|
res.status(400).json({
|
|
221
245
|
jsonrpc: "2.0",
|
|
@@ -290,8 +314,12 @@ export function startHttpServer(port = 3100) {
|
|
|
290
314
|
console.log(` Health check: http://localhost:${port}/health`);
|
|
291
315
|
});
|
|
292
316
|
|
|
293
|
-
//
|
|
317
|
+
// Start periodic session cleanup (sweeps stale sessions every 5 min)
|
|
318
|
+
startSessionSweeper();
|
|
319
|
+
|
|
320
|
+
// Graceful shutdown — close all transports and clear sessions
|
|
294
321
|
process.on("SIGINT", async () => {
|
|
322
|
+
console.log(`[HTTP] Shutting down — closing ${Object.keys(transports).length} transport(s)...`);
|
|
295
323
|
for (const sid of Object.keys(transports)) {
|
|
296
324
|
try {
|
|
297
325
|
await transports[sid].close();
|
|
@@ -314,13 +342,27 @@ function getNewestToken() {
|
|
|
314
342
|
}
|
|
315
343
|
|
|
316
344
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
345
|
+
* Seed session credentials from the OAuth bearer token.
|
|
346
|
+
*
|
|
347
|
+
* The bearer IS the user's API key (set during OAuth authorization).
|
|
348
|
+
* If the user previously called ateam_auth to override (e.g., switch tenants),
|
|
349
|
+
* that override is stored per bearer and takes priority here.
|
|
319
350
|
*/
|
|
320
351
|
function seedCredentials(req, sessionId) {
|
|
321
352
|
const token = req.auth?.token;
|
|
322
353
|
if (!token) return;
|
|
323
354
|
|
|
355
|
+
// Track bearer → session (persistent actor identity)
|
|
356
|
+
bindSessionBearer(sessionId, token);
|
|
357
|
+
|
|
358
|
+
// Check for ateam_auth override for this bearer
|
|
359
|
+
const override = getAuthOverride(token);
|
|
360
|
+
if (override) {
|
|
361
|
+
setSessionCredentials(sessionId, { ...override, explicit: true });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Default: use the bearer token itself as credentials
|
|
324
366
|
const parsed = parseApiKey(token);
|
|
325
367
|
if (parsed.isValid) {
|
|
326
368
|
setSessionCredentials(sessionId, { tenant: parsed.tenant, apiKey: token });
|
package/src/tools.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import {
|
|
12
12
|
get, post, patch, del,
|
|
13
13
|
setSessionCredentials, isAuthenticated, isExplicitlyAuthenticated,
|
|
14
|
-
getCredentials, parseApiKey,
|
|
14
|
+
getCredentials, parseApiKey, touchSession, getSessionContext,
|
|
15
|
+
setAuthOverride, switchTenant, isMasterMode, listTenants,
|
|
15
16
|
} from "./api.js";
|
|
16
17
|
|
|
17
18
|
// ─── Tool definitions ───────────────────────────────────────────────
|
|
@@ -35,7 +36,7 @@ export const tools = [
|
|
|
35
36
|
name: "ateam_auth",
|
|
36
37
|
core: true,
|
|
37
38
|
description:
|
|
38
|
-
"Authenticate with A-Team. Required before any tenant-aware operation (reading solutions, deploying, testing, etc.). The user can get their API key at https://mcp.ateam-ai.com/get-api-key. Only global endpoints (spec, examples, validate) work without auth. IMPORTANT: Even if environment variables (ADAS_API_KEY) are configured, you MUST call ateam_auth explicitly — env vars alone are not sufficient.",
|
|
39
|
+
"Authenticate with A-Team. Required before any tenant-aware operation (reading solutions, deploying, testing, etc.). The user can get their API key at https://mcp.ateam-ai.com/get-api-key. Only global endpoints (spec, examples, validate) work without auth. IMPORTANT: Even if environment variables (ADAS_API_KEY) are configured, you MUST call ateam_auth explicitly — env vars alone are not sufficient. For cross-tenant admin operations, use master_key instead of api_key.",
|
|
39
40
|
inputSchema: {
|
|
40
41
|
type: "object",
|
|
41
42
|
properties: {
|
|
@@ -43,12 +44,19 @@ export const tools = [
|
|
|
43
44
|
type: "string",
|
|
44
45
|
description: "Your A-Team API key (e.g., adas_xxxxx)",
|
|
45
46
|
},
|
|
47
|
+
master_key: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Master key for cross-tenant operations. Authenticates across ALL tenants without per-tenant API keys. Requires tenant parameter.",
|
|
50
|
+
},
|
|
46
51
|
tenant: {
|
|
47
52
|
type: "string",
|
|
48
|
-
description: "Tenant name (e.g., dev, main). Optional
|
|
53
|
+
description: "Tenant name (e.g., dev, main). Optional with api_key if format is adas_<tenant>_<hex>. REQUIRED with master_key.",
|
|
54
|
+
},
|
|
55
|
+
url: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Optional API URL override (e.g., https://dev-api.ateam-ai.com). Use this to target a different environment without restarting the MCP server.",
|
|
49
58
|
},
|
|
50
59
|
},
|
|
51
|
-
required: ["api_key"],
|
|
52
60
|
},
|
|
53
61
|
},
|
|
54
62
|
{
|
|
@@ -61,9 +69,9 @@ export const tools = [
|
|
|
61
69
|
properties: {
|
|
62
70
|
topic: {
|
|
63
71
|
type: "string",
|
|
64
|
-
enum: ["overview", "skill", "solution", "enums"],
|
|
72
|
+
enum: ["overview", "skill", "solution", "enums", "connector-multi-user"],
|
|
65
73
|
description:
|
|
66
|
-
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values",
|
|
74
|
+
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide (actor isolation, zod gotcha, complete examples)",
|
|
67
75
|
},
|
|
68
76
|
},
|
|
69
77
|
required: ["topic"],
|
|
@@ -123,6 +131,10 @@ export const tools = [
|
|
|
123
131
|
type: "object",
|
|
124
132
|
description: "Optional: connector source code files. Key = connector id, value = array of {path, content}.",
|
|
125
133
|
},
|
|
134
|
+
github: {
|
|
135
|
+
type: "boolean",
|
|
136
|
+
description: "Optional: if true, pull connector source code from the solution's GitHub repo instead of requiring mcp_store. Use this after the first deploy (which creates the repo). Cannot be used on first deploy.",
|
|
137
|
+
},
|
|
126
138
|
test_message: {
|
|
127
139
|
type: "string",
|
|
128
140
|
description: "Optional: send a test message after deployment to verify the skill works. Returns the full execution result.",
|
|
@@ -164,6 +176,63 @@ export const tools = [
|
|
|
164
176
|
required: ["solution_id", "skill_id", "message"],
|
|
165
177
|
},
|
|
166
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: "ateam_test_pipeline",
|
|
181
|
+
core: true,
|
|
182
|
+
description:
|
|
183
|
+
"Test the decision pipeline (intent detection → planning) for a skill WITHOUT executing tools. Returns intent classification, first planned action, and timing. Use this to debug why a skill classifies intent incorrectly or plans the wrong action.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
solution_id: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "The solution ID",
|
|
190
|
+
},
|
|
191
|
+
skill_id: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "The skill ID to test",
|
|
194
|
+
},
|
|
195
|
+
message: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "The test message to classify and plan for",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ["solution_id", "skill_id", "message"],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "ateam_test_voice",
|
|
205
|
+
core: true,
|
|
206
|
+
description:
|
|
207
|
+
"Simulate a voice conversation with a deployed solution. Runs the full voice pipeline (session → caller verification → prompt → skill dispatch → response) using text instead of audio. Returns each turn with bot response, verification status, tool calls, and entities. Use this to test voice-enabled solutions end-to-end without making a phone call.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
solution_id: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "The solution ID",
|
|
214
|
+
},
|
|
215
|
+
messages: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: { type: "string" },
|
|
218
|
+
description: "Array of user messages to send sequentially (simulates a multi-turn phone conversation)",
|
|
219
|
+
},
|
|
220
|
+
phone_number: {
|
|
221
|
+
type: "string",
|
|
222
|
+
description: "Optional: simulated caller phone number (e.g., '+14155551234'). If the number is in the solution's known phones list, the caller is auto-verified.",
|
|
223
|
+
},
|
|
224
|
+
skill_slug: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "Optional: target a specific skill by slug instead of using voice routing.",
|
|
227
|
+
},
|
|
228
|
+
timeout_ms: {
|
|
229
|
+
type: "number",
|
|
230
|
+
description: "Optional: max wait time per skill execution in milliseconds (default: 60000).",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
required: ["solution_id", "messages"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
167
236
|
{
|
|
168
237
|
name: "ateam_patch",
|
|
169
238
|
core: true,
|
|
@@ -249,6 +318,26 @@ export const tools = [
|
|
|
249
318
|
required: ["solution_id"],
|
|
250
319
|
},
|
|
251
320
|
},
|
|
321
|
+
{
|
|
322
|
+
name: "ateam_delete_connector",
|
|
323
|
+
core: true,
|
|
324
|
+
description:
|
|
325
|
+
"Remove a connector from a deployed solution. Stops and deletes it from A-Team Core, removes references from the solution definition (grants, platform_connectors) and skill definitions (connectors array), and cleans up mcp-store files.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
solution_id: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "The solution ID (e.g. 'smart-home-assistant')",
|
|
332
|
+
},
|
|
333
|
+
connector_id: {
|
|
334
|
+
type: "string",
|
|
335
|
+
description: "The connector ID to remove (e.g. 'device-mock-mcp')",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
required: ["solution_id", "connector_id"],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
252
341
|
|
|
253
342
|
// ═══════════════════════════════════════════════════════════════════
|
|
254
343
|
// ADVANCED TOOLS — hidden from tools/list, still callable by name
|
|
@@ -535,19 +624,19 @@ export const tools = [
|
|
|
535
624
|
},
|
|
536
625
|
{
|
|
537
626
|
name: "ateam_get_connector_source",
|
|
538
|
-
core:
|
|
627
|
+
core: true,
|
|
539
628
|
description:
|
|
540
|
-
|
|
629
|
+
`Read the source code files of a deployed MCP connector. Returns all files (server.js, package.json, etc.) stored in the mcp_store for this connector. Use this BEFORE patching or rewriting a connector — always read the current code first so you can make surgical fixes instead of blind full rewrites.`,
|
|
541
630
|
inputSchema: {
|
|
542
631
|
type: "object",
|
|
543
632
|
properties: {
|
|
544
633
|
solution_id: {
|
|
545
634
|
type: "string",
|
|
546
|
-
description: "The solution ID",
|
|
635
|
+
description: "The solution ID (e.g. 'smart-home-assistant')",
|
|
547
636
|
},
|
|
548
637
|
connector_id: {
|
|
549
638
|
type: "string",
|
|
550
|
-
description: "The connector ID",
|
|
639
|
+
description: "The connector ID to read (e.g. 'home-assistant-mcp')",
|
|
551
640
|
},
|
|
552
641
|
},
|
|
553
642
|
required: ["solution_id", "connector_id"],
|
|
@@ -597,6 +686,165 @@ export const tools = [
|
|
|
597
686
|
required: ["solution_id"],
|
|
598
687
|
},
|
|
599
688
|
},
|
|
689
|
+
|
|
690
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
691
|
+
// GITHUB TOOLS — version control for solutions
|
|
692
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
693
|
+
|
|
694
|
+
{
|
|
695
|
+
name: "ateam_github_push",
|
|
696
|
+
core: true,
|
|
697
|
+
description:
|
|
698
|
+
"Push the current deployed solution to GitHub. Auto-creates the repo on first use. Commits the full bundle (solution + skills + connector source) atomically. Use after ateam_build_and_run to version your solution, or anytime you want to snapshot the current state.",
|
|
699
|
+
inputSchema: {
|
|
700
|
+
type: "object",
|
|
701
|
+
properties: {
|
|
702
|
+
solution_id: {
|
|
703
|
+
type: "string",
|
|
704
|
+
description: "The solution ID (e.g. 'smart-home-assistant')",
|
|
705
|
+
},
|
|
706
|
+
message: {
|
|
707
|
+
type: "string",
|
|
708
|
+
description: "Optional commit message (default: 'Deploy <solution_id>')",
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
required: ["solution_id"],
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: "ateam_github_pull",
|
|
716
|
+
core: true,
|
|
717
|
+
description:
|
|
718
|
+
"Deploy a solution FROM its GitHub repo. Reads .ateam/export.json + connector source from the repo and feeds it into the deploy pipeline. Use this to restore a previous version or deploy from GitHub as the source of truth.",
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: "object",
|
|
721
|
+
properties: {
|
|
722
|
+
solution_id: {
|
|
723
|
+
type: "string",
|
|
724
|
+
description: "The solution ID to pull and deploy from GitHub",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
required: ["solution_id"],
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
name: "ateam_github_status",
|
|
732
|
+
core: true,
|
|
733
|
+
description:
|
|
734
|
+
"Check if a solution has a GitHub repo, its URL, and the latest commit. Use this to verify GitHub integration is working for a solution.",
|
|
735
|
+
inputSchema: {
|
|
736
|
+
type: "object",
|
|
737
|
+
properties: {
|
|
738
|
+
solution_id: {
|
|
739
|
+
type: "string",
|
|
740
|
+
description: "The solution ID",
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
required: ["solution_id"],
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "ateam_github_read",
|
|
748
|
+
core: true,
|
|
749
|
+
description:
|
|
750
|
+
"Read any file from a solution's GitHub repo. Returns the file content. Use this to read connector source code, skill definitions, or any versioned file. Great for reviewing previous versions or understanding what's in the repo.",
|
|
751
|
+
inputSchema: {
|
|
752
|
+
type: "object",
|
|
753
|
+
properties: {
|
|
754
|
+
solution_id: {
|
|
755
|
+
type: "string",
|
|
756
|
+
description: "The solution ID",
|
|
757
|
+
},
|
|
758
|
+
path: {
|
|
759
|
+
type: "string",
|
|
760
|
+
description: "File path in the repo (e.g. 'connectors/home-assistant-mcp/server.js', 'solution.json', 'skills/order-support/skill.json')",
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
required: ["solution_id", "path"],
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "ateam_github_patch",
|
|
768
|
+
core: true,
|
|
769
|
+
description:
|
|
770
|
+
"Edit a specific file in the solution's GitHub repo and commit. Creates the file if it doesn't exist. Use this to make surgical fixes to connector source code, update skill definitions, or add new files directly in the repo.",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: "object",
|
|
773
|
+
properties: {
|
|
774
|
+
solution_id: {
|
|
775
|
+
type: "string",
|
|
776
|
+
description: "The solution ID",
|
|
777
|
+
},
|
|
778
|
+
path: {
|
|
779
|
+
type: "string",
|
|
780
|
+
description: "File path to create/update (e.g. 'connectors/home-assistant-mcp/server.js')",
|
|
781
|
+
},
|
|
782
|
+
content: {
|
|
783
|
+
type: "string",
|
|
784
|
+
description: "The full file content to write",
|
|
785
|
+
},
|
|
786
|
+
message: {
|
|
787
|
+
type: "string",
|
|
788
|
+
description: "Optional commit message (default: 'Update <path>')",
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
required: ["solution_id", "path", "content"],
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "ateam_github_log",
|
|
796
|
+
core: true,
|
|
797
|
+
description:
|
|
798
|
+
"View commit history for a solution's GitHub repo. Shows recent commits with messages, SHAs, timestamps, and links. Use this to see what changes have been made and when.",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: {
|
|
802
|
+
solution_id: {
|
|
803
|
+
type: "string",
|
|
804
|
+
description: "The solution ID",
|
|
805
|
+
},
|
|
806
|
+
limit: {
|
|
807
|
+
type: "number",
|
|
808
|
+
description: "Max commits to return (default: 10)",
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
required: ["solution_id"],
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
816
|
+
// MASTER KEY TOOLS — cross-tenant bulk operations (master key only)
|
|
817
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
818
|
+
|
|
819
|
+
{
|
|
820
|
+
name: "ateam_status_all",
|
|
821
|
+
core: true,
|
|
822
|
+
description:
|
|
823
|
+
"Show GitHub sync status for ALL tenants and solutions in one call. Requires master key authentication. Returns a summary table of every tenant's solutions with their GitHub sync state.",
|
|
824
|
+
inputSchema: {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {},
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "ateam_sync_all",
|
|
831
|
+
core: true,
|
|
832
|
+
description:
|
|
833
|
+
"Sync ALL tenants: push Builder FS → GitHub, then pull GitHub → Core MongoDB. Requires master key authentication. Returns a summary table with results for each tenant/solution.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: {
|
|
837
|
+
push_only: {
|
|
838
|
+
type: "boolean",
|
|
839
|
+
description: "Only push to GitHub (skip pull to Core). Default: false (full sync).",
|
|
840
|
+
},
|
|
841
|
+
pull_only: {
|
|
842
|
+
type: "boolean",
|
|
843
|
+
description: "Only pull from GitHub to Core (skip push). Default: false (full sync).",
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
},
|
|
600
848
|
];
|
|
601
849
|
|
|
602
850
|
/**
|
|
@@ -612,6 +860,7 @@ const SPEC_PATHS = {
|
|
|
612
860
|
skill: "/spec/skill",
|
|
613
861
|
solution: "/spec/solution",
|
|
614
862
|
enums: "/spec/enums",
|
|
863
|
+
"connector-multi-user": "/spec/multi-user-connector",
|
|
615
864
|
};
|
|
616
865
|
|
|
617
866
|
const EXAMPLE_PATHS = {
|
|
@@ -637,17 +886,30 @@ const TENANT_TOOLS = new Set([
|
|
|
637
886
|
"ateam_update",
|
|
638
887
|
"ateam_redeploy",
|
|
639
888
|
"ateam_delete_solution",
|
|
889
|
+
"ateam_delete_connector",
|
|
640
890
|
"ateam_solution_chat",
|
|
641
891
|
// Read operations (tenant-specific data)
|
|
642
892
|
"ateam_list_solutions",
|
|
643
893
|
"ateam_get_solution",
|
|
644
894
|
"ateam_get_execution_logs",
|
|
645
895
|
"ateam_test_skill",
|
|
896
|
+
"ateam_test_pipeline",
|
|
897
|
+
"ateam_test_voice",
|
|
646
898
|
"ateam_test_status",
|
|
647
899
|
"ateam_test_abort",
|
|
648
900
|
"ateam_get_connector_source",
|
|
649
901
|
"ateam_get_metrics",
|
|
650
902
|
"ateam_diff",
|
|
903
|
+
// GitHub operations
|
|
904
|
+
"ateam_github_push",
|
|
905
|
+
"ateam_github_pull",
|
|
906
|
+
"ateam_github_status",
|
|
907
|
+
"ateam_github_read",
|
|
908
|
+
"ateam_github_patch",
|
|
909
|
+
"ateam_github_log",
|
|
910
|
+
// Master key bulk operations
|
|
911
|
+
"ateam_status_all",
|
|
912
|
+
"ateam_sync_all",
|
|
651
913
|
]);
|
|
652
914
|
|
|
653
915
|
/** Small delay helper */
|
|
@@ -676,12 +938,13 @@ const handlers = {
|
|
|
676
938
|
{ name: "Enterprise Compliance Platform", description: "Approval flows, audit logs, policy enforcement" },
|
|
677
939
|
],
|
|
678
940
|
developer_loop: {
|
|
679
|
-
_note: "This is the recommended build loop.
|
|
941
|
+
_note: "This is the recommended build loop. 5 steps from definition to running skill with GitHub version control.",
|
|
680
942
|
steps: [
|
|
681
943
|
{ step: 1, action: "Learn", description: "Get the spec and study examples", tools: ["ateam_get_spec", "ateam_get_examples"] },
|
|
682
|
-
{ step: 2, action: "Build & Run", description: "Define your solution + skills, then validate, deploy, and health-check in one call.
|
|
683
|
-
{ step: 3, action: "
|
|
684
|
-
{ step: 4, action: "Iterate", description: "
|
|
944
|
+
{ step: 2, action: "Build & Run", description: "Define your solution + skills + connector code, then validate, deploy, and health-check in one call. Include mcp_store with connector source code on the first deploy.", tools: ["ateam_build_and_run"] },
|
|
945
|
+
{ step: 3, action: "Version", description: "Every deploy auto-pushes to GitHub. The repo (tenant--solution-id) is the source of truth for connector code.", tools: ["ateam_github_status", "ateam_github_log"] },
|
|
946
|
+
{ step: 4, action: "Iterate", description: "Edit connector code via ateam_github_patch, then redeploy with ateam_build_and_run(github:true). For skill definition changes (intents, tools, policy), use ateam_patch.", tools: ["ateam_github_patch", "ateam_build_and_run", "ateam_patch"] },
|
|
947
|
+
{ step: 5, action: "Test & Debug", description: "Test the decision pipeline or full execution, then diagnose with logs and metrics. For voice-enabled solutions, use ateam_test_voice to simulate phone conversations.", tools: ["ateam_test_pipeline", "ateam_test_skill", "ateam_test_voice", "ateam_get_execution_logs", "ateam_get_metrics"] },
|
|
685
948
|
],
|
|
686
949
|
},
|
|
687
950
|
first_questions: [
|
|
@@ -690,6 +953,27 @@ const handlers = {
|
|
|
690
953
|
{ id: "systems", question: "Which systems should the Team connect to?", type: "multi_select", options: ["slack", "email", "zendesk", "shopify", "jira", "postgres", "custom_api", "none"] },
|
|
691
954
|
{ id: "security", question: "What environment constraints?", type: "enum", options: ["sandbox", "controlled", "regulated"] },
|
|
692
955
|
],
|
|
956
|
+
github_tools: {
|
|
957
|
+
_note: "Version control for solutions. Every deploy auto-pushes to GitHub. The repo is the source of truth for connector code.",
|
|
958
|
+
tools: ["ateam_github_push", "ateam_github_pull", "ateam_github_status", "ateam_github_read", "ateam_github_patch", "ateam_github_log"],
|
|
959
|
+
repo_structure: {
|
|
960
|
+
"solution.json": "Full solution definition",
|
|
961
|
+
"skills/{skill-id}/skill.json": "Individual skill definitions",
|
|
962
|
+
"connectors/{connector-id}/server.js": "Connector MCP server code",
|
|
963
|
+
"connectors/{connector-id}/package.json": "Connector dependencies",
|
|
964
|
+
},
|
|
965
|
+
iteration_workflow: {
|
|
966
|
+
code_changes: "ateam_github_patch (edit connector files) → ateam_build_and_run(github:true) (redeploy from repo)",
|
|
967
|
+
definition_changes: "ateam_patch (edit skill/solution definitions directly in Builder)",
|
|
968
|
+
first_deploy: "Must include mcp_store — this creates the GitHub repo",
|
|
969
|
+
},
|
|
970
|
+
when_to_use_what: {
|
|
971
|
+
ateam_github_patch: "Edit connector source code (server.js, utils, package.json, UI assets)",
|
|
972
|
+
ateam_patch: "Edit skill definitions (intents, tools, policy) or solution definitions (grants, handoffs, routing)",
|
|
973
|
+
"ateam_build_and_run(github:true)": "Redeploy solution pulling latest connector code from GitHub",
|
|
974
|
+
"ateam_build_and_run(mcp_store)": "First deploy or when you want to pass connector code inline",
|
|
975
|
+
},
|
|
976
|
+
},
|
|
693
977
|
advanced_tools: {
|
|
694
978
|
_note: "These tools are available but hidden from the default tool list. Call them by name when you need fine-grained control.",
|
|
695
979
|
debugging: ["ateam_get_execution_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"],
|
|
@@ -728,7 +1012,8 @@ const handlers = {
|
|
|
728
1012
|
always: [
|
|
729
1013
|
"Explain Skill vs Solution vs Connector before building",
|
|
730
1014
|
"Use ateam_build_and_run for the full lifecycle (validates automatically)",
|
|
731
|
-
"Use ateam_patch for
|
|
1015
|
+
"Use ateam_patch for skill/solution definition changes (updates + redeploys automatically)",
|
|
1016
|
+
"Use ateam_github_patch + ateam_build_and_run(github:true) for connector code changes after first deploy",
|
|
732
1017
|
"Study the connector example (ateam_get_examples type='connector') before writing connector code",
|
|
733
1018
|
"Ask discovery questions if goal unclear",
|
|
734
1019
|
],
|
|
@@ -741,21 +1026,52 @@ const handlers = {
|
|
|
741
1026
|
},
|
|
742
1027
|
}),
|
|
743
1028
|
|
|
744
|
-
ateam_auth: async ({ api_key, tenant }, sessionId) => {
|
|
1029
|
+
ateam_auth: async ({ api_key, master_key, tenant, url }, sessionId) => {
|
|
1030
|
+
// Master key mode: cross-tenant auth using shared secret
|
|
1031
|
+
if (master_key) {
|
|
1032
|
+
if (!tenant) {
|
|
1033
|
+
return { ok: false, message: "Master key requires a tenant parameter. Specify which tenant to operate on." };
|
|
1034
|
+
}
|
|
1035
|
+
const apiUrl = url ? url.replace(/\/+$/, "") : undefined;
|
|
1036
|
+
setSessionCredentials(sessionId, { tenant, apiKey: null, apiUrl, explicit: true, masterKey: master_key });
|
|
1037
|
+
// Verify by listing solutions
|
|
1038
|
+
try {
|
|
1039
|
+
const result = await get("/deploy/solutions", sessionId);
|
|
1040
|
+
const urlNote = apiUrl ? ` (via ${apiUrl})` : "";
|
|
1041
|
+
return {
|
|
1042
|
+
ok: true,
|
|
1043
|
+
tenant,
|
|
1044
|
+
masterMode: true,
|
|
1045
|
+
message: `Master key authenticated to tenant "${tenant}"${urlNote}. ${result.solutions?.length || 0} solution(s) found. Use tenant parameter on any tool to switch tenants without re-auth.`,
|
|
1046
|
+
};
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
return { ok: false, tenant, message: `Master key auth failed: ${err.message}` };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Normal API key mode
|
|
1053
|
+
if (!api_key) {
|
|
1054
|
+
return { ok: false, message: "Provide either api_key or master_key." };
|
|
1055
|
+
}
|
|
745
1056
|
// Auto-extract tenant from key if not provided
|
|
746
1057
|
let resolvedTenant = tenant;
|
|
747
1058
|
if (!resolvedTenant) {
|
|
748
1059
|
const parsed = parseApiKey(api_key);
|
|
749
1060
|
resolvedTenant = parsed.tenant || "main";
|
|
750
1061
|
}
|
|
751
|
-
|
|
1062
|
+
// Normalize URL: strip trailing slash
|
|
1063
|
+
const apiUrl = url ? url.replace(/\/+$/, "") : undefined;
|
|
1064
|
+
setSessionCredentials(sessionId, { tenant: resolvedTenant, apiKey: api_key, apiUrl, explicit: true });
|
|
1065
|
+
// Persist override per bearer (survives session changes)
|
|
1066
|
+
setAuthOverride(sessionId, { tenant: resolvedTenant, apiKey: api_key, apiUrl });
|
|
752
1067
|
// Verify the key works by listing solutions
|
|
753
1068
|
try {
|
|
754
1069
|
const result = await get("/deploy/solutions", sessionId);
|
|
1070
|
+
const urlNote = apiUrl ? ` (via ${apiUrl})` : "";
|
|
755
1071
|
return {
|
|
756
1072
|
ok: true,
|
|
757
1073
|
tenant: resolvedTenant,
|
|
758
|
-
message: `Authenticated to tenant "${resolvedTenant}". ${result.solutions?.length || 0} solution(s) found.`,
|
|
1074
|
+
message: `Authenticated to tenant "${resolvedTenant}"${urlNote}. ${result.solutions?.length || 0} solution(s) found.`,
|
|
759
1075
|
};
|
|
760
1076
|
} catch (err) {
|
|
761
1077
|
return {
|
|
@@ -776,13 +1092,49 @@ const handlers = {
|
|
|
776
1092
|
// Validates → Deploys → Health-checks → Optionally tests
|
|
777
1093
|
// One call replaces: validate_solution + deploy_solution + get_solution(health)
|
|
778
1094
|
|
|
779
|
-
ateam_build_and_run: async ({ solution, skills, connectors, mcp_store, test_message, test_skill_id }, sid) => {
|
|
1095
|
+
ateam_build_and_run: async ({ solution, skills, connectors, mcp_store, github, test_message, test_skill_id }, sid) => {
|
|
780
1096
|
const phases = [];
|
|
781
1097
|
|
|
1098
|
+
// Phase 0: GitHub pull (if github:true — pull connector source from repo)
|
|
1099
|
+
let effectiveMcpStore = mcp_store;
|
|
1100
|
+
if (github && !mcp_store) {
|
|
1101
|
+
try {
|
|
1102
|
+
const pullResult = await post(
|
|
1103
|
+
`/deploy/solutions/${solution.id}/github/pull-connectors`,
|
|
1104
|
+
{},
|
|
1105
|
+
sid,
|
|
1106
|
+
{ timeoutMs: 30_000 },
|
|
1107
|
+
);
|
|
1108
|
+
if (!pullResult.ok) {
|
|
1109
|
+
return {
|
|
1110
|
+
ok: false,
|
|
1111
|
+
phase: "github_pull",
|
|
1112
|
+
error: pullResult.error || "Failed to pull connectors from GitHub",
|
|
1113
|
+
hint: pullResult.hint || "Deploy the solution first (with mcp_store) to auto-create the GitHub repo.",
|
|
1114
|
+
message: "Cannot pull connector code from GitHub. The repo may not exist yet — deploy with mcp_store first.",
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
effectiveMcpStore = pullResult.mcp_store;
|
|
1118
|
+
phases.push({
|
|
1119
|
+
phase: "github_pull",
|
|
1120
|
+
status: "done",
|
|
1121
|
+
connectors_found: pullResult.connectors_found || 0,
|
|
1122
|
+
files_loaded: pullResult.files_loaded || 0,
|
|
1123
|
+
});
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
return {
|
|
1126
|
+
ok: false,
|
|
1127
|
+
phase: "github_pull",
|
|
1128
|
+
error: err.message,
|
|
1129
|
+
message: "Failed to pull connector code from GitHub. The repo may not exist yet — deploy with mcp_store first.",
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
782
1134
|
// Phase 1: Validate
|
|
783
1135
|
let validation;
|
|
784
1136
|
try {
|
|
785
|
-
validation = await post("/validate/solution", { solution, skills }, sid);
|
|
1137
|
+
validation = await post("/validate/solution", { solution, skills, connectors, mcp_store: effectiveMcpStore }, sid, { timeoutMs: 120_000 });
|
|
786
1138
|
phases.push({ phase: "validate", status: "done" });
|
|
787
1139
|
} catch (err) {
|
|
788
1140
|
return {
|
|
@@ -808,7 +1160,7 @@ const handlers = {
|
|
|
808
1160
|
// Phase 2: Deploy
|
|
809
1161
|
let deploy;
|
|
810
1162
|
try {
|
|
811
|
-
deploy = await post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid);
|
|
1163
|
+
deploy = await post("/deploy/solution", { solution, skills, connectors, mcp_store: effectiveMcpStore }, sid, { timeoutMs: 300_000 });
|
|
812
1164
|
phases.push({ phase: "deploy", status: deploy.ok ? "done" : "failed" });
|
|
813
1165
|
} catch (err) {
|
|
814
1166
|
return {
|
|
@@ -863,6 +1215,25 @@ const handlers = {
|
|
|
863
1215
|
}
|
|
864
1216
|
}
|
|
865
1217
|
|
|
1218
|
+
// Phase 5: GitHub push (auto — non-blocking, failures don't fail the deploy)
|
|
1219
|
+
let github_result;
|
|
1220
|
+
try {
|
|
1221
|
+
github_result = await post(
|
|
1222
|
+
`/deploy/solutions/${solution.id}/github/push`,
|
|
1223
|
+
{ message: `Deploy: ${solution.name || solution.id}` },
|
|
1224
|
+
sid,
|
|
1225
|
+
{ timeoutMs: 60_000 },
|
|
1226
|
+
);
|
|
1227
|
+
phases.push({
|
|
1228
|
+
phase: "github",
|
|
1229
|
+
status: github_result.skipped ? "skipped" : "done",
|
|
1230
|
+
...(github_result.repo_url && { repo_url: github_result.repo_url }),
|
|
1231
|
+
});
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
github_result = { error: err.message };
|
|
1234
|
+
phases.push({ phase: "github", status: "error", error: err.message });
|
|
1235
|
+
}
|
|
1236
|
+
|
|
866
1237
|
return {
|
|
867
1238
|
ok: true,
|
|
868
1239
|
solution_id: solution.id,
|
|
@@ -875,6 +1246,7 @@ const handlers = {
|
|
|
875
1246
|
},
|
|
876
1247
|
health,
|
|
877
1248
|
...(test_result && { test_result }),
|
|
1249
|
+
...(github_result && !github_result.error && !github_result.skipped && { github: github_result }),
|
|
878
1250
|
...(validation.warnings?.length > 0 && { validation_warnings: validation.warnings }),
|
|
879
1251
|
};
|
|
880
1252
|
},
|
|
@@ -956,8 +1328,8 @@ const handlers = {
|
|
|
956
1328
|
|
|
957
1329
|
ateam_validate_skill: async ({ skill }, sid) => post("/validate/skill", { skill }, sid),
|
|
958
1330
|
|
|
959
|
-
ateam_validate_solution: async ({ solution, skills }, sid) =>
|
|
960
|
-
post("/validate/solution", { solution, skills }, sid),
|
|
1331
|
+
ateam_validate_solution: async ({ solution, skills, connectors, mcp_store }, sid) =>
|
|
1332
|
+
post("/validate/solution", { solution, skills, connectors, mcp_store }, sid),
|
|
961
1333
|
|
|
962
1334
|
ateam_deploy_solution: async ({ solution, skills, connectors, mcp_store }, sid) =>
|
|
963
1335
|
post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid),
|
|
@@ -1042,6 +1414,20 @@ const handlers = {
|
|
|
1042
1414
|
return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs });
|
|
1043
1415
|
},
|
|
1044
1416
|
|
|
1417
|
+
ateam_test_pipeline: async ({ solution_id, skill_id, message }, sid) =>
|
|
1418
|
+
post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test-pipeline`, { message }, sid, { timeoutMs: 30_000 }),
|
|
1419
|
+
|
|
1420
|
+
ateam_test_voice: async ({ solution_id, messages, phone_number, skill_slug, timeout_ms }, sid) => {
|
|
1421
|
+
const body = { messages };
|
|
1422
|
+
if (phone_number) body.phone_number = phone_number;
|
|
1423
|
+
if (skill_slug) body.skill_slug = skill_slug;
|
|
1424
|
+
if (timeout_ms) body.timeout_ms = timeout_ms;
|
|
1425
|
+
// Timeout scales with message count — each turn may invoke skills
|
|
1426
|
+
const perTurnMs = timeout_ms || 60_000;
|
|
1427
|
+
const timeoutTotal = Math.min(perTurnMs * messages.length + 30_000, 600_000);
|
|
1428
|
+
return post(`/deploy/voice-test`, body, sid, { timeoutMs: timeoutTotal });
|
|
1429
|
+
},
|
|
1430
|
+
|
|
1045
1431
|
ateam_test_status: async ({ solution_id, skill_id, job_id }, sid) =>
|
|
1046
1432
|
get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid),
|
|
1047
1433
|
|
|
@@ -1064,8 +1450,115 @@ const handlers = {
|
|
|
1064
1450
|
return get(`/deploy/solutions/${solution_id}/diff${qs}`, sid);
|
|
1065
1451
|
},
|
|
1066
1452
|
|
|
1453
|
+
// ─── GitHub tools ──────────────────────────────────────────────────
|
|
1454
|
+
|
|
1455
|
+
ateam_github_push: async ({ solution_id, message }, sid) =>
|
|
1456
|
+
post(`/deploy/solutions/${solution_id}/github/push`, { message }, sid, { timeoutMs: 60_000 }),
|
|
1457
|
+
|
|
1458
|
+
ateam_github_pull: async ({ solution_id }, sid) =>
|
|
1459
|
+
post(`/deploy/solutions/${solution_id}/github/pull`, {}, sid, { timeoutMs: 300_000 }),
|
|
1460
|
+
|
|
1461
|
+
ateam_github_status: async ({ solution_id }, sid) =>
|
|
1462
|
+
get(`/deploy/solutions/${solution_id}/github/status`, sid),
|
|
1463
|
+
|
|
1464
|
+
ateam_github_read: async ({ solution_id, path: filePath }, sid) =>
|
|
1465
|
+
get(`/deploy/solutions/${solution_id}/github/read?path=${encodeURIComponent(filePath)}`, sid),
|
|
1466
|
+
|
|
1467
|
+
ateam_github_patch: async ({ solution_id, path: filePath, content, message }, sid) =>
|
|
1468
|
+
post(`/deploy/solutions/${solution_id}/github/patch`, { path: filePath, content, message }, sid),
|
|
1469
|
+
|
|
1470
|
+
ateam_github_log: async ({ solution_id, limit }, sid) => {
|
|
1471
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1472
|
+
return get(`/deploy/solutions/${solution_id}/github/log${qs}`, sid);
|
|
1473
|
+
},
|
|
1474
|
+
|
|
1067
1475
|
ateam_delete_solution: async ({ solution_id }, sid) =>
|
|
1068
1476
|
del(`/deploy/solutions/${solution_id}`, sid),
|
|
1477
|
+
|
|
1478
|
+
ateam_delete_connector: async ({ solution_id, connector_id }, sid) =>
|
|
1479
|
+
del(`/deploy/solutions/${solution_id}/connectors/${connector_id}`, sid),
|
|
1480
|
+
|
|
1481
|
+
// ─── Master Key Bulk Tools ───────────────────────────────────────────
|
|
1482
|
+
|
|
1483
|
+
ateam_status_all: async (_args, sid) => {
|
|
1484
|
+
if (!isMasterMode(sid)) {
|
|
1485
|
+
return { ok: false, message: "Master key required. Call ateam_auth(master_key: \"<key>\", tenant: \"<any>\") first." };
|
|
1486
|
+
}
|
|
1487
|
+
const tenants = await listTenants(sid);
|
|
1488
|
+
const results = [];
|
|
1489
|
+
for (const t of tenants) {
|
|
1490
|
+
switchTenant(sid, t.id);
|
|
1491
|
+
try {
|
|
1492
|
+
const { solutions } = await get("/deploy/solutions", sid);
|
|
1493
|
+
for (const sol of (solutions || [])) {
|
|
1494
|
+
let ghStatus = null;
|
|
1495
|
+
try {
|
|
1496
|
+
ghStatus = await get(`/deploy/solutions/${sol.id}/github/status`, sid);
|
|
1497
|
+
} catch { /* no github config */ }
|
|
1498
|
+
results.push({
|
|
1499
|
+
tenant: t.id,
|
|
1500
|
+
solution: sol.id,
|
|
1501
|
+
name: sol.name || sol.id,
|
|
1502
|
+
github: ghStatus ? {
|
|
1503
|
+
repo: ghStatus.repo || ghStatus.repoUrl,
|
|
1504
|
+
lastCommit: ghStatus.lastCommit?.message?.slice(0, 60),
|
|
1505
|
+
lastPush: ghStatus.lastCommit?.date,
|
|
1506
|
+
branch: ghStatus.branch,
|
|
1507
|
+
} : "not configured",
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
results.push({ tenant: t.id, error: err.message });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return { ok: true, tenants: tenants.length, solutions: results.length, results };
|
|
1515
|
+
},
|
|
1516
|
+
|
|
1517
|
+
ateam_sync_all: async ({ push_only, pull_only }, sid) => {
|
|
1518
|
+
if (!isMasterMode(sid)) {
|
|
1519
|
+
return { ok: false, message: "Master key required. Call ateam_auth(master_key: \"<key>\", tenant: \"<any>\") first." };
|
|
1520
|
+
}
|
|
1521
|
+
const tenants = await listTenants(sid);
|
|
1522
|
+
const results = [];
|
|
1523
|
+
for (const t of tenants) {
|
|
1524
|
+
switchTenant(sid, t.id);
|
|
1525
|
+
try {
|
|
1526
|
+
const { solutions } = await get("/deploy/solutions", sid);
|
|
1527
|
+
for (const sol of (solutions || [])) {
|
|
1528
|
+
const entry = { tenant: t.id, solution: sol.id, name: sol.name || sol.id };
|
|
1529
|
+
// Push: Builder FS → GitHub
|
|
1530
|
+
if (!pull_only) {
|
|
1531
|
+
try {
|
|
1532
|
+
const pushResult = await post(`/deploy/solutions/${sol.id}/github/push`, {}, sid);
|
|
1533
|
+
entry.push = { ok: true, commit: pushResult.commitSha?.slice(0, 8), files: pushResult.filesCommitted };
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
entry.push = { ok: false, error: err.message.slice(0, 100) };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// Pull: GitHub → Core MongoDB
|
|
1539
|
+
if (!push_only) {
|
|
1540
|
+
try {
|
|
1541
|
+
const pullResult = await post(`/deploy/solutions/${sol.id}/github/pull`, {}, sid);
|
|
1542
|
+
entry.pull = { ok: true, skills: pullResult.skills?.length, connectors: pullResult.connectors?.length };
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
entry.pull = { ok: false, error: err.message.slice(0, 100) };
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
results.push(entry);
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
results.push({ tenant: t.id, error: err.message });
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const pushCount = results.filter(r => r.push?.ok).length;
|
|
1554
|
+
const pullCount = results.filter(r => r.pull?.ok).length;
|
|
1555
|
+
const errors = results.filter(r => r.error || r.push?.ok === false || r.pull?.ok === false).length;
|
|
1556
|
+
return {
|
|
1557
|
+
ok: errors === 0,
|
|
1558
|
+
summary: `Synced ${tenants.length} tenant(s), ${results.length} solution(s). Push: ${pushCount} ok. Pull: ${pullCount} ok. Errors: ${errors}.`,
|
|
1559
|
+
results,
|
|
1560
|
+
};
|
|
1561
|
+
},
|
|
1069
1562
|
};
|
|
1070
1563
|
|
|
1071
1564
|
// ─── Response formatting ────────────────────────────────────────────
|
|
@@ -1137,6 +1630,13 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
1137
1630
|
};
|
|
1138
1631
|
}
|
|
1139
1632
|
|
|
1633
|
+
// Track activity + context on every tool call (keeps session alive, records what user is working on)
|
|
1634
|
+
touchSession(sessionId, {
|
|
1635
|
+
toolName: name,
|
|
1636
|
+
solutionId: args?.solution_id,
|
|
1637
|
+
skillId: args?.skill_id,
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1140
1640
|
// Check auth for tenant-aware operations — requires explicit ateam_auth call.
|
|
1141
1641
|
// Env vars (ADAS_API_KEY / ADAS_TENANT) are NOT sufficient — they may be
|
|
1142
1642
|
// baked into MCP config and silently target the wrong tenant.
|
|
@@ -1165,8 +1665,27 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
1165
1665
|
};
|
|
1166
1666
|
}
|
|
1167
1667
|
|
|
1668
|
+
// Master mode: per-call tenant override (no re-auth needed)
|
|
1669
|
+
if (TENANT_TOOLS.has(name) && isMasterMode(sessionId) && args?.tenant) {
|
|
1670
|
+
switchTenant(sessionId, args.tenant);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1168
1673
|
try {
|
|
1169
1674
|
const result = await handler(args, sessionId);
|
|
1675
|
+
|
|
1676
|
+
// For ateam_bootstrap, inject session context so the LLM knows what the user was working on
|
|
1677
|
+
if (name === "ateam_bootstrap") {
|
|
1678
|
+
const ctx = getSessionContext(sessionId);
|
|
1679
|
+
if (ctx.activeSolutionId || ctx.lastSkillId) {
|
|
1680
|
+
result.session_context = {
|
|
1681
|
+
_note: "This user has an active session. You can reference their previous work.",
|
|
1682
|
+
active_solution_id: ctx.activeSolutionId || null,
|
|
1683
|
+
last_skill_id: ctx.lastSkillId || null,
|
|
1684
|
+
last_tool_used: ctx.lastToolName || null,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1170
1689
|
return {
|
|
1171
1690
|
content: [{ type: "text", text: formatResult(result, name) }],
|
|
1172
1691
|
};
|