@ateam-ai/mcp 0.4.47 → 0.4.49

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.4.47",
3
+ "version": "0.4.49",
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",
@@ -0,0 +1,48 @@
1
+ // src/mcpFailure.js
2
+ //
3
+ // Failure classification for the MCP tool dispatcher (used by handleToolCall in
4
+ // tools.js). A tool call can succeed at the TRANSPORT layer (MCP returns a
5
+ // result) while FAILING logically — the classic case being an upstream that
6
+ // answers HTTP 200 with "Authentication required" sitting in the body. MCP's
7
+ // `isError` and a machine-readable `code` are exactly for that gap: they let a
8
+ // caller (e.g. the ateam-proxy connector) ask "did this actually work?" WITHOUT
9
+ // parsing English.
10
+ //
11
+ // The human sentence STAYS in the result's content[].text — the reasoning loop
12
+ // reads it to decide what to do next — we only ADD `isError` +
13
+ // `structuredContent.code` alongside it. Never strip the prose.
14
+ //
15
+ // node --test src/mcpFailure.test.js
16
+
17
+ // This is the ONE boundary where recognizing the auth phrase from text is
18
+ // correct: we translate the upstream lie into a structured code exactly once,
19
+ // here, so nothing downstream ever has to.
20
+ export const AUTH_SIGNAL_RX = /\b(unauthenticated|authentication required|authentication failed|not authenticated|no api_key in session|call ateam_auth|master key required|invalid api key|expired token|401)\b/i;
21
+
22
+ /**
23
+ * Map a failure to a machine-readable code. An explicit code (set by a handler
24
+ * or carried on a thrown error) always wins; otherwise recognize the auth
25
+ * signal at this single boundary; otherwise a generic TOOL_FAILED.
26
+ * @param {string|object} source the message/result the failure came with
27
+ * @param {string} [explicit] a code the handler/error already set
28
+ * @returns {string}
29
+ */
30
+ export function deriveErrorCode(source, explicit) {
31
+ if (explicit && typeof explicit === "string") return explicit;
32
+ const s = typeof source === "string"
33
+ ? source
34
+ : (() => { try { return JSON.stringify(source || ""); } catch { return String(source); } })();
35
+ if (AUTH_SIGNAL_RX.test(s)) return "UNAUTHENTICATED";
36
+ return "TOOL_FAILED";
37
+ }
38
+
39
+ /**
40
+ * A top-level object result with ok:false is a logical failure. Nested *.ok
41
+ * (widget_health.ok / validation.valid) are their own advisory signals and do
42
+ * NOT flip the tool to error — only the tool's OWN primary `ok` does.
43
+ * @param {any} result
44
+ * @returns {boolean}
45
+ */
46
+ export function isLogicalFailure(result) {
47
+ return Boolean(result) && typeof result === "object" && !Array.isArray(result) && result.ok === false;
48
+ }
@@ -0,0 +1,64 @@
1
+ // src/mcpFailure.test.js
2
+ //
3
+ // Proves the dispatcher's failure-classification rule: a logically-failed tool
4
+ // result (returned ok:false, or an upstream 200-with-auth-text) is recognized
5
+ // structurally and mapped to a machine-readable code — so the ateam-proxy never
6
+ // has to read English to know a call failed. Tests the REAL exported functions.
7
+ // node --test src/mcpFailure.test.js
8
+
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { deriveErrorCode, isLogicalFailure, AUTH_SIGNAL_RX } from "./mcpFailure.js";
12
+
13
+ test("auth phrases → UNAUTHENTICATED (the upstream 200-with-auth-text lie)", () => {
14
+ for (const s of [
15
+ "Authentication required — call ateam_auth first.",
16
+ "No api_key in session — call ateam_auth(api_key) first.",
17
+ "Master key required. Call ateam_auth(master_key) first.",
18
+ "Authentication failed: bad key",
19
+ "upstream said 401 Unauthorized",
20
+ "invalid API key",
21
+ ]) {
22
+ assert.equal(deriveErrorCode(s), "UNAUTHENTICATED", `expected UNAUTHENTICATED for: ${s}`);
23
+ }
24
+ });
25
+
26
+ test("non-auth failure → TOOL_FAILED", () => {
27
+ assert.equal(deriveErrorCode("could not read solution definition"), "TOOL_FAILED");
28
+ assert.equal(deriveErrorCode("redeploy timed out"), "TOOL_FAILED");
29
+ });
30
+
31
+ test("an explicit handler/error code always wins over text sniffing", () => {
32
+ // Even auth-looking text must not override a code the handler already set.
33
+ assert.equal(deriveErrorCode("Authentication required", "RATE_LIMITED"), "RATE_LIMITED");
34
+ assert.equal(deriveErrorCode("boom", "MISSING_SOLUTION"), "MISSING_SOLUTION");
35
+ });
36
+
37
+ test("object sources are stringified before matching (not just plain strings)", () => {
38
+ assert.equal(deriveErrorCode({ ok: false, message: "Authentication required" }), "UNAUTHENTICATED");
39
+ assert.equal(deriveErrorCode({ ok: false, error: "disk full" }), "TOOL_FAILED");
40
+ });
41
+
42
+ test("nullish / weird sources never throw, fall back to TOOL_FAILED", () => {
43
+ assert.equal(deriveErrorCode(null), "TOOL_FAILED");
44
+ assert.equal(deriveErrorCode(undefined), "TOOL_FAILED");
45
+ });
46
+
47
+ test("top-level ok:false is a logical failure; ok:true / missing / nested are not", () => {
48
+ assert.equal(isLogicalFailure({ ok: false, message: "x" }), true);
49
+ assert.equal(isLogicalFailure({ ok: true }), false);
50
+ assert.equal(isLogicalFailure({}), false);
51
+ // Nested advisory signals must NOT flip the tool to error (patch succeeded,
52
+ // widget just isn't rendering / def isn't valid yet).
53
+ assert.equal(isLogicalFailure({ ok: true, widget_health: { ok: false } }), false);
54
+ assert.equal(isLogicalFailure({ ok: true, validation: { valid: false } }), false);
55
+ // Arrays / primitives / null are never a logical failure.
56
+ assert.equal(isLogicalFailure([{ ok: false }]), false);
57
+ assert.equal(isLogicalFailure(null), false);
58
+ assert.equal(isLogicalFailure("ok:false"), false);
59
+ });
60
+
61
+ test("AUTH_SIGNAL_RX is exported for reuse and is case-insensitive", () => {
62
+ assert.ok(AUTH_SIGNAL_RX.test("AUTHENTICATION REQUIRED"));
63
+ assert.ok(!AUTH_SIGNAL_RX.test("everything is fine"));
64
+ });
package/src/tools.js CHANGED
@@ -29,6 +29,7 @@ const STAMP_WHERE_TOOLS = new Set([
29
29
  "ateam_verify_surface",
30
30
  ]);
