@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 +1 -1
- package/src/mcpFailure.js +48 -0
- package/src/mcpFailure.test.js +64 -0
- package/src/tools.js +38 -2
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|