@ateam-ai/mcp 0.4.76 → 0.4.78

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/api.js +24 -0
  3. package/src/tools.js +93 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.76",
3
+ "version": "0.4.78",
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
@@ -487,6 +487,30 @@ function formatError(method, path, status, body, baseUrl) {
487
487
  503: "A-Team API is temporarily unavailable. Try again in a minute.",
488
488
  };
489
489
 
490
+ // A 401 IS NOT ALWAYS AN AUTH PROBLEM, AND SAYING SO COSTS A RUN.
491
+ //
492
+ // The table below attaches "your API key may be invalid or expired — get a
493
+ // new key and call ateam_auth" to EVERY 401, by status code, never by cause.
494
+ // Core also returns 401 for `Actor "X" not found`, where the key is perfectly
495
+ // valid and the actor is the problem. A PROD agent believed that hint this
496
+ // morning, stopped, and asked the admin to paste an API key — for an error
497
+ // that had nothing to do with keys. An error naming the wrong remedy does not
498
+ // just fail; it sends someone competent in the wrong direction.
499
+ if (status === 401) {
500
+ const t = typeof body === "string"
501
+ ? body
502
+ : (() => { try { return JSON.stringify(body || ""); } catch { return ""; } })();
503
+ const m = /Actor\s+\\?"([^"\\]+)\\?"\s+not found|unknown actor\s+\\?"?([^"\\,}]+)/i.exec(t);
504
+ if (m) {
505
+ const who = (m[1] || m[2] || "").trim();
506
+ hints[401] =
507
+ `NOT an auth problem — your key is fine. Core does not recognise the ACTOR "${who}" in this tenant. ` +
508
+ `Re-authenticating will not help. Either pass a real actor id (the one ateam_conversation returned for the thread), ` +
509
+ `or omit the actor entirely to act as the tenant. If you never sent an actor, the session is bound to a stale one: ` +
510
+ `call ateam_auth again to reset the session binding.`;
511
+ }
512
+ }
513
+
490
514
  // A 500 THAT NAMES A MISSING CONFIGURATION IS NOT "TRY AGAIN IN A MINUTE".
491
515
  //
492
516
  // /chat answered 500 {"message":"OPENAI_API_KEY is not set"} and this table
package/src/tools.js CHANGED
@@ -19,6 +19,17 @@ import {
19
19
  // (tenant + the app URL to view the change). ateam-mcp is a PUBLIC MCP used
20
20
  // from non-desktop clients too, so the location must live IN the tool result
21
21
  // — not only in a desktop plugin's SKILL.md. Read-only/global tools skip it.
22
+ // Tools that START a run and therefore MINT the actor that ran it. Only these
23
+ // may rebind the session's actor — see the call site for what accepting it from
24
+ // any result cost.
25
+ const ACTOR_MINTING_TOOLS = new Set([
26
+ "ateam_conversation",
27
+ "ateam_test_skill",
28
+ "ateam_solution_chat",
29
+ "ateam_test_pipeline",
30
+ "ateam_test_voice",
31
+ ]);
32
+
22
33
  const STAMP_WHERE_TOOLS = new Set([
23
34
  "ateam_build_and_run", "ateam_patch", "ateam_upload_connector", "ateam_redeploy",
24
35
  "ateam_create_skill", "ateam_create_connector", "ateam_create_plugin",
@@ -5347,10 +5358,78 @@ const handlers = {
5347
5358
  gaps.push(`solution health unavailable: ${e.message}`);
5348
5359
  }
5349
5360
 
5361
+ // 4. SMOKE CALL — actually invoke a tool the SOLUTION depends on.
5362
+ //
5363
+ // Everything above is tools/list-level: `connected`, `tools > 0`, "renders",
5364
+ // "deployed". All of it stayed green on a clinic connector that answered
5365
+ // tools/list correctly and returned 401 on EVERY storage call, because its
5366
+ // generated client put a PAT in the shared-secret header. The build shipped,
5367
+ // reported healthy, and failed the first time a user touched it. The only
5368
+ // check that could have caught it is calling something.
5369
+ //
5370
+ // Tool names come from the SKILLS, not the connector: the connector
5371
+ // endpoints expose a count and a description but no name (verified live —
5372
+ // /connectors/<id>/tools returns 7 objects that are all {description:""}),
5373
+ // and the tools a skill declares are the ones that actually have to work.
5374
+ // Testing those is a better question than testing an arbitrary one.
5375
+ out.smoke = [];
5376
+ try {
5377
+ const wanted = new Map(); // toolName -> connectorId (first skill that declares it)
5378
+ for (const sk of Array.isArray(out.skills) ? out.skills : []) {
5379
+ if (!sk?.id) continue;
5380
+ const def = await get(`/deploy/solutions/${solution_id}/skills/${sk.id}`, sid).catch(() => null);
5381
+ for (const t of (def?.skill?.tools || def?.tools || [])) {
5382
+ const name = typeof t === "string" ? t : t?.name;
5383
+ if (!name || wanted.has(name)) continue;
5384
+ const conn = (typeof t === "object" && (t?.source?.connector || t?.source?.id)) || null;
5385
+ wanted.set(name, conn);
5386
+ }
5387
+ }
5388
+
5389
+ // READ-SHAPED ONLY. A smoke test must never book an appointment or delete a
5390
+ // row to prove a connector is alive, so a write-shaped name is skipped and
5391
+ // SAID to be skipped rather than quietly passed over.
5392
+ const readish = [...wanted.keys()].filter((n) => /(^|[._])(list|get|today|available|health|status|ping|info|search)([._]|$)/i.test(n));
5393
+ const perConnector = new Map();
5394
+ for (const name of readish) {
5395
+ const conn = wanted.get(name) || (Array.isArray(out.connectors) && out.connectors[0]?.id) || null;
5396
+ if (!conn || perConnector.has(conn)) continue;
5397
+ perConnector.set(conn, name);
5398
+ }
5399
+
5400
+ for (const c of Array.isArray(out.connectors) ? out.connectors : []) {
5401
+ // DELIBERATELY NOT GATED ON c.connected. Connectors are LAZY — one idles
5402
+ // back to sleep between step 1 and here, and the /call endpoint wakes it
5403
+ // on demand. Skipping a sleeping connector would make this check silently
5404
+ // untestable exactly when it is most needed, and "asleep" is not an
5405
+ // answer to "do its calls work?". The call itself is the verdict.
5406
+ const pick = perConnector.get(c.id);
5407
+ if (!pick) {
5408
+ out.smoke.push({ connector: c.id, called: null, note: "no read-shaped tool declared by any skill — NOT smoke-tested" });
5409
+ gaps.push(`connector '${c.id}' was not smoke-tested (no read-shaped tool declared by a skill); tools/list working does NOT prove its calls succeed`);
5410
+ continue;
5411
+ }
5412
+ try {
5413
+ const r = await post(`/deploy/solutions/${solution_id}/connectors/${c.id}/call`, { tool: pick, args: {} }, sid);
5414
+ const failed = r?.ok === false;
5415
+ out.smoke.push({ connector: c.id, called: pick, ok: !failed, ...(failed && { error: String(r?.error || "").slice(0, 200) }) });
5416
+ if (failed) {
5417
+ gaps.push(`connector '${c.id}' lists ${c.tools} tool(s) but CALLING ${pick} failed: ${String(r?.error || "").slice(0, 160)} — tools/list works and real calls do not`);
5418
+ }
5419
+ } catch (e) {
5420
+ out.smoke.push({ connector: c.id, called: pick, ok: false, error: e.message.slice(0, 200) });
5421
+ gaps.push(`connector '${c.id}' smoke call ${pick} errored: ${e.message.slice(0, 160)}`);
5422
+ }
5423
+ }
5424
+ } catch (e) {
5425
+ out.smoke = { error: e.message };
5426
+ gaps.push(`smoke check could not run: ${e.message}`);
5427
+ }
5428
+
5350
5429
  out.gaps = gaps;
5351
5430
  out.ok = gaps.length === 0;
5352
5431
  out._status = out.ok
5353
- ? "✅ Verified live — connectors connected, widgets render, skills deployed."
5432
+ ? "✅ Verified live — connectors connected AND answering real calls, widgets render, skills deployed."
5354
5433
  : `⚠️ ${gaps.length} gap(s): ${gaps.slice(0, 5).join("; ")}${gaps.length > 5 ? " …" : ""}`;
5355
5434
  return out;
5356
5435
  },
@@ -6061,7 +6140,19 @@ export async function handleToolCall(name, args, sessionId) {
6061
6140
  // it on the way out so the follow-up ateam_get_execution_logs /
6062
6141
  // ateam_get_metrics on that very job is not refused for not knowing who ran
6063
6142
  // it — the single most common dead end when debugging a run.
6064
- if (result && typeof result === "object" && result.actor_id) {
6143
+ //
6144
+ // ONLY FROM TOOLS THAT ACTUALLY MINT ONE. This used to accept `actor_id` off
6145
+ // ANY tool's result, so a single unrelated payload carrying that field
6146
+ // silently repointed the whole session — observed on a clean e2e where every
6147
+ // later call 401'd with `Actor "dev" not found`, from an agent that had
6148
+ // never sent an actor at all. A session that binds to a non-existent actor
6149
+ // never recovers on its own, because nothing ever unbinds it.
6150
+ //
6151
+ // The generated-thread-key filter in api.js does not help here: it rejects
6152
+ // test_<ts>_<rand>, and a short literal like "dev" walks straight past it.
6153
+ // An allow-list of minting tools is the honest boundary — "who ran this job"
6154
+ // is knowledge only the tools that START a job possess.
6155
+ if (result && typeof result === "object" && result.actor_id && ACTOR_MINTING_TOOLS.has(name)) {
6065
6156
  touchSession(sessionId, { actorId: result.actor_id });
6066
6157
  }
6067
6158