@ateam-ai/mcp 0.1.13 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.1.13",
3
+ "version": "0.2.2",
4
4
  "mcpName": "io.github.ariekogan/ateam-mcp",
5
5
  "description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -5,6 +5,9 @@
5
5
  * 1. Per-session override (set via ateam_auth tool — used by HTTP transport)
6
6
  * 2. Environment variables (ADAS_API_KEY, ADAS_TENANT — used by stdio transport)
7
7
  * 3. Defaults (no key, tenant "main")
8
+ *
9
+ * Sessions also track activity timestamps and optional context (active solution,
10
+ * last skill) to support TTL-based cleanup and smarter UX.
8
11
  */
9
12
 
10
13
  const BASE_URL = process.env.ADAS_API_URL || "https://api.ateam-ai.com";
@@ -14,14 +17,15 @@ const ENV_API_KEY = process.env.ADAS_API_KEY || "";
14
17
  // Request timeout (30 seconds)
15
18
  const REQUEST_TIMEOUT_MS = 30_000;
16
19
 
17
- // Per-session credential store (sessionId { tenant, apiKey })
18
- const sessions = new Map();
20
+ // Session TTL sessions idle longer than this are swept
21
+ const SESSION_TTL = 60 * 60 * 1000; // 60 minutes
22
+
23
+ // Sweep interval — how often we check for stale sessions
24
+ const SWEEP_INTERVAL = 5 * 60 * 1000; // every 5 minutes
19
25
 
20
- // Per-tenant credential fallback for MCP clients that don't persist sessions
21
- // (e.g., ChatGPT's bridge creates a new session per tool call).
22
- // Keyed by tenant to prevent cross-user credential leaks in shared MCP servers.
23
- const tenantFallbacks = new Map(); // tenant → { tenant, apiKey, createdAt }
24
- const FALLBACK_TTL = 60 * 60 * 1000; // 60 minutes
26
+ // Per-session store (sessionId { tenant, apiKey, lastActivity, context })
27
+ // context: { activeSolutionId, lastSkillId, lastToolName }
28
+ const sessions = new Map();
25
29
 
26
30
  /**
27
31
  * Parse a tenant-embedded API key.
@@ -41,7 +45,6 @@ export function parseApiKey(key) {
41
45
  /**
42
46
  * Set credentials for a session (called by ateam_auth tool).
43
47
  * If tenant is not provided, it's auto-extracted from the key.
44
- * Also updates the global fallback so new sessions inherit credentials.
45
48
  */
46
49
  export function setSessionCredentials(sessionId, { tenant, apiKey }) {
47
50
  let resolvedTenant = tenant;
@@ -49,12 +52,14 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
49
52
  const parsed = parseApiKey(apiKey);
50
53
  if (parsed.tenant) resolvedTenant = parsed.tenant;
51
54
  }
52
- const creds = { tenant: resolvedTenant || "main", apiKey };
53
- sessions.set(sessionId, creds);
54
-
55
- // Update per-tenant fallback — only sessions for the SAME tenant will inherit this
56
- tenantFallbacks.set(creds.tenant, { ...creds, createdAt: Date.now() });
57
- console.log(`[Auth] Credentials set for session ${sessionId}, tenant fallback updated (tenant: ${creds.tenant})`);
55
+ const existing = sessions.get(sessionId);
56
+ sessions.set(sessionId, {
57
+ tenant: resolvedTenant || "main",
58
+ apiKey,
59
+ lastActivity: Date.now(),
60
+ context: existing?.context || {},
61
+ });
62
+ console.log(`[Auth] Credentials set for session ${sessionId} (tenant: ${resolvedTenant || "main"})`);
58
63
  }
59
64
 
60
65
  /**
@@ -62,10 +67,6 @@ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
62
67
  * Resolution order:
63
68
  * 1. Per-session (from ateam_auth or seedCredentials)
64
69
  * 2. Environment variables (ADAS_API_KEY, ADAS_TENANT)
65
- *
66
- * Note: tenantFallbacks are NOT used in getCredentials() to prevent
67
- * cross-user credential leaks. They are only used in seedFromFallback()
68
- * which requires explicit tenant matching.
69
70
  */
