@ateam-ai/mcp 0.4.48 → 0.4.50

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.48",
3
+ "version": "0.4.50",
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
  //
@@ -3090,8 +3091,23 @@ const handlers = {
3090
3091
  // The pull-bundle endpoint returns mcp_store (files) and solution.platform_connectors
3091
3092
  // (declarations) but not a top-level connectors[] array. The validator/deploy
3092
3093
  // pipeline expects one, so build it from the mcp_store we just pulled.
3094
+ //
3095
+ // ROBUSTNESS: mcp_store is normally keyed by CONNECTOR ID, but a bad/older
3096
+ // pull-bundle can key it by the full `connectors/<id>/<file>` path — in
3097
+ // which case the old `map(id => ...)` registered ONE connector PER FILE with
3098
+ // the path as its id (observed 2026-08-15: db.connectors got
3099
+ // connectors/expense-tracker-mcp/server.js etc. as rows). Collapse either
3100
+ // shape to the connector id and dedupe, so a mis-keyed mcp_store can never
3101
+ // manufacture file-path connectors. (Core also rejects "/"-bearing ids at
3102
+ // its boundary as defense-in-depth.)
3093
3103
  if (!connectors?.length && Object.keys(effectiveMcpStore).length > 0) {
3094
- connectors = Object.keys(effectiveMcpStore).map((id) => ({
3104
+ const connIds = [...new Set(
3105
+ Object.keys(effectiveMcpStore).map((k) => {
3106
+ const m = String(k).match(/^connectors\/([^/]+)\//);
3107
+ return m ? m[1] : k;
3108
+ })
3109
+ )];
3110
+ connectors = connIds.map((id) => ({
3095
3111
  id,
3096
3112
  name: id,
3097
3113
  transport: "stdio",
@@ -5216,6 +5232,10 @@ function summarizeLargeResult(result, toolName) {
5216
5232
  return JSON.stringify(result, null, 2).slice(0, MAX_RESPONSE_CHARS);
5217
5233
  }
5218
5234
 
5235
+ // Failure classification (isError + a machine-readable code, WITHOUT parsing
5236
+ // English) lives in ./mcpFailure.js — imported at the top — so the rule is
5237
+ // unit-testable in isolation (mcpFailure.test.js).
5238
+
5219
5239
  // ─── Dispatcher ─────────────────────────────────────────────────────
5220
5240
 
5221
5241
  export async function handleToolCall(name, args, sessionId) {
@@ -5259,6 +5279,7 @@ export async function handleToolCall(name, args, sessionId) {
5259
5279
  ].join("\n"),
5260
5280
  }],
5261
5281
  isError: true,
5282
+ structuredContent: { ok: false, code: "UNAUTHENTICATED" },
5262
5283
  };
5263
5284
  }
5264
5285
 
@@ -5315,13 +5336,28 @@ export async function handleToolCall(name, args, sessionId) {
5315
5336
  } catch { /* non-fatal — unauthed sessions or API blips shouldn't break bootstrap */ }
5316
5337
  }
5317
5338
 
5339
+ const text = formatResult(result, name);
5340
+ if (isLogicalFailure(result)) {
5341
+ // Logical failure RETURNED (not thrown) — e.g. { ok:false, message:"…
5342
+ // Authentication required" } or an upstream 200-with-auth-text. Flag it
5343
+ // so a caller detects it from isError/code, not by reading the prose.
5344
+ // The sentence stays in content[].text for the reasoning loop.
5345
+ const code = deriveErrorCode(result.message || result.error || text, result.code);
5346
+ return {
5347
+ content: [{ type: "text", text }],
5348
+ isError: true,
5349
+ structuredContent: { ok: false, code },
5350
+ };
5351
+ }
5318
5352
  return {
5319
- content: [{ type: "text", text: formatResult(result, name) }],
5353
+ content: [{ type: "text", text }],
5320
5354
  };
5321
5355
  } catch (err) {
5356
+ const code = deriveErrorCode(err.message, err.code);
5322
5357
  return {
5323
5358
  content: [{ type: "text", text: err.message }],
5324
5359
  isError: true,
5360
+ structuredContent: { ok: false, code },
5325
5361
  };
5326
5362
  }
5327
5363
  }