@ateam-ai/mcp 0.2.2 → 0.2.4
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 +171 -14
- package/src/http.js +34 -4
- package/src/tools.js +552 -22
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
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.)
|
|
14
15
|
const ENV_TENANT = process.env.ADAS_TENANT || "";
|
|
15
16
|
const ENV_API_KEY = process.env.ADAS_API_KEY || "";
|
|
16
17
|
|
|
17
|
-
// Request timeout (
|
|
18
|
-
const REQUEST_TIMEOUT_MS =
|
|
18
|
+
// Request timeout (120 seconds — deploys can take 60-90s)
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
19
20
|
|
|
20
21
|
// Session TTL — sessions idle longer than this are swept
|
|
21
22
|
const SESSION_TTL = 60 * 60 * 1000; // 60 minutes
|
|
@@ -27,6 +28,16 @@ const SWEEP_INTERVAL = 5 * 60 * 1000; // every 5 minutes
|
|
|
27
28
|
// context: { activeSolutionId, lastSkillId, lastToolName }
|
|
28
29
|
const sessions = new Map();
|
|
29
30
|
|
|
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
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Parse a tenant-embedded API key.
|
|
32
43
|
* Format: adas_<tenant>_<32hex>
|
|
@@ -43,10 +54,12 @@ export function parseApiKey(key) {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
|
-
* Set credentials for a session
|
|
57
|
+
* Set credentials for a session.
|
|
47
58
|
* If tenant is not provided, it's auto-extracted from the key.
|
|
59
|
+
* Set explicit=true when called from ateam_auth (not from seedCredentials).
|
|
60
|
+
* Set masterKey for cross-tenant master mode (uses shared secret auth).
|
|
48
61
|
*/
|
|
49
|
-
export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
62
|
+
export function setSessionCredentials(sessionId, { tenant, apiKey, apiUrl, explicit = false, masterKey = null }) {
|
|
50
63
|
let resolvedTenant = tenant;
|
|
51
64
|
if (!resolvedTenant && apiKey) {
|
|
52
65
|
const parsed = parseApiKey(apiKey);
|
|
@@ -56,10 +69,36 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
|
|
|
56
69
|
sessions.set(sessionId, {
|
|
57
70
|
tenant: resolvedTenant || "main",
|
|
58
71
|
apiKey,
|
|
72
|
+
apiUrl: apiUrl || existing?.apiUrl || null,
|
|
73
|
+
authExplicit: explicit || existing?.authExplicit || false,
|
|
74
|
+
masterKey: masterKey || existing?.masterKey || null,
|
|
59
75
|
lastActivity: Date.now(),
|
|
60
76
|
context: existing?.context || {},
|
|
61
77
|
});
|
|
62
|
-
|
|
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
|
+
}
|
|
82
|
+
|
|
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);
|
|
63
102
|
}
|
|
64
103
|
|
|
65
104
|
/**
|
|
@@ -95,13 +134,17 @@ export function isAuthenticated(sessionId) {
|
|
|
95
134
|
|
|
96
135
|
/**
|
|
97
136
|
* Check if a session has been explicitly authenticated via ateam_auth.
|
|
98
|
-
*
|
|
137
|
+
* Checks per-session credentials AND bearer auth overrides.
|
|
99
138
|
* Used to gate tenant-aware operations — env vars alone are not sufficient
|
|
100
139
|
* to deploy, update, or read solutions.
|
|
101
140
|
*/
|
|
102
141
|
export function isExplicitlyAuthenticated(sessionId) {
|
|
103
142
|
if (!sessionId) return false;
|
|
104
|
-
|
|
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);
|
|
105
148
|
}
|
|
106
149
|
|
|
107
150
|
/**
|
|
@@ -134,9 +177,69 @@ export function getSessionContext(sessionId) {
|
|
|
134
177
|
* Remove session credentials (on disconnect).
|
|
135
178
|
*/
|
|
136
179
|
export function clearSession(sessionId) {
|
|
180
|
+
sessionBearers.delete(sessionId);
|
|
137
181
|
sessions.delete(sessionId);
|
|
138
182
|
}
|
|
139
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
|
+
|
|
140
243
|
/**
|
|
141
244
|
* Sweep expired sessions — removes sessions idle longer than SESSION_TTL.
|
|
142
245
|
* Returns the number of sessions removed.
|
|
@@ -146,12 +249,21 @@ export function sweepStaleSessions() {
|
|
|
146
249
|
let swept = 0;
|
|
147
250
|
for (const [sid, session] of sessions) {
|
|
148
251
|
if (now - session.lastActivity > SESSION_TTL) {
|
|
252
|
+
sessionBearers.delete(sid);
|
|
149
253
|
sessions.delete(sid);
|
|
150
254
|
swept++;
|
|
151
255
|
}
|
|
152
256
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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.`);
|
|
155
267
|
}
|
|
156
268
|
return swept;
|
|
157
269
|
}
|
|
@@ -186,6 +298,17 @@ export function getSessionStats() {
|
|
|
186
298
|
}
|
|
187
299
|
|
|
188
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
|
|
189
312
|
const { tenant, apiKey } = getCredentials(sessionId);
|
|
190
313
|
const h = { "Content-Type": "application/json" };
|
|
191
314
|
if (tenant) h["X-ADAS-TENANT"] = tenant;
|
|
@@ -232,6 +355,7 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
232
355
|
const timeoutMs = opts.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
233
356
|
const controller = new AbortController();
|
|
234
357
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
358
|
+
const baseUrl = getBaseUrl(sessionId);
|
|
235
359
|
|
|
236
360
|
try {
|
|
237
361
|
const fetchOpts = {
|
|
@@ -243,7 +367,7 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
243
367
|
fetchOpts.body = JSON.stringify(body);
|
|
244
368
|
}
|
|
245
369
|
|
|
246
|
-
const res = await fetch(`${
|
|
370
|
+
const res = await fetch(`${baseUrl}${path}`, fetchOpts);
|
|
247
371
|
|
|
248
372
|
if (!res.ok) {
|
|
249
373
|
const text = await res.text().catch(() => "");
|
|
@@ -255,18 +379,18 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
255
379
|
if (err.name === "AbortError") {
|
|
256
380
|
throw new Error(
|
|
257
381
|
`A-Team API timeout: ${method} ${path} did not respond within ${timeoutMs / 1000}s.\n` +
|
|
258
|
-
`Hint: The A-Team API at ${
|
|
382
|
+
`Hint: The A-Team API at ${baseUrl} may be down. Check ${baseUrl}/health`
|
|
259
383
|
);
|
|
260
384
|
}
|
|
261
385
|
if (err.cause?.code === "ECONNREFUSED") {
|
|
262
386
|
throw new Error(
|
|
263
|
-
`Cannot connect to A-Team API at ${
|
|
264
|
-
`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`
|
|
265
389
|
);
|
|
266
390
|
}
|
|
267
391
|
if (err.cause?.code === "ENOTFOUND") {
|
|
268
392
|
throw new Error(
|
|
269
|
-
`Cannot resolve A-Team API host: ${
|
|
393
|
+
`Cannot resolve A-Team API host: ${baseUrl}.\n` +
|
|
270
394
|
`Hint: Check your internet connection and ADAS_API_URL setting.`
|
|
271
395
|
);
|
|
272
396
|
}
|
|
@@ -291,3 +415,36 @@ export async function patch(path, body, sessionId, opts) {
|
|
|
291
415
|
export async function del(path, sessionId, opts) {
|
|
292
416
|
return request("DELETE", path, undefined, sessionId, opts);
|
|
293
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
|
@@ -26,6 +26,7 @@ import { createServer } from "./server.js";
|
|
|
26
26
|
import {
|
|
27
27
|
clearSession, setSessionCredentials, parseApiKey,
|
|
28
28
|
startSessionSweeper, getSessionStats, sweepStaleSessions,
|
|
29
|
+
bindSessionBearer, getAuthOverride,
|
|
29
30
|
} from "./api.js";
|
|
30
31
|
import { mountOAuth } from "./oauth.js";
|
|
31
32
|
|
|
@@ -197,8 +198,13 @@ export function startHttpServer(port = 3100) {
|
|
|
197
198
|
// Reuse existing session — seed credentials if Bearer token present
|
|
198
199
|
transport = transports[sessionId];
|
|
199
200
|
seedCredentials(req, sessionId);
|
|
200
|
-
} else if (
|
|
201
|
-
// 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
|
+
|
|
202
208
|
const newSessionId = randomUUID();
|
|
203
209
|
|
|
204
210
|
// Seed credentials from OAuth Bearer token before server starts
|
|
@@ -224,6 +230,16 @@ export function startHttpServer(port = 3100) {
|
|
|
224
230
|
await server.connect(transport);
|
|
225
231
|
await transport.handleRequest(req, res, req.body);
|
|
226
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;
|
|
227
243
|
} else {
|
|
228
244
|
res.status(400).json({
|
|
229
245
|
jsonrpc: "2.0",
|
|
@@ -326,13 +342,27 @@ function getNewestToken() {
|
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
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.
|
|
331
350
|
*/
|
|
332
351
|
function seedCredentials(req, sessionId) {
|
|
333
352
|
const token = req.auth?.token;
|
|
334
353
|
if (!token) return;
|
|
335
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
|
|
336
366
|
const parsed = parseApiKey(token);
|
|
337
367
|
if (parsed.isValid) {
|
|
338
368
|
setSessionCredentials(sessionId, { tenant: parsed.tenant, apiKey: token });
|
package/src/tools.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
get, post, patch, del,
|
|
13
13
|
setSessionCredentials, isAuthenticated, isExplicitlyAuthenticated,
|
|
14
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,181 @@ 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_redeploy",
|
|
821
|
+
core: true,
|
|
822
|
+
description:
|
|
823
|
+
"Re-deploy all skills in a solution without changing anything. Regenerates MCP servers and pushes to A-Team Core. Use after connector restarts, Core hiccups, or when you just need a fresh deploy without modifying the solution/skill definitions.",
|
|
824
|
+
inputSchema: {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {
|
|
827
|
+
solution_id: {
|
|
828
|
+
type: "string",
|
|
829
|
+
description: "The solution ID to redeploy (e.g. 'smart-home-assistant')",
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
required: ["solution_id"],
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
name: "ateam_status_all",
|
|
837
|
+
core: true,
|
|
838
|
+
description:
|
|
839
|
+
"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.",
|
|
840
|
+
inputSchema: {
|
|
841
|
+
type: "object",
|
|
842
|
+
properties: {},
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: "ateam_sync_all",
|
|
847
|
+
core: true,
|
|
848
|
+
description:
|
|
849
|
+
"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.",
|
|
850
|
+
inputSchema: {
|
|
851
|
+
type: "object",
|
|
852
|
+
properties: {
|
|
853
|
+
push_only: {
|
|
854
|
+
type: "boolean",
|
|
855
|
+
description: "Only push to GitHub (skip pull to Core). Default: false (full sync).",
|
|
856
|
+
},
|
|
857
|
+
pull_only: {
|
|
858
|
+
type: "boolean",
|
|
859
|
+
description: "Only pull from GitHub to Core (skip push). Default: false (full sync).",
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
},
|
|
600
864
|
];
|
|
601
865
|
|
|
602
866
|
/**
|
|
@@ -612,6 +876,7 @@ const SPEC_PATHS = {
|
|
|
612
876
|
skill: "/spec/skill",
|
|
613
877
|
solution: "/spec/solution",
|
|
614
878
|
enums: "/spec/enums",
|
|
879
|
+
"connector-multi-user": "/spec/multi-user-connector",
|
|
615
880
|
};
|
|
616
881
|
|
|
617
882
|
const EXAMPLE_PATHS = {
|
|
@@ -637,17 +902,31 @@ const TENANT_TOOLS = new Set([
|
|
|
637
902
|
"ateam_update",
|
|
638
903
|
"ateam_redeploy",
|
|
639
904
|
"ateam_delete_solution",
|
|
905
|
+
"ateam_delete_connector",
|
|
906
|
+
"ateam_redeploy",
|
|
640
907
|
"ateam_solution_chat",
|
|
641
908
|
// Read operations (tenant-specific data)
|
|
642
909
|
"ateam_list_solutions",
|
|
643
910
|
"ateam_get_solution",
|
|
644
911
|
"ateam_get_execution_logs",
|
|
645
912
|
"ateam_test_skill",
|
|
913
|
+
"ateam_test_pipeline",
|
|
914
|
+
"ateam_test_voice",
|
|
646
915
|
"ateam_test_status",
|
|
647
916
|
"ateam_test_abort",
|
|
648
917
|
"ateam_get_connector_source",
|
|
649
918
|
"ateam_get_metrics",
|
|
650
919
|
"ateam_diff",
|
|
920
|
+
// GitHub operations
|
|
921
|
+
"ateam_github_push",
|
|
922
|
+
"ateam_github_pull",
|
|
923
|
+
"ateam_github_status",
|
|
924
|
+
"ateam_github_read",
|
|
925
|
+
"ateam_github_patch",
|
|
926
|
+
"ateam_github_log",
|
|
927
|
+
// Master key bulk operations
|
|
928
|
+
"ateam_status_all",
|
|
929
|
+
"ateam_sync_all",
|
|
651
930
|
]);
|
|
652
931
|
|
|
653
932
|
/** Small delay helper */
|
|
@@ -676,12 +955,13 @@ const handlers = {
|
|
|
676
955
|
{ name: "Enterprise Compliance Platform", description: "Approval flows, audit logs, policy enforcement" },
|
|
677
956
|
],
|
|
678
957
|
developer_loop: {
|
|
679
|
-
_note: "This is the recommended build loop.
|
|
958
|
+
_note: "This is the recommended build loop. 5 steps from definition to running skill with GitHub version control.",
|
|
680
959
|
steps: [
|
|
681
960
|
{ 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: "
|
|
961
|
+
{ 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"] },
|
|
962
|
+
{ 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"] },
|
|
963
|
+
{ 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"] },
|
|
964
|
+
{ 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
965
|
],
|
|
686
966
|
},
|
|
687
967
|
first_questions: [
|
|
@@ -690,6 +970,27 @@ const handlers = {
|
|
|
690
970
|
{ id: "systems", question: "Which systems should the Team connect to?", type: "multi_select", options: ["slack", "email", "zendesk", "shopify", "jira", "postgres", "custom_api", "none"] },
|
|
691
971
|
{ id: "security", question: "What environment constraints?", type: "enum", options: ["sandbox", "controlled", "regulated"] },
|
|
692
972
|
],
|
|
973
|
+
github_tools: {
|
|
974
|
+
_note: "Version control for solutions. Every deploy auto-pushes to GitHub. The repo is the source of truth for connector code.",
|
|
975
|
+
tools: ["ateam_github_push", "ateam_github_pull", "ateam_github_status", "ateam_github_read", "ateam_github_patch", "ateam_github_log"],
|
|
976
|
+
repo_structure: {
|
|
977
|
+
"solution.json": "Full solution definition",
|
|
978
|
+
"skills/{skill-id}/skill.json": "Individual skill definitions",
|
|
979
|
+
"connectors/{connector-id}/server.js": "Connector MCP server code",
|
|
980
|
+
"connectors/{connector-id}/package.json": "Connector dependencies",
|
|
981
|
+
},
|
|
982
|
+
iteration_workflow: {
|
|
983
|
+
code_changes: "ateam_github_patch (edit connector files) → ateam_build_and_run(github:true) (redeploy from repo)",
|
|
984
|
+
definition_changes: "ateam_patch (edit skill/solution definitions directly in Builder)",
|
|
985
|
+
first_deploy: "Must include mcp_store — this creates the GitHub repo",
|
|
986
|
+
},
|
|
987
|
+
when_to_use_what: {
|
|
988
|
+
ateam_github_patch: "Edit connector source code (server.js, utils, package.json, UI assets)",
|
|
989
|
+
ateam_patch: "Edit skill definitions (intents, tools, policy) or solution definitions (grants, handoffs, routing)",
|
|
990
|
+
"ateam_build_and_run(github:true)": "Redeploy solution pulling latest connector code from GitHub",
|
|
991
|
+
"ateam_build_and_run(mcp_store)": "First deploy or when you want to pass connector code inline",
|
|
992
|
+
},
|
|
993
|
+
},
|
|
693
994
|
advanced_tools: {
|
|
694
995
|
_note: "These tools are available but hidden from the default tool list. Call them by name when you need fine-grained control.",
|
|
695
996
|
debugging: ["ateam_get_execution_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"],
|
|
@@ -728,7 +1029,8 @@ const handlers = {
|
|
|
728
1029
|
always: [
|
|
729
1030
|
"Explain Skill vs Solution vs Connector before building",
|
|
730
1031
|
"Use ateam_build_and_run for the full lifecycle (validates automatically)",
|
|
731
|
-
"Use ateam_patch for
|
|
1032
|
+
"Use ateam_patch for skill/solution definition changes (updates + redeploys automatically)",
|
|
1033
|
+
"Use ateam_github_patch + ateam_build_and_run(github:true) for connector code changes after first deploy",
|
|
732
1034
|
"Study the connector example (ateam_get_examples type='connector') before writing connector code",
|
|
733
1035
|
"Ask discovery questions if goal unclear",
|
|
734
1036
|
],
|
|
@@ -741,21 +1043,52 @@ const handlers = {
|
|
|
741
1043
|
},
|
|
742
1044
|
}),
|
|
743
1045
|
|
|
744
|
-
ateam_auth: async ({ api_key, tenant }, sessionId) => {
|
|
1046
|
+
ateam_auth: async ({ api_key, master_key, tenant, url }, sessionId) => {
|
|
1047
|
+
// Master key mode: cross-tenant auth using shared secret
|
|
1048
|
+
if (master_key) {
|
|
1049
|
+
if (!tenant) {
|
|
1050
|
+
return { ok: false, message: "Master key requires a tenant parameter. Specify which tenant to operate on." };
|
|
1051
|
+
}
|
|
1052
|
+
const apiUrl = url ? url.replace(/\/+$/, "") : undefined;
|
|
1053
|
+
setSessionCredentials(sessionId, { tenant, apiKey: null, apiUrl, explicit: true, masterKey: master_key });
|
|
1054
|
+
// Verify by listing solutions
|
|
1055
|
+
try {
|
|
1056
|
+
const result = await get("/deploy/solutions", sessionId);
|
|
1057
|
+
const urlNote = apiUrl ? ` (via ${apiUrl})` : "";
|
|
1058
|
+
return {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
tenant,
|
|
1061
|
+
masterMode: true,
|
|
1062
|
+
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.`,
|
|
1063
|
+
};
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
return { ok: false, tenant, message: `Master key auth failed: ${err.message}` };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Normal API key mode
|
|
1070
|
+
if (!api_key) {
|
|
1071
|
+
return { ok: false, message: "Provide either api_key or master_key." };
|
|
1072
|
+
}
|
|
745
1073
|
// Auto-extract tenant from key if not provided
|
|
746
1074
|
let resolvedTenant = tenant;
|
|
747
1075
|
if (!resolvedTenant) {
|
|
748
1076
|
const parsed = parseApiKey(api_key);
|
|
749
1077
|
resolvedTenant = parsed.tenant || "main";
|
|
750
1078
|
}
|
|
751
|
-
|
|
1079
|
+
// Normalize URL: strip trailing slash
|
|
1080
|
+
const apiUrl = url ? url.replace(/\/+$/, "") : undefined;
|
|
1081
|
+
setSessionCredentials(sessionId, { tenant: resolvedTenant, apiKey: api_key, apiUrl, explicit: true });
|
|
1082
|
+
// Persist override per bearer (survives session changes)
|
|
1083
|
+
setAuthOverride(sessionId, { tenant: resolvedTenant, apiKey: api_key, apiUrl });
|
|
752
1084
|
// Verify the key works by listing solutions
|
|
753
1085
|
try {
|
|
754
1086
|
const result = await get("/deploy/solutions", sessionId);
|
|
1087
|
+
const urlNote = apiUrl ? ` (via ${apiUrl})` : "";
|
|
755
1088
|
return {
|
|
756
1089
|
ok: true,
|
|
757
1090
|
tenant: resolvedTenant,
|
|
758
|
-
message: `Authenticated to tenant "${resolvedTenant}". ${result.solutions?.length || 0} solution(s) found.`,
|
|
1091
|
+
message: `Authenticated to tenant "${resolvedTenant}"${urlNote}. ${result.solutions?.length || 0} solution(s) found.`,
|
|
759
1092
|
};
|
|
760
1093
|
} catch (err) {
|
|
761
1094
|
return {
|
|
@@ -776,13 +1109,49 @@ const handlers = {
|
|
|
776
1109
|
// Validates → Deploys → Health-checks → Optionally tests
|
|
777
1110
|
// One call replaces: validate_solution + deploy_solution + get_solution(health)
|
|
778
1111
|
|
|
779
|
-
ateam_build_and_run: async ({ solution, skills, connectors, mcp_store, test_message, test_skill_id }, sid) => {
|
|
1112
|
+
ateam_build_and_run: async ({ solution, skills, connectors, mcp_store, github, test_message, test_skill_id }, sid) => {
|
|
780
1113
|
const phases = [];
|
|
781
1114
|
|
|
1115
|
+
// Phase 0: GitHub pull (if github:true — pull connector source from repo)
|
|
1116
|
+
let effectiveMcpStore = mcp_store;
|
|
1117
|
+
if (github && !mcp_store) {
|
|
1118
|
+
try {
|
|
1119
|
+
const pullResult = await post(
|
|
1120
|
+
`/deploy/solutions/${solution.id}/github/pull-connectors`,
|
|
1121
|
+
{},
|
|
1122
|
+
sid,
|
|
1123
|
+
{ timeoutMs: 30_000 },
|
|
1124
|
+
);
|
|
1125
|
+
if (!pullResult.ok) {
|
|
1126
|
+
return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
phase: "github_pull",
|
|
1129
|
+
error: pullResult.error || "Failed to pull connectors from GitHub",
|
|
1130
|
+
hint: pullResult.hint || "Deploy the solution first (with mcp_store) to auto-create the GitHub repo.",
|
|
1131
|
+
message: "Cannot pull connector code from GitHub. The repo may not exist yet — deploy with mcp_store first.",
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
effectiveMcpStore = pullResult.mcp_store;
|
|
1135
|
+
phases.push({
|
|
1136
|
+
phase: "github_pull",
|
|
1137
|
+
status: "done",
|
|
1138
|
+
connectors_found: pullResult.connectors_found || 0,
|
|
1139
|
+
files_loaded: pullResult.files_loaded || 0,
|
|
1140
|
+
});
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
return {
|
|
1143
|
+
ok: false,
|
|
1144
|
+
phase: "github_pull",
|
|
1145
|
+
error: err.message,
|
|
1146
|
+
message: "Failed to pull connector code from GitHub. The repo may not exist yet — deploy with mcp_store first.",
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
782
1151
|
// Phase 1: Validate
|
|
783
1152
|
let validation;
|
|
784
1153
|
try {
|
|
785
|
-
validation = await post("/validate/solution", { solution, skills }, sid);
|
|
1154
|
+
validation = await post("/validate/solution", { solution, skills, connectors, mcp_store: effectiveMcpStore }, sid, { timeoutMs: 120_000 });
|
|
786
1155
|
phases.push({ phase: "validate", status: "done" });
|
|
787
1156
|
} catch (err) {
|
|
788
1157
|
return {
|
|
@@ -808,7 +1177,7 @@ const handlers = {
|
|
|
808
1177
|
// Phase 2: Deploy
|
|
809
1178
|
let deploy;
|
|
810
1179
|
try {
|
|
811
|
-
deploy = await post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid);
|
|
1180
|
+
deploy = await post("/deploy/solution", { solution, skills, connectors, mcp_store: effectiveMcpStore }, sid, { timeoutMs: 300_000 });
|
|
812
1181
|
phases.push({ phase: "deploy", status: deploy.ok ? "done" : "failed" });
|
|
813
1182
|
} catch (err) {
|
|
814
1183
|
return {
|
|
@@ -863,6 +1232,25 @@ const handlers = {
|
|
|
863
1232
|
}
|
|
864
1233
|
}
|
|
865
1234
|
|
|
1235
|
+
// Phase 5: GitHub push (auto — non-blocking, failures don't fail the deploy)
|
|
1236
|
+
let github_result;
|
|
1237
|
+
try {
|
|
1238
|
+
github_result = await post(
|
|
1239
|
+
`/deploy/solutions/${solution.id}/github/push`,
|
|
1240
|
+
{ message: `Deploy: ${solution.name || solution.id}` },
|
|
1241
|
+
sid,
|
|
1242
|
+
{ timeoutMs: 60_000 },
|
|
1243
|
+
);
|
|
1244
|
+
phases.push({
|
|
1245
|
+
phase: "github",
|
|
1246
|
+
status: github_result.skipped ? "skipped" : "done",
|
|
1247
|
+
...(github_result.repo_url && { repo_url: github_result.repo_url }),
|
|
1248
|
+
});
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
github_result = { error: err.message };
|
|
1251
|
+
phases.push({ phase: "github", status: "error", error: err.message });
|
|
1252
|
+
}
|
|
1253
|
+
|
|
866
1254
|
return {
|
|
867
1255
|
ok: true,
|
|
868
1256
|
solution_id: solution.id,
|
|
@@ -875,6 +1263,7 @@ const handlers = {
|
|
|
875
1263
|
},
|
|
876
1264
|
health,
|
|
877
1265
|
...(test_result && { test_result }),
|
|
1266
|
+
...(github_result && !github_result.error && !github_result.skipped && { github: github_result }),
|
|
878
1267
|
...(validation.warnings?.length > 0 && { validation_warnings: validation.warnings }),
|
|
879
1268
|
};
|
|
880
1269
|
},
|
|
@@ -956,8 +1345,8 @@ const handlers = {
|
|
|
956
1345
|
|
|
957
1346
|
ateam_validate_skill: async ({ skill }, sid) => post("/validate/skill", { skill }, sid),
|
|
958
1347
|
|
|
959
|
-
ateam_validate_solution: async ({ solution, skills }, sid) =>
|
|
960
|
-
post("/validate/solution", { solution, skills }, sid),
|
|
1348
|
+
ateam_validate_solution: async ({ solution, skills, connectors, mcp_store }, sid) =>
|
|
1349
|
+
post("/validate/solution", { solution, skills, connectors, mcp_store }, sid),
|
|
961
1350
|
|
|
962
1351
|
ateam_deploy_solution: async ({ solution, skills, connectors, mcp_store }, sid) =>
|
|
963
1352
|
post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid),
|
|
@@ -1042,6 +1431,20 @@ const handlers = {
|
|
|
1042
1431
|
return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs });
|
|
1043
1432
|
},
|
|
1044
1433
|
|
|
1434
|
+
ateam_test_pipeline: async ({ solution_id, skill_id, message }, sid) =>
|
|
1435
|
+
post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test-pipeline`, { message }, sid, { timeoutMs: 30_000 }),
|
|
1436
|
+
|
|
1437
|
+
ateam_test_voice: async ({ solution_id, messages, phone_number, skill_slug, timeout_ms }, sid) => {
|
|
1438
|
+
const body = { messages };
|
|
1439
|
+
if (phone_number) body.phone_number = phone_number;
|
|
1440
|
+
if (skill_slug) body.skill_slug = skill_slug;
|
|
1441
|
+
if (timeout_ms) body.timeout_ms = timeout_ms;
|
|
1442
|
+
// Timeout scales with message count — each turn may invoke skills
|
|
1443
|
+
const perTurnMs = timeout_ms || 60_000;
|
|
1444
|
+
const timeoutTotal = Math.min(perTurnMs * messages.length + 30_000, 600_000);
|
|
1445
|
+
return post(`/deploy/voice-test`, body, sid, { timeoutMs: timeoutTotal });
|
|
1446
|
+
},
|
|
1447
|
+
|
|
1045
1448
|
ateam_test_status: async ({ solution_id, skill_id, job_id }, sid) =>
|
|
1046
1449
|
get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid),
|
|
1047
1450
|
|
|
@@ -1064,8 +1467,130 @@ const handlers = {
|
|
|
1064
1467
|
return get(`/deploy/solutions/${solution_id}/diff${qs}`, sid);
|
|
1065
1468
|
},
|
|
1066
1469
|
|
|
1470
|
+
// ─── GitHub tools ──────────────────────────────────────────────────
|
|
1471
|
+
|
|
1472
|
+
ateam_github_push: async ({ solution_id, message }, sid) =>
|
|
1473
|
+
post(`/deploy/solutions/${solution_id}/github/push`, { message }, sid, { timeoutMs: 60_000 }),
|
|
1474
|
+
|
|
1475
|
+
ateam_github_pull: async ({ solution_id }, sid) =>
|
|
1476
|
+
post(`/deploy/solutions/${solution_id}/github/pull`, {}, sid, { timeoutMs: 300_000 }),
|
|
1477
|
+
|
|
1478
|
+
ateam_github_status: async ({ solution_id }, sid) =>
|
|
1479
|
+
get(`/deploy/solutions/${solution_id}/github/status`, sid),
|
|
1480
|
+
|
|
1481
|
+
ateam_github_read: async ({ solution_id, path: filePath }, sid) =>
|
|
1482
|
+
get(`/deploy/solutions/${solution_id}/github/read?path=${encodeURIComponent(filePath)}`, sid),
|
|
1483
|
+
|
|
1484
|
+
ateam_github_patch: async ({ solution_id, path: filePath, content, message }, sid) =>
|
|
1485
|
+
post(`/deploy/solutions/${solution_id}/github/patch`, { path: filePath, content, message }, sid),
|
|
1486
|
+
|
|
1487
|
+
ateam_github_log: async ({ solution_id, limit }, sid) => {
|
|
1488
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
1489
|
+
return get(`/deploy/solutions/${solution_id}/github/log${qs}`, sid);
|
|
1490
|
+
},
|
|
1491
|
+
|
|
1067
1492
|
ateam_delete_solution: async ({ solution_id }, sid) =>
|
|
1068
1493
|
del(`/deploy/solutions/${solution_id}`, sid),
|
|
1494
|
+
|
|
1495
|
+
ateam_delete_connector: async ({ solution_id, connector_id }, sid) =>
|
|
1496
|
+
del(`/deploy/solutions/${solution_id}/connectors/${connector_id}`, sid),
|
|
1497
|
+
|
|
1498
|
+
ateam_redeploy: async ({ solution_id }, sid) => {
|
|
1499
|
+
const result = await post(`/deploy/solutions/${solution_id}/redeploy`, {}, sid, { timeoutMs: 300_000 });
|
|
1500
|
+
return {
|
|
1501
|
+
ok: result.ok,
|
|
1502
|
+
solution_id,
|
|
1503
|
+
deployed: result.deployed || 0,
|
|
1504
|
+
failed: result.failed || 0,
|
|
1505
|
+
total: result.total || 0,
|
|
1506
|
+
skills: result.skills || [],
|
|
1507
|
+
message: result.ok
|
|
1508
|
+
? `Re-deployed ${result.deployed || 0} skill(s) successfully.`
|
|
1509
|
+
: `Re-deploy had ${result.failed || 0} failure(s). Check skills array for details.`,
|
|
1510
|
+
};
|
|
1511
|
+
},
|
|
1512
|
+
|
|
1513
|
+
// ─── Master Key Bulk Tools ───────────────────────────────────────────
|
|
1514
|
+
|
|
1515
|
+
ateam_status_all: async (_args, sid) => {
|
|
1516
|
+
if (!isMasterMode(sid)) {
|
|
1517
|
+
return { ok: false, message: "Master key required. Call ateam_auth(master_key: \"<key>\", tenant: \"<any>\") first." };
|
|
1518
|
+
}
|
|
1519
|
+
const tenants = await listTenants(sid);
|
|
1520
|
+
const results = [];
|
|
1521
|
+
for (const t of tenants) {
|
|
1522
|
+
switchTenant(sid, t.id);
|
|
1523
|
+
try {
|
|
1524
|
+
const { solutions } = await get("/deploy/solutions", sid);
|
|
1525
|
+
for (const sol of (solutions || [])) {
|
|
1526
|
+
let ghStatus = null;
|
|
1527
|
+
try {
|
|
1528
|
+
ghStatus = await get(`/deploy/solutions/${sol.id}/github/status`, sid);
|
|
1529
|
+
} catch { /* no github config */ }
|
|
1530
|
+
results.push({
|
|
1531
|
+
tenant: t.id,
|
|
1532
|
+
solution: sol.id,
|
|
1533
|
+
name: sol.name || sol.id,
|
|
1534
|
+
github: ghStatus ? {
|
|
1535
|
+
repo: ghStatus.repo || ghStatus.repoUrl,
|
|
1536
|
+
lastCommit: ghStatus.lastCommit?.message?.slice(0, 60),
|
|
1537
|
+
lastPush: ghStatus.lastCommit?.date,
|
|
1538
|
+
branch: ghStatus.branch,
|
|
1539
|
+
} : "not configured",
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
results.push({ tenant: t.id, error: err.message });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return { ok: true, tenants: tenants.length, solutions: results.length, results };
|
|
1547
|
+
},
|
|
1548
|
+
|
|
1549
|
+
ateam_sync_all: async ({ push_only, pull_only }, sid) => {
|
|
1550
|
+
if (!isMasterMode(sid)) {
|
|
1551
|
+
return { ok: false, message: "Master key required. Call ateam_auth(master_key: \"<key>\", tenant: \"<any>\") first." };
|
|
1552
|
+
}
|
|
1553
|
+
const tenants = await listTenants(sid);
|
|
1554
|
+
const results = [];
|
|
1555
|
+
for (const t of tenants) {
|
|
1556
|
+
switchTenant(sid, t.id);
|
|
1557
|
+
try {
|
|
1558
|
+
const { solutions } = await get("/deploy/solutions", sid);
|
|
1559
|
+
for (const sol of (solutions || [])) {
|
|
1560
|
+
const entry = { tenant: t.id, solution: sol.id, name: sol.name || sol.id };
|
|
1561
|
+
// Push: Builder FS → GitHub
|
|
1562
|
+
if (!pull_only) {
|
|
1563
|
+
try {
|
|
1564
|
+
const pushResult = await post(`/deploy/solutions/${sol.id}/github/push`, {}, sid);
|
|
1565
|
+
entry.push = { ok: true, commit: pushResult.commitSha?.slice(0, 8), files: pushResult.filesCommitted };
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
entry.push = { ok: false, error: err.message.slice(0, 100) };
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// Pull: GitHub → Core MongoDB
|
|
1571
|
+
if (!push_only) {
|
|
1572
|
+
try {
|
|
1573
|
+
const pullResult = await post(`/deploy/solutions/${sol.id}/github/pull`, {}, sid);
|
|
1574
|
+
entry.pull = { ok: true, skills: pullResult.skills?.length, connectors: pullResult.connectors?.length };
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
entry.pull = { ok: false, error: err.message.slice(0, 100) };
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
results.push(entry);
|
|
1580
|
+
}
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
results.push({ tenant: t.id, error: err.message });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const pushCount = results.filter(r => r.push?.ok).length;
|
|
1586
|
+
const pullCount = results.filter(r => r.pull?.ok).length;
|
|
1587
|
+
const errors = results.filter(r => r.error || r.push?.ok === false || r.pull?.ok === false).length;
|
|
1588
|
+
return {
|
|
1589
|
+
ok: errors === 0,
|
|
1590
|
+
summary: `Synced ${tenants.length} tenant(s), ${results.length} solution(s). Push: ${pushCount} ok. Pull: ${pullCount} ok. Errors: ${errors}.`,
|
|
1591
|
+
results,
|
|
1592
|
+
};
|
|
1593
|
+
},
|
|
1069
1594
|
};
|
|
1070
1595
|
|
|
1071
1596
|
// ─── Response formatting ────────────────────────────────────────────
|
|
@@ -1172,6 +1697,11 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
1172
1697
|
};
|
|
1173
1698
|
}
|
|
1174
1699
|
|
|
1700
|
+
// Master mode: per-call tenant override (no re-auth needed)
|
|
1701
|
+
if (TENANT_TOOLS.has(name) && isMasterMode(sessionId) && args?.tenant) {
|
|
1702
|
+
switchTenant(sessionId, args.tenant);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1175
1705
|
try {
|
|
1176
1706
|
const result = await handler(args, sessionId);
|
|
1177
1707
|
|