@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.
- package/package.json +1 -1
- package/src/api.js +24 -0
- package/src/tools.js +93 -2
package/package.json
CHANGED
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
|
-
|
|
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
|
|