70
71
  export function getCredentials(sessionId) {
71
72
  // 1. Per-session credentials
@@ -84,21 +85,6 @@ export function getCredentials(sessionId) {
84
85
  return { tenant: tenant || "main", apiKey };
85
86
  }
86
87
 
87
- /**
88
- * Seed a session's credentials from a matching tenant fallback.
89
- * Called by HTTP transport when a new session is created with a known tenant
90
- * (e.g., from OAuth token). Only inherits from the SAME tenant.
91
- */
92
- export function seedFromFallback(sessionId, tenant) {
93
- const fallback = tenantFallbacks.get(tenant);
94
- if (fallback && (Date.now() - fallback.createdAt < FALLBACK_TTL)) {
95
- sessions.set(sessionId, { tenant: fallback.tenant, apiKey: fallback.apiKey });
96
- console.log(`[Auth] Seeded session ${sessionId} from tenant fallback (tenant: ${tenant})`);
97
- return true;
98
- }
99
- return false;
100
- }
101
-
102
88
  /**
103
89
  * Check if a session is authenticated (has an API key from any source).
104
90
  */
@@ -107,6 +93,43 @@ export function isAuthenticated(sessionId) {
107
93
  return apiKey.length > 0;
108
94
  }
109
95
 
96
+ /**
97
+ * Check if a session has been explicitly authenticated via ateam_auth.
98
+ * This checks ONLY per-session credentials, ignoring env vars.
99
+ * Used to gate tenant-aware operations — env vars alone are not sufficient
100
+ * to deploy, update, or read solutions.
101
+ */
102
+ export function isExplicitlyAuthenticated(sessionId) {
103
+ if (!sessionId) return false;
104
+ return sessions.has(sessionId);
105
+ }
106
+
107
+ /**
108
+ * Record activity on a session — called on every tool call.
109
+ * Keeps the session alive and updates context for smarter UX.
110
+ */
111
+ export function touchSession(sessionId, { toolName, solutionId, skillId } = {}) {
112
+ const session = sessions.get(sessionId);
113
+ if (!session) return;
114
+
115
+ session.lastActivity = Date.now();
116
+
117
+ // Update context — track what the user is working on
118
+ if (toolName) session.context.lastToolName = toolName;
119
+ if (solutionId) session.context.activeSolutionId = solutionId;
120
+ if (skillId) session.context.lastSkillId = skillId;
121
+ }
122
+
123
+ /**
124
+ * Get session context — what the user has been working on.
125
+ * Returns {} if no session or no context.
126
+ */
127
+ export function getSessionContext(sessionId) {
128
+ const session = sessions.get(sessionId);
129
+ if (!session) return {};
130
+ return { ...session.context };
131
+ }
132
+
110
133
  /**
111
134
  * Remove session credentials (on disconnect).
112
135
  */
@@ -114,6 +137,54 @@ export function clearSession(sessionId) {
114
137
  sessions.delete(sessionId);
115
138
  }
116
139
 
140
+ /**
141
+ * Sweep expired sessions — removes sessions idle longer than SESSION_TTL.
142
+ * Returns the number of sessions removed.
143
+ */
144
+ export function sweepStaleSessions() {
145
+ const now = Date.now();
146
+ let swept = 0;
147
+ for (const [sid, session] of sessions) {
148
+ if (now - session.lastActivity > SESSION_TTL) {
149
+ sessions.delete(sid);
150
+ swept++;
151
+ }
152
+ }
153
+ if (swept > 0) {
154
+ console.log(`[Session] Swept ${swept} stale session(s). ${sessions.size} active.`);
155
+ }
156
+ return swept;
157
+ }
158
+
159
+ /**
160
+ * Start the periodic session sweep timer.
161
+ * Called once from HTTP transport on startup.
162
+ */
163
+ export function startSessionSweeper() {
164
+ const timer = setInterval(sweepStaleSessions, SWEEP_INTERVAL);
165
+ timer.unref(); // don't prevent process exit
166
+ console.log(`[Session] Sweep timer started (interval: ${SWEEP_INTERVAL / 1000}s, TTL: ${SESSION_TTL / 1000}s)`);
167
+ return timer;
168
+ }
169
+
170
+ /**
171
+ * Get session stats — for health checks and debugging.
172
+ */
173
+ export function getSessionStats() {
174
+ const now = Date.now();
175
+ let oldest = Infinity;
176
+ let newest = 0;
177
+ for (const [, session] of sessions) {
178
+ if (session.lastActivity < oldest) oldest = session.lastActivity;
179
+ if (session.lastActivity > newest) newest = session.lastActivity;
180
+ }
181
+ return {
182
+ active: sessions.size,
183
+ oldestAge: sessions.size > 0 ? Math.round((now - oldest) / 1000) : 0,
184
+ newestAge: sessions.size > 0 ? Math.round((now - newest) / 1000) : 0,
185
+ };
186
+ }
187
+
117
188
  function headers(sessionId) {
118
189
  const { tenant, apiKey } = getCredentials(sessionId);
119
190
  const h = { "Content-Type": "application/json" };
package/src/http.js CHANGED
@@ -23,7 +23,10 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
23
23
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
24
24
  import express from "express";
25
25
  import { createServer } from "./server.js";
26
- import { clearSession, setSessionCredentials, parseApiKey } from "./api.js";
26
+ import {
27
+ clearSession, setSessionCredentials, parseApiKey,
28
+ startSessionSweeper, getSessionStats, sweepStaleSessions,
29
+ } from "./api.js";
27
30
  import { mountOAuth } from "./oauth.js";
28
31
 
29
32
  // Active sessions
@@ -169,7 +172,12 @@ export function startHttpServer(port = 3100) {
169
172
 
170
173
  // ─── Health check ─────────────────────────────────────────────
171
174
  app.get("/health", (_req, res) => {
172
- res.json({ ok: true, service: "ateam-mcp", transport: "http" });
175
+ res.json({
176
+ ok: true,
177
+ service: "ateam-mcp",
178
+ transport: "http",
179
+ sessions: getSessionStats(),
180
+ });
173
181
  });
174
182
 
175
183
  // ─── Get API Key — redirect to Skill Builder with auto-open ──
@@ -290,8 +298,12 @@ export function startHttpServer(port = 3100) {
290
298
  console.log(` Health check: http://localhost:${port}/health`);
291
299
  });
292
300
 
293
- // Graceful shutdown
301
+ // Start periodic session cleanup (sweeps stale sessions every 5 min)
302
+ startSessionSweeper();
303
+
304
+ // Graceful shutdown — close all transports and clear sessions
294
305
  process.on("SIGINT", async () => {
306
+ console.log(`[HTTP] Shutting down — closing ${Object.keys(transports).length} transport(s)...`);
295
307
  for (const sid of Object.keys(transports)) {
296
308
  try {
297
309
  await transports[sid].close();
package/src/server.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  CallToolRequestSchema,
11
11
  ListToolsRequestSchema,
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
- import { tools, handleToolCall } from "./tools.js";
13
+ import { tools, coreTools, handleToolCall } from "./tools.js";
14
14
 
15
15
  /**
16
16
  * @param {string} sessionId — identifier for credential isolation.
@@ -18,7 +18,7 @@ import { tools, handleToolCall } from "./tools.js";
18
18
  */
19
19
  export function createServer(sessionId = "stdio") {
20
20
  const server = new Server(
21
- { name: "ateam-mcp", version: "0.2.0" },
21
+ { name: "ateam-mcp", version: "0.3.0" },
22
22
  {
23
23
  capabilities: { tools: {} },
24
24
  instructions: [
@@ -30,7 +30,9 @@ export function createServer(sessionId = "stdio") {
30
30
  }
31
31
  );
32
32
 
33
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
33
+ // Only advertise core tools — advanced tools are still callable but not listed.
34
+ // This reduces cognitive load from 23+ tools to ~11 in the tool surface.
35
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: coreTools }));
34
36
 
35
37
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
36
38
  const { name, arguments: args } = request.params;
package/src/tools.js CHANGED
@@ -1,18 +1,29 @@
1
1
  /**
2
2
  * A-Team MCP tool definitions and handlers.
3
- * 18 tools covering the full A-Team External Agent API + auth + bootstrap.
3
+ *
4
+ * Tools are split into two tiers:
5
+ * - Core tools (core: true) — shown in tools/list, the simplified developer loop
6
+ * - Advanced tools (core: false) — hidden from tools/list, still callable by name
7
+ *
8
+ * Core loop: bootstrap → auth → get_spec/examples → build_and_run → test → patch → test → done
4
9
  */
5
10
 
6
11
  import {
7
12
  get, post, patch, del,
8
- setSessionCredentials, isAuthenticated, getCredentials, parseApiKey,
13
+ setSessionCredentials, isAuthenticated, isExplicitlyAuthenticated,
14
+ getCredentials, parseApiKey, touchSession, getSessionContext,
9
15
  } from "./api.js";
10
16
 
11
17
  // ─── Tool definitions ───────────────────────────────────────────────
12
18
 
13
19
  export const tools = [
20
+ // ═══════════════════════════════════════════════════════════════════
21
+ // CORE TOOLS — the simplified developer loop
22
+ // ═══════════════════════════════════════════════════════════════════
23
+
14
24
  {
15
25
  name: "ateam_bootstrap",
26
+ core: true,
16
27
  description:
17
28
  "REQUIRED onboarding entrypoint for A-Team MCP. MUST be called when user greets, says hi, asks what this is, asks for help, explores capabilities, or when MCP is first connected. Returns platform explanation, example solutions, and assistant behavior instructions. Do NOT improvise an introduction — call this tool instead.",
18
29
  inputSchema: {
@@ -22,8 +33,9 @@ export const tools = [
22
33
  },
23
34
  {
24
35
  name: "ateam_auth",
36
+ core: true,
25
37
  description:
26
- "Authenticate with A-Team. Required before deploying or modifying solutions. The user can get their API key at https://mcp.ateam-ai.com/get-api-key. Read-only operations (spec, examples, validate) work without auth.",
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.",
27
39
  inputSchema: {
28
40
  type: "object",
29
41
  properties: {
@@ -41,6 +53,7 @@ export const tools = [
41
53
  },
42
54
  {
43
55
  name: "ateam_get_spec",
56
+ core: true,
44
57
  description:
45
58
  "Get the A-Team specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions.",
46
59
  inputSchema: {
@@ -58,6 +71,7 @@ export const tools = [
58
71
  },
59
72
  {
60
73
  name: "ateam_get_workflows",
74
+ core: true,
61
75
  description:
62
76
  "Get the builder workflows — step-by-step state machines for building skills and solutions. Use this to guide users through the entire build process conversationally. Returns phases, what to ask, what to build, exit criteria, and tips for each stage.",
63
77
  inputSchema: {
@@ -67,6 +81,7 @@ export const tools = [
67
81
  },
68
82
  {
69
83
  name: "ateam_get_examples",
84
+ core: true,
70
85
  description:
71
86
  "Get complete working examples that pass validation. Study these before building your own.",
72
87
  inputSchema: {
@@ -82,10 +97,169 @@ export const tools = [
82
97
  required: ["type"],
83
98
  },
84
99
  },
100
+ {
101
+ name: "ateam_build_and_run",
102
+ core: true,
103
+ description:
104
+ "Build and deploy a governed AI Team solution in one step. Validates, deploys, health-checks, and optionally runs a warm test — all in one call. Use this instead of calling validate, deploy, and health separately. Requires authentication.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ solution: {
109
+ type: "object",
110
+ description: "Solution architecture — identity, grants, handoffs, routing",
111
+ },
112
+ skills: {
113
+ type: "array",
114
+ items: { type: "object" },
115
+ description: "Array of full skill definitions",
116
+ },
117
+ connectors: {
118
+ type: "array",
119
+ items: { type: "object" },
120
+ description: "Optional: connector metadata (id, name, transport). Entry points auto-detected from mcp_store.",
121
+ },
122
+ mcp_store: {
123
+ type: "object",
124
+ description: "Optional: connector source code files. Key = connector id, value = array of {path, content}.",
125
+ },
126
+ test_message: {
127
+ type: "string",
128
+ description: "Optional: send a test message after deployment to verify the skill works. Returns the full execution result.",
129
+ },
130
+ test_skill_id: {
131
+ type: "string",
132
+ description: "Optional: which skill to test (defaults to the first skill).",
133
+ },
134
+ },
135
+ required: ["solution", "skills"],
136
+ },
137
+ },
138
+ {
139
+ name: "ateam_test_skill",
140
+ core: true,
141
+ description:
142
+ "Send a test message to a deployed skill and get the full execution result. By default waits for completion (up to 60s). Set wait=false for async mode — returns job_id immediately, then poll with ateam_test_status.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ solution_id: {
147
+ type: "string",
148
+ description: "The solution ID",
149
+ },
150
+ skill_id: {
151
+ type: "string",
152
+ description: "The skill ID to test (original or internal ID)",
153
+ },
154
+ message: {
155
+ type: "string",
156
+ description: "The test message to send to the skill",
157
+ },
158
+ wait: {
159
+ type: "boolean",
160
+ description:
161
+ "If true (default), wait for completion. If false, return job_id immediately for polling via ateam_test_status.",
162
+ },
163
+ },
164
+ required: ["solution_id", "skill_id", "message"],
165
+ },
166
+ },
167
+ {
168
+ name: "ateam_patch",
169
+ core: true,
170
+ description:
171
+ "Update a deployed skill or solution, redeploy, and optionally re-test — all in one step. Use this instead of calling update + redeploy separately. Requires authentication.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ solution_id: {
176
+ type: "string",
177
+ description: "The solution ID",
178
+ },
179
+ target: {
180
+ type: "string",
181
+ enum: ["solution", "skill"],
182
+ description: "What to update: 'solution' or 'skill'",
183
+ },
184
+ skill_id: {
185
+ type: "string",
186
+ description: "Required when target is 'skill'",
187
+ },
188
+ updates: {
189
+ type: "object",
190
+ description:
191
+ "The update payload — use dot notation for scalars (e.g. 'problem.statement'), and tools_push/tools_delete/tools_update for array operations",
192
+ },
193
+ test_message: {
194
+ type: "string",
195
+ description: "Optional: re-test the skill after patching. Requires skill_id.",
196
+ },
197
+ },
198
+ required: ["solution_id", "target", "updates"],
199
+ },
200
+ },
201
+ {
202
+ name: "ateam_get_solution",
203
+ core: true,
204
+ description:
205
+ "Read solution state — definition, skills, health, status, or export. Use this to inspect deployed solutions.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ solution_id: {
210
+ type: "string",
211
+ description: "The solution ID",
212
+ },
213
+ view: {
214
+ type: "string",
215
+ enum: ["definition", "skills", "health", "status", "export", "validate", "connectors_health"],
216
+ description:
217
+ "What to read: 'definition' = full solution def, 'skills' = list skills, 'health' = live health check, 'status' = deploy status, 'export' = exportable bundle, 'validate' = re-validate from stored state, 'connectors_health' = connector status",
218
+ },
219
+ skill_id: {
220
+ type: "string",
221
+ description: "Optional: read a specific skill by ID (original or internal)",
222
+ },
223
+ },
224
+ required: ["solution_id", "view"],
225
+ },
226
+ },
227
+ {
228
+ name: "ateam_list_solutions",
229
+ core: true,
230
+ description: "List all solutions deployed in the Skill Builder.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {},
234
+ },
235
+ },
236
+ {
237
+ name: "ateam_delete_solution",
238
+ core: true,
239
+ description:
240
+ "Delete a deployed solution and all its skills from A-Team. Use with caution — this removes the solution from both the Skill Builder and A-Team Core. Useful for cleaning up test solutions or starting fresh.",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ solution_id: {
245
+ type: "string",
246
+ description: "The solution ID to delete",
247
+ },
248
+ },
249
+ required: ["solution_id"],
250
+ },
251
+ },
252
+
253
+ // ═══════════════════════════════════════════════════════════════════
254
+ // ADVANCED TOOLS — hidden from tools/list, still callable by name
255
+ // Use these for manual lifecycle control, debugging, and diagnostics
256
+ // ═══════════════════════════════════════════════════════════════════
257
+
85
258
  {
86
259
  name: "ateam_validate_skill",
260
+ core: false,
87
261
  description:
88
- "Validate a skill definition through the 5-stage A-Team validation pipeline. Part of building a governed AI Team solution. Returns errors and suggestions to fix. Always validate before deploying.",
262
+ "Validate a skill definition through the 5-stage A-Team validation pipeline. Returns errors and suggestions to fix. (Advanced ateam_build_and_run validates automatically.)",
89
263
  inputSchema: {
90
264
  type: "object",
91
265
  properties: {
@@ -99,8 +273,9 @@ export const tools = [
99
273
  },
100
274
  {
101
275
  name: "ateam_validate_solution",
276
+ core: false,
102
277
  description:
103
- "Validate a governed AI Team solution — cross-skill contracts, grant economy, handoffs, and LLM quality scoring. Part of building a governed AI Team solution. Always validate before deploying.",
278
+ "Validate a governed AI Team solution — cross-skill contracts, grant economy, handoffs, and LLM quality scoring. (Advanced ateam_build_and_run validates automatically.)",
104
279
  inputSchema: {
105
280
  type: "object",
106
281
  properties: {
@@ -119,8 +294,9 @@ export const tools = [
119
294
  },
120
295
  {
121
296
  name: "ateam_deploy_solution",
297
+ core: false,
122
298
  description:
123
- "Deploy a governed AI Team solution to A-Team Core — identity, connectors, skills. The Skill Builder auto-generates MCP servers from tool definitions. For connectors with UI or Node.js code, send SOURCE files only via mcp_store the server runs npm install + npm run build automatically (like Vercel/Netlify). Never send dist/ or node_modules/. Always validate first using ateam_validate_solution. Requires authentication (call ateam_auth first if not using env vars).",
299
+ "Deploy a governed AI Team solution to A-Team Core. (Advancedprefer ateam_build_and_run which validates + deploys + health-checks in one step.)",
124
300
  inputSchema: {
125
301
  type: "object",
126
302
  properties: {
@@ -136,12 +312,12 @@ export const tools = [
136
312
  connectors: {
137
313
  type: "array",
138
314
  items: { type: "object" },
139
- description: "Array of connector metadata (id, name, transport). command and args are OPTIONAL when mcp_store provides the code — the system auto-detects the entry point (server.js, index.js, package.json main, server.py) and runtime (node, python3). You can still provide explicit command/args to override auto-detection.",
315
+ description: "Array of connector metadata (id, name, transport). command and args are OPTIONAL when mcp_store provides the code — the system auto-detects the entry point.",
140
316
  },
141
317
  mcp_store: {
142
318
  type: "object",
143
319
  description:
144
- "Optional: connector source code files. Key = connector id, value = array of {path, content}. Send SOURCE files only (server.js, package.json, src/*.jsx, etc.) — the server runs npm install + npm run build automatically. Never include node_modules/ or dist/ folders. Entry point is auto-detected from the uploaded files.",
320
+ "Optional: connector source code files. Key = connector id, value = array of {path, content}.",
145
321
  },
146
322
  },
147
323
  required: ["solution", "skills"],
@@ -149,7 +325,8 @@ export const tools = [
149
325
  },
150
326
  {
151
327
  name: "ateam_deploy_skill",
152
- description: "Deploy a single skill into an existing solution. Requires authentication.",
328
+ core: false,
329
+ description: "Deploy a single skill into an existing solution. (Advanced — use ateam_build_and_run for new solutions.)",
153
330
  inputSchema: {
154
331
  type: "object",
155
332
  properties: {
@@ -167,7 +344,8 @@ export const tools = [
167
344
  },
168
345
  {
169
346
  name: "ateam_deploy_connector",
170
- description: "Deploy a connector — registers in the Skill Builder catalog and connects in A-Team Core. Requires authentication.",
347
+ core: false,
348
+ description: "Deploy a connector — registers in the Skill Builder catalog and connects in A-Team Core. (Advanced.)",
171
349
  inputSchema: {
172
350
  type: "object",
173
351
  properties: {
@@ -181,8 +359,9 @@ export const tools = [
181
359
  },
182
360
  {
183
361
  name: "ateam_upload_connector_files",
362
+ core: false,
184
363
  description:
185
- "Upload source files for a connector's MCP server. Use this INSTEAD of mcp_store in ateam_deploy_solution when the source code is too large to inline. Upload files first (one call per file or a few small files at a time), then deploy the solution without mcp_store, then redeploy. Files are staged and automatically included in the next deploy. Requires authentication.",
364
+ "Upload source files for a connector's MCP server. Use this INSTEAD of mcp_store in ateam_build_and_run when the source code is too large to inline. Upload files first, then build_and_run without mcp_store. (Advanced.)",
186
365
  inputSchema: {
187
366
  type: "object",
188
367
  properties: {
@@ -202,49 +381,17 @@ export const tools = [
202
381
  },
203
382
  required: ["path"],
204
383
  },
205
- description: "Array of files to upload. Each file needs 'path' plus ONE of: 'content' (inline string), 'content_base64' (base64), or 'url' (server fetches it). Send one file per call for large files.",
384
+ description: "Array of files to upload. Each file needs 'path' plus ONE of: 'content' (inline string), 'content_base64' (base64), or 'url' (server fetches it).",
206
385
  },
207
386
  },
208
387
  required: ["connector_id", "files"],
209
388
  },
210
389
  },
211
- {
212
- name: "ateam_list_solutions",
213
- description: "List all solutions deployed in the Skill Builder.",
214
- inputSchema: {
215
- type: "object",
216
- properties: {},
217
- },
218
- },
219
- {
220
- name: "ateam_get_solution",
221
- description:
222
- "Read solution state — definition, skills, health, status, or export. Use this to inspect deployed solutions.",
223
- inputSchema: {
224
- type: "object",
225
- properties: {
226
- solution_id: {
227
- type: "string",
228
- description: "The solution ID",
229
- },
230
- view: {
231
- type: "string",
232
- enum: ["definition", "skills", "health", "status", "export", "validate", "connectors_health"],
233
- description:
234
- "What to read: 'definition' = full solution def, 'skills' = list skills, 'health' = live health check, 'status' = deploy status, 'export' = exportable bundle, 'validate' = re-validate from stored state, 'connectors_health' = connector status",
235
- },
236
- skill_id: {
237
- type: "string",
238
- description: "Optional: read a specific skill by ID (original or internal)",
239
- },
240
- },
241
- required: ["solution_id", "view"],
242
- },
243
- },
244
390
  {
245
391
  name: "ateam_update",
392
+ core: false,
246
393
  description:
247
- "Update a deployed solution or skill incrementally using PATCH. Supports dot notation for scalar fields and _push/_delete/_update for arrays. Requires authentication.",
394
+ "Update a deployed solution or skill incrementally using PATCH. (Advanced prefer ateam_patch which updates + redeploys + tests in one step.)",
248
395
  inputSchema: {
249
396
  type: "object",
250
397
  properties: {
@@ -272,8 +419,9 @@ export const tools = [
272
419
  },
273
420
  {
274
421
  name: "ateam_redeploy",
422
+ core: false,
275
423
  description:
276
- "Re-deploy after making updates. Regenerates MCP servers and pushes to A-Team Core. Requires authentication.",
424
+ "Re-deploy after making updates. (Advanced prefer ateam_patch which updates + redeploys in one step.)",
277
425
  inputSchema: {
278
426
  type: "object",
279
427
  properties: {
@@ -291,8 +439,9 @@ export const tools = [
291
439
  },
292
440
  {
293
441
  name: "ateam_solution_chat",
442
+ core: false,
294
443
  description:
295
- "Send a message to the Solution Bot — an AI assistant that understands your deployed solution and can help with modifications.",
444
+ "Send a message to the Solution Bot — an AI assistant that understands your deployed solution and can help with modifications. (Advanced.)",
296
445
  inputSchema: {
297
446
  type: "object",
298
447
  properties: {
@@ -308,13 +457,11 @@ export const tools = [
308
457
  required: ["solution_id", "message"],
309
458
  },
310
459
  },
311
-
312
- // ─── Developer Tools ────────────────────────────────────────────
313
-
314
460
  {
315
461
  name: "ateam_get_execution_logs",
462
+ core: false,
316
463
  description:
317
- "Get execution logs for a solution — recent jobs with step traces, tool calls, errors, and timing. Essential for debugging what actually happened during skill execution.",
464
+ "Get execution logs for a solution — recent jobs with step traces, tool calls, errors, and timing. Essential for debugging what actually happened during skill execution. (Advanced.)",
318
465
  inputSchema: {
319
466
  type: "object",
320
467
  properties: {
@@ -338,38 +485,11 @@ export const tools = [
338
485
  required: ["solution_id"],
339
486
  },
340
487
  },
341
- {
342
- name: "ateam_test_skill",
343
- description:
344
- "Send a test message to a deployed skill and get the full execution result. By default waits for completion (up to 60s). Set wait=false for async mode — returns job_id immediately, then poll with ateam_test_status.",
345
- inputSchema: {
346
- type: "object",
347
- properties: {
348
- solution_id: {
349
- type: "string",
350
- description: "The solution ID",
351
- },
352
- skill_id: {
353
- type: "string",
354
- description: "The skill ID to test (original or internal ID)",
355
- },
356
- message: {
357
- type: "string",
358
- description: "The test message to send to the skill",
359
- },
360
- wait: {
361
- type: "boolean",
362
- description:
363
- "If true (default), wait for completion. If false, return job_id immediately for polling via ateam_test_status.",
364
- },
365
- },
366
- required: ["solution_id", "skill_id", "message"],
367
- },
368
- },
369
488
  {
370
489
  name: "ateam_test_status",
490
+ core: false,
371
491
  description:
372
- "Poll the progress of an async skill test. Returns iteration count, tool call steps, status, pending questions, and result when done. Uses the same data pipeline as the Job Progress UI.",
492
+ "Poll the progress of an async skill test. Returns iteration count, tool call steps, status, pending questions, and result when done. (Advanced use ateam_test_skill with wait=true for synchronous testing.)",
373
493
  inputSchema: {
374
494
  type: "object",
375
495
  properties: {
@@ -391,8 +511,9 @@ export const tools = [
391
511
  },
392
512
  {
393
513
  name: "ateam_test_abort",
514
+ core: false,
394
515
  description:
395
- "Abort a running skill test. Stops the job execution at the next iteration boundary.",
516
+ "Abort a running skill test. Stops the job execution at the next iteration boundary. (Advanced.)",
396
517
  inputSchema: {
397
518
  type: "object",
398
519
  properties: {
@@ -414,8 +535,9 @@ export const tools = [
414
535
  },
415
536
  {
416
537
  name: "ateam_get_connector_source",
538
+ core: false,
417
539
  description:
418
- "Read the source code of a connector's MCP server. Returns the files that make up the connector implementation.",
540
+ "Read the source code of a connector's MCP server. Returns the files that make up the connector implementation. (Advanced.)",
419
541
  inputSchema: {
420
542
  type: "object",
421
543
  properties: {
@@ -433,8 +555,9 @@ export const tools = [
433
555
  },
434
556
  {
435
557
  name: "ateam_get_metrics",
558
+ core: false,
436
559
  description:
437
- "Get execution metrics — timing, tool stats, bottlenecks, signals, and recommendations. Use job_id for single-job deep analysis, or skill_id for recent history overview.",
560
+ "Get execution metrics — timing, tool stats, bottlenecks, signals, and recommendations. (Advanced.)",
438
561
  inputSchema: {
439
562
  type: "object",
440
563
  properties: {
@@ -456,8 +579,9 @@ export const tools = [
456
579
  },
457
580
  {
458
581
  name: "ateam_diff",
582
+ core: false,
459
583
  description:
460
- "Compare the current Builder definition against what's deployed in ADAS Core. Shows which skills are undeployed, orphaned, or have changed fields. Use skill_id to diff a single skill.",
584
+ "Compare the current Builder definition against what's deployed in ADAS Core. Shows which skills are undeployed, orphaned, or have changed fields. (Advanced.)",
461
585
  inputSchema: {
462
586
  type: "object",
463
587
  properties: {
@@ -473,23 +597,14 @@ export const tools = [
473
597
  required: ["solution_id"],
474
598
  },
475
599
  },
476
- {
477
- name: "ateam_delete_solution",
478
- description:
479
- "Delete a deployed solution and all its skills from A-Team. Use with caution — this removes the solution from both the Skill Builder and A-Team Core. Useful for cleaning up test solutions or starting fresh.",
480
- inputSchema: {
481
- type: "object",
482
- properties: {
483
- solution_id: {
484
- type: "string",
485
- description: "The solution ID to delete",
486
- },
487
- },
488
- required: ["solution_id"],
489
- },
490
- },
491
600
  ];
492
601
 
602
+ /**
603
+ * Core tools — shown in MCP tools/list.
604
+ * Advanced tools are still callable but not advertised.
605
+ */
606
+ export const coreTools = tools.filter(t => t.core !== false);
607
+
493
608
  // ─── Tool handlers ──────────────────────────────────────────────────
494
609
 
495
610
  const SPEC_PATHS = {
@@ -507,16 +622,25 @@ const EXAMPLE_PATHS = {
507
622
  solution: "/spec/examples/solution",
508
623
  };
509
624
 
510
- // Tools that require authentication (write operations)
511
- const WRITE_TOOLS = new Set([
625
+ // Tools that are tenant-aware — require EXPLICIT ateam_auth (env vars alone not enough).
626
+ // This prevents accidental reads/writes to the wrong tenant when env vars are
627
+ // baked into MCP config (e.g., ADAS_TENANT + ADAS_API_KEY in ~/.claude.json).
628
+ // Any tool that touches tenant-specific data (solutions, skills, logs, tests) is here.
629
+ const TENANT_TOOLS = new Set([
630
+ // Write operations
631
+ "ateam_build_and_run",
632
+ "ateam_patch",
512
633
  "ateam_deploy_solution",
513
634
  "ateam_deploy_skill",
514
635
  "ateam_deploy_connector",
515
636
  "ateam_upload_connector_files",
516
637
  "ateam_update",
517
638
  "ateam_redeploy",
639
+ "ateam_delete_solution",
518
640
  "ateam_solution_chat",
519
- // Developer tools (read tenant-specific runtime data)
641
+ // Read operations (tenant-specific data)
642
+ "ateam_list_solutions",
643
+ "ateam_get_solution",
520
644
  "ateam_get_execution_logs",
521
645
  "ateam_test_skill",
522
646
  "ateam_test_status",
@@ -524,9 +648,11 @@ const WRITE_TOOLS = new Set([
524
648
  "ateam_get_connector_source",
525
649
  "ateam_get_metrics",
526
650
  "ateam_diff",
527
- "ateam_delete_solution",
528
651
  ]);
529
652
 
653
+ /** Small delay helper */
654
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
655
+
530
656
  const handlers = {
531
657
  ateam_bootstrap: async () => ({
532
658
  platform_positioning: {
@@ -549,20 +675,28 @@ const handlers = {
549
675
  { name: "Customer Support Operations Team", description: "Multi-role support system with escalation, refund controls, CRM integration" },
550
676
  { name: "Enterprise Compliance Platform", description: "Approval flows, audit logs, policy enforcement" },
551
677
  ],
552
- recommended_flow: [
553
- { step: 1, title: "Clarify the goal", description: "Understand what the user wants their Team to do", suggested_tools: [] },
554
- { step: 2, title: "Generate Team map", description: "Design skills, solution architecture, and connectors", suggested_tools: ["ateam_get_spec", "ateam_get_examples", "ateam_get_workflows"] },
555
- { step: 3, title: "Validate", description: "Run validation before deploying", suggested_tools: ["ateam_validate_skill", "ateam_validate_solution"] },
556
- { step: 4, title: "Deploy", description: "Push the Team to A-Team Core. If connector source code is too large for mcp_store, use ateam_upload_connector_files first (one file per call), then deploy without mcp_store.", suggested_tools: ["ateam_auth", "ateam_upload_connector_files", "ateam_deploy_solution"] },
557
- { step: 5, title: "Iterate", description: "Inspect, update, and redeploy as needed", suggested_tools: ["ateam_get_solution", "ateam_update", "ateam_redeploy", "ateam_solution_chat"] },
558
- { step: 6, title: "Operate & Debug", description: "Test skills (async or sync), poll progress, abort tests, read execution logs, analyze metrics, diff definitions, inspect connector source", suggested_tools: ["ateam_test_skill", "ateam_test_status", "ateam_test_abort", "ateam_get_execution_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"] },
559
- ],
678
+ developer_loop: {
679
+ _note: "This is the recommended build loop. Only 4 steps from definition to running skill.",
680
+ steps: [
681
+ { 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. Optionally include a test_message to verify it works immediately.", tools: ["ateam_build_and_run"] },
683
+ { step: 3, action: "Test", description: "Send test messages to your deployed skill and see the full execution trace.", tools: ["ateam_test_skill"] },
684
+ { step: 4, action: "Iterate", description: "Patch the skill (update + redeploy + re-test in one call), repeat until satisfied.", tools: ["ateam_patch"] },
685
+ ],
686
+ },
560
687
  first_questions: [
561
688
  { id: "goal", question: "What do you want your Team to accomplish?", type: "text" },
562
689
  { id: "domain", question: "Which domain fits best?", type: "enum", options: ["ecommerce", "logistics", "enterprise_ops", "other"] },
563
690
  { id: "systems", question: "Which systems should the Team connect to?", type: "multi_select", options: ["slack", "email", "zendesk", "shopify", "jira", "postgres", "custom_api", "none"] },
564
691
  { id: "security", question: "What environment constraints?", type: "enum", options: ["sandbox", "controlled", "regulated"] },
565
692
  ],
693
+ advanced_tools: {
694
+ _note: "These tools are available but hidden from the default tool list. Call them by name when you need fine-grained control.",
695
+ debugging: ["ateam_get_execution_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"],
696
+ manual_lifecycle: ["ateam_validate_skill", "ateam_validate_solution", "ateam_deploy_solution", "ateam_deploy_skill", "ateam_deploy_connector", "ateam_update", "ateam_redeploy"],
697
+ async_testing: ["ateam_test_status", "ateam_test_abort"],
698
+ other: ["ateam_upload_connector_files", "ateam_solution_chat"],
699
+ },
566
700
  static_pages: {
567
701
  features: "https://ateam-ai.com/#features",
568
702
  use_cases: "https://ateam-ai.com/#usecases",
@@ -589,19 +723,20 @@ const handlers = {
589
723
  "Define Skill vs Solution vs Connector",
590
724
  "Ask user what solution they want to build",
591
725
  ],
592
- thinking_order: ["Platform", "Solution", "Skills", "Connectors", "Governance", "Validation", "Deployment"],
726
+ thinking_order: ["Platform", "Solution", "Skills", "Connectors", "Governance", "Build & Run"],
593
727
  tone: "Architectural, enterprise-grade, serious",
594
728
  always: [
595
- "Explain Skill vs Solution vs Connector before deploying anything",
596
- "Validate before deploy",
729
+ "Explain Skill vs Solution vs Connector before building",
730
+ "Use ateam_build_and_run for the full lifecycle (validates automatically)",
731
+ "Use ateam_patch for iterations (updates + redeploys automatically)",
597
732
  "Study the connector example (ateam_get_examples type='connector') before writing connector code",
598
733
  "Ask discovery questions if goal unclear",
599
734
  ],
600
735
  never: [
601
- "Deploy before validation",
736
+ "Call validate + deploy + health separately when ateam_build_and_run does it in one step",
737
+ "Call update + redeploy separately when ateam_patch does it in one step",
602
738
  "Dump raw spec unless requested",
603
- "Write connector code that starts a web server (express, http.createServer, app.listen) — connectors MUST use stdio transport",
604
- "Use HttpServerTransport, SSEServerTransport, or any non-stdio MCP transport",
739
+ "Write connector code that starts a web server — connectors MUST use stdio transport",
605
740
  ],
606
741
  },
607
742
  }),
@@ -637,6 +772,188 @@ const handlers = {
637
772
 
638
773
  ateam_get_examples: async ({ type }, sid) => get(EXAMPLE_PATHS[type], sid),
639
774
 
775
+ // ─── Composite: Build & Run ────────────────────────────────────────
776
+ // Validates → Deploys → Health-checks → Optionally tests
777
+ // One call replaces: validate_solution + deploy_solution + get_solution(health)
778
+
779
+ ateam_build_and_run: async ({ solution, skills, connectors, mcp_store, test_message, test_skill_id }, sid) => {
780
+ const phases = [];
781
+
782
+ // Phase 1: Validate
783
+ let validation;
784
+ try {
785
+ validation = await post("/validate/solution", { solution, skills }, sid);
786
+ phases.push({ phase: "validate", status: "done" });
787
+ } catch (err) {
788
+ return {
789
+ ok: false,
790
+ phase: "validation",
791
+ error: err.message,
792
+ message: "Validation call failed. Check your solution/skill format against the spec (ateam_get_spec topic='solution').",
793
+ };
794
+ }
795
+
796
+ // Check for blocking errors
797
+ const errors = validation.errors || validation.validation?.errors || [];
798
+ if (errors.length > 0) {
799
+ return {
800
+ ok: false,
801
+ phase: "validation",
802
+ errors,
803
+ warnings: validation.warnings || validation.validation?.warnings || [],
804
+ message: `Validation found ${errors.length} error(s). Fix them and try again.`,
805
+ };
806
+ }
807
+
808
+ // Phase 2: Deploy
809
+ let deploy;
810
+ try {
811
+ deploy = await post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid);
812
+ phases.push({ phase: "deploy", status: deploy.ok ? "done" : "failed" });
813
+ } catch (err) {
814
+ return {
815
+ ok: false,
816
+ phase: "deployment",
817
+ phases,
818
+ error: err.message,
819
+ validation_warnings: validation.warnings || [],
820
+ message: "Deployment failed. See error details above.",
821
+ };
822
+ }
823
+
824
+ if (!deploy.ok) {
825
+ return {
826
+ ok: false,
827
+ phase: "deployment",
828
+ phases,
829
+ deploy,
830
+ validation_warnings: validation.warnings || [],
831
+ message: "Deployment returned an error. See deploy details above.",
832
+ };
833
+ }
834
+
835
+ // Phase 3: Health check (with brief wait for propagation)
836
+ let health;
837
+ try {
838
+ await sleep(2000);
839
+ health = await get(`/deploy/solutions/${solution.id}/health`, sid);
840
+ phases.push({ phase: "health", status: "done" });
841
+ } catch (err) {
842
+ health = { error: err.message };
843
+ phases.push({ phase: "health", status: "error", error: err.message });
844
+ }
845
+
846
+ // Phase 4: Warm test (optional)
847
+ let test_result;
848
+ if (test_message) {
849
+ const skillId = test_skill_id || skills?.[0]?.id;
850
+ if (skillId) {
851
+ try {
852
+ test_result = await post(
853
+ `/deploy/solutions/${solution.id}/skills/${skillId}/test`,
854
+ { message: test_message },
855
+ sid,
856
+ { timeoutMs: 90_000 },
857
+ );
858
+ phases.push({ phase: "test", status: "done", skill_id: skillId });
859
+ } catch (err) {
860
+ test_result = { error: err.message };
861
+ phases.push({ phase: "test", status: "error", error: err.message });
862
+ }
863
+ }
864
+ }
865
+
866
+ return {
867
+ ok: true,
868
+ solution_id: solution.id,
869
+ phases,
870
+ deploy: {
871
+ skills_deployed: deploy.import?.skills || [],
872
+ connectors: deploy.import?.connectors || 0,
873
+ ...(deploy.deploy_warnings?.length > 0 && { warnings: deploy.deploy_warnings }),
874
+ ...(deploy.auto_expanded_skills?.length > 0 && { auto_expanded: deploy.auto_expanded_skills }),
875
+ },
876
+ health,
877
+ ...(test_result && { test_result }),
878
+ ...(validation.warnings?.length > 0 && { validation_warnings: validation.warnings }),
879
+ };
880
+ },
881
+
882
+ // ─── Composite: Patch ──────────────────────────────────────────────
883
+ // Updates → Redeploys → Optionally tests
884
+ // One call replaces: ateam_update + ateam_redeploy
885
+
886
+ ateam_patch: async ({ solution_id, target, skill_id, updates, test_message }, sid) => {
887
+ const phases = [];
888
+
889
+ // Phase 1: Apply PATCH
890
+ let patchResult;
891
+ try {
892
+ if (target === "skill") {
893
+ patchResult = await patch(`/deploy/solutions/${solution_id}/skills/${skill_id}`, { updates }, sid);
894
+ } else {
895
+ patchResult = await patch(`/deploy/solutions/${solution_id}`, { state_update: updates }, sid);
896
+ }
897
+ phases.push({ phase: "update", status: "done" });
898
+ } catch (err) {
899
+ return {
900
+ ok: false,
901
+ phase: "update",
902
+ error: err.message,
903
+ message: "Patch failed. Check your updates payload format.",
904
+ };
905
+ }
906
+
907
+ // Phase 2: Redeploy
908
+ let redeployResult;
909
+ try {
910
+ if (target === "skill" && skill_id) {
911
+ redeployResult = await post(`/deploy/solutions/${solution_id}/skills/${skill_id}/redeploy`, {}, sid);
912
+ } else {
913
+ redeployResult = await post(`/deploy/solutions/${solution_id}/redeploy`, {}, sid);
914
+ }
915
+ phases.push({ phase: "redeploy", status: "done" });
916
+ } catch (err) {
917
+ return {
918
+ ok: false,
919
+ phase: "redeploy",
920
+ phases,
921
+ patch: patchResult,
922
+ error: err.message,
923
+ message: "Update succeeded but redeploy failed. Try ateam_redeploy manually.",
924
+ };
925
+ }
926
+
927
+ // Phase 3: Optional re-test
928
+ let test_result;
929
+ if (test_message && skill_id) {
930
+ try {
931
+ await sleep(1000);
932
+ test_result = await post(
933
+ `/deploy/solutions/${solution_id}/skills/${skill_id}/test`,
934
+ { message: test_message },
935
+ sid,
936
+ { timeoutMs: 90_000 },
937
+ );
938
+ phases.push({ phase: "test", status: "done" });
939
+ } catch (err) {
940
+ test_result = { error: err.message };
941
+ phases.push({ phase: "test", status: "error", error: err.message });
942
+ }
943
+ }
944
+
945
+ return {
946
+ ok: true,
947
+ solution_id,
948
+ phases,
949
+ patch: patchResult,
950
+ redeploy: redeployResult,
951
+ ...(test_result && { test_result }),
952
+ };
953
+ },
954
+
955
+ // ─── Original handlers (unchanged) ────────────────────────────────
956
+
640
957
  ateam_validate_skill: async ({ skill }, sid) => post("/validate/skill", { skill }, sid),
641
958
 
642
959
  ateam_validate_solution: async ({ solution, skills }, sid) =>
@@ -820,21 +1137,35 @@ export async function handleToolCall(name, args, sessionId) {
820
1137
  };
821
1138
  }
822
1139
 
823
- // Check auth for write operations
824
- if (WRITE_TOOLS.has(name) && !isAuthenticated(sessionId)) {
1140
+ // Track activity + context on every tool call (keeps session alive, records what user is working on)
1141
+ touchSession(sessionId, {
1142
+ toolName: name,
1143
+ solutionId: args?.solution_id,
1144
+ skillId: args?.skill_id,
1145
+ });
1146
+
1147
+ // Check auth for tenant-aware operations — requires explicit ateam_auth call.
1148
+ // Env vars (ADAS_API_KEY / ADAS_TENANT) are NOT sufficient — they may be
1149
+ // baked into MCP config and silently target the wrong tenant.
1150
+ // Only global/public tools (bootstrap, spec, examples, validate) bypass this.
1151
+ if (TENANT_TOOLS.has(name) && !isExplicitlyAuthenticated(sessionId)) {
1152
+ const hasEnvVars = isAuthenticated(sessionId);
825
1153
  return {
826
1154
  content: [{
827
1155
  type: "text",
828
1156
  text: [
829
- "🔐 Authentication required.",
1157
+ "Authentication required — call ateam_auth first.",
830
1158
  "",
831
- "This tool needs an API key. Please ask the user to:",
1159
+ hasEnvVars
1160
+ ? "Environment variables (ADAS_API_KEY) were detected, but they are not sufficient for tenant-aware operations. You must call ateam_auth explicitly to confirm which tenant you intend to use."
1161
+ : "No authentication found.",
832
1162
  "",
1163
+ "Please ask the user to:",
833
1164
  "1. Get their API key at: https://mcp.ateam-ai.com/get-api-key",
834
1165
  "2. Then call: ateam_auth(api_key: \"<their key>\")",
835
1166
  "",
836
- "The key looks like: adas_<tenant>_<32hex>",
837
- "The tenant is auto-extracted no separate tenant parameter needed.",
1167
+ "The key format is: adas_<tenant>_<32hex> — the tenant is auto-extracted.",
1168
+ "This prevents accidental operations on the wrong tenant from pre-configured env vars.",
838
1169
  ].join("\n"),
839
1170
  }],
840
1171
  isError: true,
@@ -843,6 +1174,20 @@ export async function handleToolCall(name, args, sessionId) {
843
1174
 
844
1175
  try {
845
1176
  const result = await handler(args, sessionId);
1177
+
1178
+ // For ateam_bootstrap, inject session context so the LLM knows what the user was working on
1179
+ if (name === "ateam_bootstrap") {
1180
+ const ctx = getSessionContext(sessionId);
1181
+ if (ctx.activeSolutionId || ctx.lastSkillId) {
1182
+ result.session_context = {
1183
+ _note: "This user has an active session. You can reference their previous work.",
1184
+ active_solution_id: ctx.activeSolutionId || null,
1185
+ last_skill_id: ctx.lastSkillId || null,
1186
+ last_tool_used: ctx.lastToolName || null,
1187
+ };
1188
+ }
1189
+ }
1190
+
846
1191
  return {
847
1192
  content: [{ type: "text", text: formatResult(result, name) }],
848
1193
  };