31
31
  import { renderAgentDocHeader, mergeAgentDoc, AGENT_DOC_SENTINEL } from "./agentDoc.js";
32
+ import { deriveErrorCode, isLogicalFailure } from "./mcpFailure.js";
32
33
 
33
34
  // ─── Async deploy helper ────────────────────────────────────────────
34
35
  //
@@ -795,7 +796,8 @@ export const tools = [
795
796
  "- Add a tool: updates: { \"tools_push\": [{ name: \"conn.tool\", description: \"...\", inputs: [...], output: {...} }] }\n" +
796
797
  "- Change intent: updates: { \"intents.supported_update\": [{ id: \"i1\", description: \"new desc\" }] }\n" +
797
798
  "- CREATE a new skill: target='skill', skill_id='my-new-skill', updates: { \"problem.statement\": \"...\", \"role.persona\": \"...\" } — auto-scaffolded and added to solution topology.\n\n" +
798
- "PREVIEW BEFORE WRITING: pass dry_run:true to see the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids) without applying. Use this before any destructive-looking edit.",
799
+ "PREVIEW BEFORE WRITING: pass dry_run:true to see the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids) without applying. Use this before any destructive-looking edit.\n\n" +
800
+ "VERDICT (skill target): the response carries a NON-BLOCKING `validation` block { skill_id, valid, ready_to_export, error_count, incomplete_sections[], unresolved_refs } — the patch always saves even if the def is now invalid, so CHECK valid: false and fix incomplete_sections before relying on it (build_and_run will refuse to deploy an invalid skill). error_count can include auto-import connector-tool artifacts, so act on incomplete_sections first.",
799
801
  inputSchema: {
800
802
  type: "object",
801
803
  properties: {
@@ -3831,6 +3833,46 @@ const handlers = {
3831
3833
  } catch { /* advisory — never downgrade a successful patch on the health check */ }
3832
3834
  }
3833
3835
 
3836
+ // Phase 5: validation verdict (skill target only). NON-BLOCKING — "silent is
3837
+ // the bug": a patch that leaves the definition invalid used to return ok:true
3838
+ // with NO verdict, so an agent (or a persona routing here as the "cheapest
3839
+ // correct tool") never saw it went red, and an invalid def slipped toward
3840
+ // Core (build_and_run refuses on errors, but the patch path reported nothing).
3841
+ // Report the verdict keyed by skill_id; never block. error_count can be
3842
+ // inflated by auto-imported connector tools (INVALID_TOOL_INPUTS /
3843
+ // MISSING_TOOL_OUTPUT fire on every solution because Core resolves their
3844
+ // contract at deploy, not the author) — so lead with the author-facing
3845
+ // signal: which sections are still incomplete, plus any unresolved refs.
3846
+ let validation = null;
3847
+ if (target === "skill" && skill_id) {
3848
+ try {
3849
+ const vr = await get(`/deploy/solutions/${solution_id}/skills/${encodeURIComponent(skill_id)}/validation`, sid);
3850
+ const v = vr?.validation || vr || {};
3851
+ const incomplete_sections = Object.entries(v.sections || {})
3852
+ .filter(([, s]) => s && s.complete === false)
3853
+ .map(([name]) => name);
3854
+ const unresolved = Object.entries(v.unresolved_refs || {}).filter(([, n]) => Number(n) > 0);
3855
+ validation = {
3856
+ skill_id,
3857
+ valid: v.valid === true,
3858
+ ready_to_export: v.ready_to_export === true,
3859
+ error_count: v.error_count ?? null,
3860
+ warning_count: v.warning_count ?? null,
3861
+ ...(incomplete_sections.length && { incomplete_sections }),
3862
+ ...(unresolved.length && { unresolved_refs: Object.fromEntries(unresolved) }),
3863
+ ...(v.error_count > 0 && {
3864
+ _note: `error_count can include auto-import connector-tool artifacts — INVALID_TOOL_INPUTS / MISSING_TOOL_OUTPUT fire on every solution because Core resolves an auto-imported tool's contract at deploy, not the author. Act on incomplete_sections${unresolved.length ? " + unresolved_refs" : ""} first.`,
3865
+ }),
3866
+ ...(v.valid === false && {
3867
+ _verdict: `Skill "${skill_id}" is INVALID — the patch was still saved + redeployed (non-blocking), but build_and_run will REFUSE to deploy while errors stand. Fix the above, then re-check.`,
3868
+ }),
3869
+ };
3870
+ } catch { /* advisory — a validation hiccup never downgrades a saved patch */ }
3871
+ }
3872
+ const validationStatus = (validation && validation.valid === false)
3873
+ ? ` ⚠️ Skill "${skill_id}" is INVALID (${validation.error_count ?? "?"} error(s)) — see validation (non-blocking; build_and_run will refuse until fixed).`
3874
+ : "";
3875
+
3834
3876
  return {
3835
3877
  ok: true,
3836
3878
  solution_id,
@@ -3860,13 +3902,14 @@ const handlers = {
3860
3902
  }),
3861
3903
  ...(isNewSkill && { created_skill: skill_id }),
3862
3904
  ...(redeployResult && { redeploy: redeployResult }),
3905
+ ...(validation && { validation }),
3863
3906
  ...(widget_health && { widget_health }),
3864
3907
  ...(test_result && { test_result }),
3865
- _status: redeployOk
3908
+ _status: (redeployOk
3866
3909
  ? (widget_health && !widget_health.ok
3867
3910
  ? `✅ Patched on ${store} + redeployed. ⚠️ ${widget_health.issues?.length || 0} widget(s) not rendering — see widget_health.`
3868
3911
  : `✅ Patched on ${store} + redeployed.`)
3869
- : `⚠️ Patched on ${store} ✅ but redeploy timed out. Run: ateam_redeploy(solution_id` + (skill_id ? `, skill_id: "${skill_id}"` : '') + ')',
3912
+ : `⚠️ Patched on ${store} ✅ but redeploy timed out. Run: ateam_redeploy(solution_id` + (skill_id ? `, skill_id: "${skill_id}"` : '') + ')') + validationStatus,
3870
3913
  _next: isLocal
3871
3914
  ? 'Local edit saved + redeployed. When the tenant connects a GitHub repo, the local state is pushed → GitHub (which then becomes master).'
3872
3915
  : 'Create a checkpoint before making more changes: ateam_github_promote(solution_id)',
@@ -5174,6 +5217,10 @@ function summarizeLargeResult(result, toolName) {
5174
5217
  return JSON.stringify(result, null, 2).slice(0, MAX_RESPONSE_CHARS);
5175
5218
  }
5176
5219
 
5220
+ // Failure classification (isError + a machine-readable code, WITHOUT parsing
5221
+ // English) lives in ./mcpFailure.js — imported at the top — so the rule is
5222
+ // unit-testable in isolation (mcpFailure.test.js).
5223
+
5177
5224
  // ─── Dispatcher ─────────────────────────────────────────────────────
5178
5225
 
5179
5226
  export async function handleToolCall(name, args, sessionId) {
@@ -5217,6 +5264,7 @@ export async function handleToolCall(name, args, sessionId) {
5217
5264
  ].join("\n"),
5218
5265
  }],
5219
5266
  isError: true,
5267
+ structuredContent: { ok: false, code: "UNAUTHENTICATED" },
5220
5268
  };
