@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
95 lines (94 loc) • 3.51 kB
JavaScript
/**
* Extract a human-readable error string from an MCP isError result object.
*
* Shared utility — no side effects, no dependencies on other SDK modules —
* so it can be imported from the neurolink.ts event loop, the telemetry
* instrumentation (which loads earlier), and the MCP discovery layer without
* creating circular imports. Any change to truncation or content-type parsing
* must happen here and propagate to all three surfaces.
*/
export function extractMcpErrorText(raw) {
let resultObj;
try {
resultObj = typeof raw === "string" ? JSON.parse(raw) : raw;
}
catch {
return "";
}
if (!resultObj || typeof resultObj !== "object") {
return "";
}
const content = resultObj.content;
if (!Array.isArray(content)) {
return "";
}
// Fail closed on malformed entries (e.g. `content: [null]`) rather than
// throwing — the caller expects an empty string for unparseable input.
const texts = content
.filter((c) => c !== null &&
typeof c === "object" &&
c.type === "text" &&
typeof c.text === "string" &&
c.text.length > 0)
.map((c) => c.text);
return texts.join(" ").substring(0, 500);
}
/**
* MCP tools signal failure by RETURNING `{ isError: true, ... }`, not throwing,
* so execute()'s try/catch never sees it. Returns a capped status message for
* failures (undefined for success) for the caller to set the span error level.
*
* Generic over input shape: accepts either a result object or a JSON-stringified
* envelope (different providers hand back different shapes), mirroring
* `extractMcpErrorText`. A non-JSON string has no `isError` field, so it is
* correctly treated as "not an error" (→ undefined).
*
* Layered on `extractMcpErrorText`: this adds the `isError === true` gate and
* the human-readable "MCP tool returned isError: …" prefix, while the shared
* helper owns the content parsing and the 500-char cap. When `isError` is set
* but no readable text is present, falls back to a generic message.
*/
export function extractMcpToolErrorMessage(result) {
let resultObj = result;
if (typeof resultObj === "string") {
try {
resultObj = JSON.parse(resultObj);
}
catch {
return undefined;
}
}
if (!resultObj || typeof resultObj !== "object") {
return undefined;
}
if (resultObj.isError !== true) {
return undefined;
}
const text = extractMcpErrorText(resultObj);
return text
? `MCP tool returned isError: ${text}`
: "MCP tool returned isError: true";
}
/**
* Detect a tool result that REPORTS failure without throwing, for the native
* loops' consecutive-failure breaker. Covers the two shapes NeuroLink itself
* produces or forwards:
* - MCP CallToolResult with `isError: true` (e.g. proxy-blocked tools) —
* delegated to {@link extractMcpToolErrorMessage}
* - our own `{ error: "..." }` payloads
* Plain strings are never classified (too false-positive-prone).
* Returns the error text, or null when the result looks like a success.
*/
export function extractToolFailureText(result) {
const mcpError = extractMcpToolErrorMessage(result);
if (mcpError) {
return mcpError;
}
if (result !== null &&
typeof result === "object" &&
typeof result.error === "string" &&
result.error.length > 0) {
return result.error;
}
return null;
}