5221
5269
  }
5222
5270
 
@@ -5273,13 +5321,28 @@ export async function handleToolCall(name, args, sessionId) {
5273
5321
  } catch { /* non-fatal — unauthed sessions or API blips shouldn't break bootstrap */ }
5274
5322
  }
5275
5323
 
5324
+ const text = formatResult(result, name);
5325
+ if (isLogicalFailure(result)) {
5326
+ // Logical failure RETURNED (not thrown) — e.g. { ok:false, message:"…
5327
+ // Authentication required" } or an upstream 200-with-auth-text. Flag it
5328
+ // so a caller detects it from isError/code, not by reading the prose.
5329
+ // The sentence stays in content[].text for the reasoning loop.
5330
+ const code = deriveErrorCode(result.message || result.error || text, result.code);
5331
+ return {
5332
+ content: [{ type: "text", text }],
5333
+ isError: true,
5334
+ structuredContent: { ok: false, code },
5335
+ };
5336
+ }
5276
5337
  return {
5277
- content: [{ type: "text", text: formatResult(result, name) }],
5338
+ content: [{ type: "text", text }],
5278
5339
  };
5279
5340
  } catch (err) {
5341
+ const code = deriveErrorCode(err.message, err.code);
5280
5342
  return {
5281
5343
  content: [{ type: "text", text: err.message }],
5282
5344
  isError: true,
5345
+ structuredContent: { ok: false, code },
5283
5346
  };
5284
5347
  }
5285
5348
  }