@ethisyscore/extension-runtime 1.70.0 → 1.71.1
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/dist/host/index.cjs +129 -7
- package/dist/host/index.cjs.map +1 -1
- package/dist/host/index.d.cts +3 -2
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +122 -8
- package/dist/host/index.js.map +1 -1
- package/dist/index.cjs +104 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +97 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-error-CCZQd8Xl.d.cts +118 -0
- package/dist/mcp-error-CCZQd8Xl.d.ts +118 -0
- package/dist/mock-host/cli.cjs +84 -4
- package/dist/mock-host/cli.cjs.map +1 -1
- package/dist/mock-host/cli.d.cts +1 -1
- package/dist/mock-host/cli.d.ts +1 -1
- package/dist/mock-host/cli.js +84 -4
- package/dist/mock-host/cli.js.map +1 -1
- package/dist/plugin/index.cjs +106 -5
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +1 -0
- package/dist/plugin/index.d.ts +1 -0
- package/dist/plugin/index.js +99 -6
- package/dist/plugin/index.js.map +1 -1
- package/dist/{transport-BPwasdNz.d.ts → transport-BuX8gzq1.d.ts} +7 -0
- package/dist/{transport-BbSPAS_N.d.cts → transport-C4xoMWBE.d.cts} +7 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,102 @@
|
|
|
2
2
|
|
|
3
3
|
var protocol = require('@ethisyscore/protocol');
|
|
4
4
|
|
|
5
|
+
// src/index.ts
|
|
6
|
+
|
|
7
|
+
// src/bridge/mcp-error.ts
|
|
8
|
+
var MCP_ERROR_CODES = [
|
|
9
|
+
"unauthorized",
|
|
10
|
+
"forbidden",
|
|
11
|
+
"not_found",
|
|
12
|
+
"invalid_request",
|
|
13
|
+
"conflict",
|
|
14
|
+
"rate_limited",
|
|
15
|
+
"timeout",
|
|
16
|
+
"unavailable",
|
|
17
|
+
"internal"
|
|
18
|
+
];
|
|
19
|
+
function isMcpErrorCode(value) {
|
|
20
|
+
return typeof value === "string" && MCP_ERROR_CODES.includes(value);
|
|
21
|
+
}
|
|
22
|
+
var RETRYABLE = ["rate_limited", "timeout", "unavailable"];
|
|
23
|
+
function isRetryableMcpErrorCode(code) {
|
|
24
|
+
return RETRYABLE.includes(code);
|
|
25
|
+
}
|
|
26
|
+
var McpToolError = class extends Error {
|
|
27
|
+
/** Machine-readable classification. `internal` when the host sent none. */
|
|
28
|
+
code;
|
|
29
|
+
/**
|
|
30
|
+
* Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
|
|
31
|
+
* check because the error can be constructed in one bundle and inspected in another, where
|
|
32
|
+
* `instanceof` compares two different class objects and answers false.
|
|
33
|
+
*/
|
|
34
|
+
isMcpToolError = true;
|
|
35
|
+
constructor(message, code = "internal") {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "McpHostError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
}
|
|
40
|
+
/** True when this failure is worth retrying unchanged. */
|
|
41
|
+
get retryable() {
|
|
42
|
+
return isRetryableMcpErrorCode(this.code);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isMcpToolError(value) {
|
|
46
|
+
return value instanceof McpToolError || value !== null && typeof value === "object" && value.isMcpToolError === true && isMcpErrorCode(value.code);
|
|
47
|
+
}
|
|
48
|
+
function mcpErrorCodeFromHttpStatus(status) {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 401:
|
|
51
|
+
return "unauthorized";
|
|
52
|
+
case 403:
|
|
53
|
+
return "forbidden";
|
|
54
|
+
case 404:
|
|
55
|
+
return "not_found";
|
|
56
|
+
case 400:
|
|
57
|
+
case 422:
|
|
58
|
+
return "invalid_request";
|
|
59
|
+
case 409:
|
|
60
|
+
case 412:
|
|
61
|
+
return "conflict";
|
|
62
|
+
case 429:
|
|
63
|
+
return "rate_limited";
|
|
64
|
+
case 408:
|
|
65
|
+
case 504:
|
|
66
|
+
return "timeout";
|
|
67
|
+
case 502:
|
|
68
|
+
case 503:
|
|
69
|
+
return "unavailable";
|
|
70
|
+
default:
|
|
71
|
+
return "internal";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function classifyHostResponse(response) {
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
if (isMcpErrorCode(response.mcpErrorCode)) {
|
|
79
|
+
return response.mcpErrorCode;
|
|
80
|
+
}
|
|
81
|
+
return typeof response.status === "number" ? mcpErrorCodeFromHttpStatus(response.status) : "internal";
|
|
82
|
+
}
|
|
83
|
+
function classifyHostError(err) {
|
|
84
|
+
if (err === null || typeof err !== "object") {
|
|
85
|
+
return "internal";
|
|
86
|
+
}
|
|
87
|
+
const candidate = err;
|
|
88
|
+
if (isMcpErrorCode(candidate.mcpErrorCode)) {
|
|
89
|
+
return candidate.mcpErrorCode;
|
|
90
|
+
}
|
|
91
|
+
const status = typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
|
|
92
|
+
if (status !== void 0) {
|
|
93
|
+
return mcpErrorCodeFromHttpStatus(status);
|
|
94
|
+
}
|
|
95
|
+
if (candidate.name === "AbortError" || candidate.name === "TimeoutError") {
|
|
96
|
+
return "timeout";
|
|
97
|
+
}
|
|
98
|
+
return "internal";
|
|
99
|
+
}
|
|
100
|
+
|
|
5
101
|
// src/index.ts
|
|
6
102
|
var EXTENSION_RUNTIME_PACKAGE = "@ethisyscore/extension-runtime";
|
|
7
103
|
var BRIDGE = protocol.BRIDGE_VERSION;
|
|
@@ -12,5 +108,13 @@ Object.defineProperty(exports, "BRIDGE_VERSION", {
|
|
|
12
108
|
});
|
|
13
109
|
exports.BRIDGE = BRIDGE;
|
|
14
110
|
exports.EXTENSION_RUNTIME_PACKAGE = EXTENSION_RUNTIME_PACKAGE;
|
|
111
|
+
exports.MCP_ERROR_CODES = MCP_ERROR_CODES;
|
|
112
|
+
exports.McpToolError = McpToolError;
|
|
113
|
+
exports.classifyHostError = classifyHostError;
|
|
114
|
+
exports.classifyHostResponse = classifyHostResponse;
|
|
115
|
+
exports.isMcpErrorCode = isMcpErrorCode;
|
|
116
|
+
exports.isMcpToolError = isMcpToolError;
|
|
117
|
+
exports.isRetryableMcpErrorCode = isRetryableMcpErrorCode;
|
|
118
|
+
exports.mcpErrorCodeFromHttpStatus = mcpErrorCodeFromHttpStatus;
|
|
15
119
|
//# sourceMappingURL=index.cjs.map
|
|
16
120
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["BRIDGE_VERSION"],"mappings":";;;;;AAEO,IAAM,yBAAA,GAA4B;AAClC,IAAM,MAAA,GAASA","file":"index.cjs","sourcesContent":["import { BRIDGE_VERSION } from \"@ethisyscore/protocol\";\n\nexport const EXTENSION_RUNTIME_PACKAGE = \"@ethisyscore/extension-runtime\";\nexport const BRIDGE = BRIDGE_VERSION;\n\n// Re-export the canonical protocol constant under its original name so plugin\n// authors can `import { BRIDGE_VERSION } from \"@ethisyscore/extension-runtime\"`\n// without taking a direct dependency on `@ethisyscore/protocol`. The runtime\n// is the public API surface for plugins; protocol is an internal dep.\nexport { BRIDGE_VERSION };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/bridge/mcp-error.ts","../src/index.ts"],"names":["BRIDGE_VERSION"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * A machine-readable classification for an MCP call that failed, carried across the host/plugin\n * bridge alongside the human-readable message.\n *\n * ## Why this exists\n *\n * The bridge used to reduce every failure to `err.message`, a bare string. A plugin therefore could\n * not tell \"you lack permission for this tool\" from \"the host is unreachable\" without matching on\n * host error prose, and the practical consequence was that plugins swallowed *all* failures into a\n * successful empty result to keep a permission denial from looking like a crash. That turns a\n * transient outage into \"you have no connectors\" - a false statement about the user's data, and one\n * that also suppresses the query layer's retry.\n *\n * A closed vocabulary lets the plugin branch on the one case it wants to tolerate and rethrow the\n * rest.\n *\n * ## Deliberately small, and deliberately not HTTP\n *\n * These are the distinctions a *caller* acts on differently, not a mirror of any status enum. Codes\n * that would prompt the same handling are folded together, because a vocabulary nobody can apply is\n * just a wider surface to get wrong. `unavailable` and `timeout` stay separate only because retry\n * policy differs between them.\n */\nexport type McpErrorCode =\n /** The caller is not authenticated, or its token expired. Re-authentication may fix it. */\n | \"unauthorized\"\n /** Authenticated, but not permitted this tool or resource. Retrying will not help. */\n | \"forbidden\"\n /** The tool, resource, or addressed entity does not exist. */\n | \"not_found\"\n /** The arguments were rejected. A caller bug, or stale client-side validation. */\n | \"invalid_request\"\n /** A concurrency or state conflict - a row version, or a duplicate. */\n | \"conflict\"\n /** Throttled. Retry later, with backoff. */\n | \"rate_limited\"\n /** The call did not complete in time. Safe to retry only if the operation is idempotent. */\n | \"timeout\"\n /** The host or an upstream dependency is down. Retryable. */\n | \"unavailable\"\n /** Anything else, including an unclassifiable failure. The default - never a claim. */\n | \"internal\";\n\n/**\n * Every valid {@link McpErrorCode}. Used to validate a code arriving over the wire: an unknown\n * string is downgraded rather than trusted, so a newer host cannot make an older plugin branch on a\n * code it has never heard of.\n */\nexport const MCP_ERROR_CODES: readonly McpErrorCode[] = [\n \"unauthorized\",\n \"forbidden\",\n \"not_found\",\n \"invalid_request\",\n \"conflict\",\n \"rate_limited\",\n \"timeout\",\n \"unavailable\",\n \"internal\",\n];\n\n/** True when `value` is a code this build understands. */\nexport function isMcpErrorCode(value: unknown): value is McpErrorCode\n{\n return typeof value === \"string\"\n && (MCP_ERROR_CODES as readonly string[]).includes(value);\n}\n\n/**\n * Codes a caller can retry without changing anything about the request. Exposed so retry policy is\n * decided once here rather than re-derived, subtly differently, at each call site.\n *\n * `unauthorized` is absent on purpose: a retry only helps once something else has refreshed the\n * token, which is a different action from retrying.\n */\nconst RETRYABLE: readonly McpErrorCode[] = [\"rate_limited\", \"timeout\", \"unavailable\"];\n\n/** True when the failure is worth retrying as-is. */\nexport function isRetryableMcpErrorCode(code: McpErrorCode): boolean\n{\n return RETRYABLE.includes(code);\n}\n\n/**\n * An MCP call rejected by the host, carrying its {@link McpErrorCode}.\n *\n * `name` stays `\"McpHostError\"` - the string the bridge has always set - so code that matches on the\n * name keeps working. Prefer {@link isMcpToolError}, which survives a name change and works across\n * realm boundaries where `instanceof` does not.\n */\nexport class McpToolError extends Error\n{\n /** Machine-readable classification. `internal` when the host sent none. */\n public readonly code: McpErrorCode;\n\n /**\n * Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype\n * check because the error can be constructed in one bundle and inspected in another, where\n * `instanceof` compares two different class objects and answers false.\n */\n public readonly isMcpToolError = true as const;\n\n public constructor(message: string, code: McpErrorCode = \"internal\")\n {\n super(message);\n this.name = \"McpHostError\";\n this.code = code;\n }\n\n /** True when this failure is worth retrying unchanged. */\n public get retryable(): boolean\n {\n return isRetryableMcpErrorCode(this.code);\n }\n}\n\n/** True when `value` is an {@link McpToolError}, including one from another bundle. */\nexport function isMcpToolError(value: unknown): value is McpToolError\n{\n return value instanceof McpToolError\n || (value !== null\n && typeof value === \"object\"\n && (value as { isMcpToolError?: unknown }).isMcpToolError === true\n && isMcpErrorCode((value as { code?: unknown }).code));\n}\n\n/**\n * Maps an HTTP status onto a code. Split out because host MCP clients are commonly HTTP clients, so\n * a status is the classification most of them already have.\n *\n * Unrecognised statuses (including every 2xx and 3xx, which should not be reaching an error path)\n * yield `internal` rather than a guess.\n */\nexport function mcpErrorCodeFromHttpStatus(status: number): McpErrorCode\n{\n switch (status)\n {\n case 401:\n return \"unauthorized\";\n case 403:\n return \"forbidden\";\n case 404:\n return \"not_found\";\n case 400:\n case 422:\n return \"invalid_request\";\n case 409:\n case 412:\n return \"conflict\";\n case 429:\n return \"rate_limited\";\n case 408:\n case 504:\n return \"timeout\";\n case 502:\n case 503:\n return \"unavailable\";\n default:\n // Everything unmapped, 500 included. `internal` is the honest answer for a status this\n // vocabulary has no distinct handling for - inventing a closer-looking code would tell\n // the caller something the status does not actually say.\n return \"internal\";\n }\n}\n\n/**\n * Derives a code from a host MCP client's RETURNED failure, as opposed to a thrown one.\n *\n * Both paths exist and both have to be covered. A client that throws is handled by\n * {@link classifyHostError}; a client that reports failure as `{ ok: false, error, status }` - the\n * common shape for anything wrapping HTTP - comes through here. Covering only the throw path leaves\n * the majority of real failures arriving as `internal`, which is the same blindness the code was\n * added to remove.\n *\n * Returns `undefined` for a successful response, so a caller can spread it without putting a\n * meaningless code on the happy path.\n */\nexport function classifyHostResponse(response: {\n readonly ok: boolean;\n readonly status?: number;\n readonly mcpErrorCode?: unknown;\n}): McpErrorCode | undefined\n{\n if (response.ok)\n {\n return undefined;\n }\n\n // Same precedence as the thrown path: an explicit code from the client beats the transport's\n // status, and neither is ever inferred from the message.\n if (isMcpErrorCode(response.mcpErrorCode))\n {\n return response.mcpErrorCode;\n }\n\n return typeof response.status === \"number\"\n ? mcpErrorCodeFromHttpStatus(response.status)\n : \"internal\";\n}\n\n/**\n * Derives a code from an arbitrary thrown value, for the host side of the bridge.\n *\n * Precedence, most explicit first:\n *\n * 1. An `mcpErrorCode` property holding a known code. The intended contract: a host MCP client that\n * knows why a call failed says so directly.\n * 2. A numeric `status` / `statusCode`, mapped by {@link mcpErrorCodeFromHttpStatus}. Covers the\n * HTTP clients that already carry one without asking every host to adopt the field above.\n * 3. `name === \"AbortError\"` / `\"TimeoutError\"`, which is how the platform's own aborts surface.\n * 4. `internal`.\n *\n * **Never infers from the message.** Matching prose would make the classification depend on wording\n * nobody treats as a contract, and it would silently reclassify itself the day someone improves an\n * error string. An unclassifiable failure is `internal`, which is honest.\n */\nexport function classifyHostError(err: unknown): McpErrorCode\n{\n if (err === null || typeof err !== \"object\")\n {\n return \"internal\";\n }\n\n const candidate = err as {\n mcpErrorCode?: unknown;\n status?: unknown;\n statusCode?: unknown;\n name?: unknown;\n };\n\n if (isMcpErrorCode(candidate.mcpErrorCode))\n {\n return candidate.mcpErrorCode;\n }\n\n const status = typeof candidate.status === \"number\"\n ? candidate.status\n : typeof candidate.statusCode === \"number\" ? candidate.statusCode : undefined;\n\n if (status !== undefined)\n {\n return mcpErrorCodeFromHttpStatus(status);\n }\n\n if (candidate.name === \"AbortError\" || candidate.name === \"TimeoutError\")\n {\n return \"timeout\";\n }\n\n return \"internal\";\n}\n","import { BRIDGE_VERSION } from \"@ethisyscore/protocol\";\n\nexport const EXTENSION_RUNTIME_PACKAGE = \"@ethisyscore/extension-runtime\";\nexport const BRIDGE = BRIDGE_VERSION;\n\n// Re-export the canonical protocol constant under its original name so plugin\n// authors can `import { BRIDGE_VERSION } from \"@ethisyscore/extension-runtime\"`\n// without taking a direct dependency on `@ethisyscore/protocol`. The runtime\n// is the public API surface for plugins; protocol is an internal dep.\nexport { BRIDGE_VERSION };\n\n// MCP failure classification. Exported from the plugin entry point because the plugin is the side\n// that branches on it, and from the host entry point because a host MCP client can attach\n// `mcpErrorCode` to the errors it throws to control what the plugin sees.\nexport {\n McpToolError,\n isMcpToolError,\n isMcpErrorCode,\n isRetryableMcpErrorCode,\n mcpErrorCodeFromHttpStatus,\n classifyHostError,\n classifyHostResponse,\n MCP_ERROR_CODES,\n type McpErrorCode,\n} from \"./bridge/mcp-error\";\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { BRIDGE_VERSION } from '@ethisyscore/protocol';
|
|
2
|
+
export { M as MCP_ERROR_CODES, a as McpErrorCode, b as McpToolError, c as classifyHostError, d as classifyHostResponse, i as isMcpErrorCode, e as isMcpToolError, f as isRetryableMcpErrorCode, m as mcpErrorCodeFromHttpStatus } from './mcp-error-CCZQd8Xl.cjs';
|
|
2
3
|
|
|
3
4
|
declare const EXTENSION_RUNTIME_PACKAGE = "@ethisyscore/extension-runtime";
|
|
4
5
|
declare const BRIDGE: "2026-06";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { BRIDGE_VERSION } from '@ethisyscore/protocol';
|
|
2
|
+
export { M as MCP_ERROR_CODES, a as McpErrorCode, b as McpToolError, c as classifyHostError, d as classifyHostResponse, i as isMcpErrorCode, e as isMcpToolError, f as isRetryableMcpErrorCode, m as mcpErrorCodeFromHttpStatus } from './mcp-error-CCZQd8Xl.js';
|
|
2
3
|
|
|
3
4
|
declare const EXTENSION_RUNTIME_PACKAGE = "@ethisyscore/extension-runtime";
|
|
4
5
|
declare const BRIDGE: "2026-06";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,106 @@
|
|
|
1
1
|
import { BRIDGE_VERSION } from '@ethisyscore/protocol';
|
|
2
2
|
export { BRIDGE_VERSION } from '@ethisyscore/protocol';
|
|
3
3
|
|
|
4
|
+
// src/index.ts
|
|
5
|
+
|
|
6
|
+
// src/bridge/mcp-error.ts
|
|
7
|
+
var MCP_ERROR_CODES = [
|
|
8
|
+
"unauthorized",
|
|
9
|
+
"forbidden",
|
|
10
|
+
"not_found",
|
|
11
|
+
"invalid_request",
|
|
12
|
+
"conflict",
|
|
13
|
+
"rate_limited",
|
|
14
|
+
"timeout",
|
|
15
|
+
"unavailable",
|
|
16
|
+
"internal"
|
|
17
|
+
];
|
|
18
|
+
function isMcpErrorCode(value) {
|
|
19
|
+
return typeof value === "string" && MCP_ERROR_CODES.includes(value);
|
|
20
|
+
}
|
|
21
|
+
var RETRYABLE = ["rate_limited", "timeout", "unavailable"];
|
|
22
|
+
function isRetryableMcpErrorCode(code) {
|
|
23
|
+
return RETRYABLE.includes(code);
|
|
24
|
+
}
|
|
25
|
+
var McpToolError = class extends Error {
|
|
26
|
+
/** Machine-readable classification. `internal` when the host sent none. */
|
|
27
|
+
code;
|
|
28
|
+
/**
|
|
29
|
+
* Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
|
|
30
|
+
* check because the error can be constructed in one bundle and inspected in another, where
|
|
31
|
+
* `instanceof` compares two different class objects and answers false.
|
|
32
|
+
*/
|
|
33
|
+
isMcpToolError = true;
|
|
34
|
+
constructor(message, code = "internal") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "McpHostError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
/** True when this failure is worth retrying unchanged. */
|
|
40
|
+
get retryable() {
|
|
41
|
+
return isRetryableMcpErrorCode(this.code);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function isMcpToolError(value) {
|
|
45
|
+
return value instanceof McpToolError || value !== null && typeof value === "object" && value.isMcpToolError === true && isMcpErrorCode(value.code);
|
|
46
|
+
}
|
|
47
|
+
function mcpErrorCodeFromHttpStatus(status) {
|
|
48
|
+
switch (status) {
|
|
49
|
+
case 401:
|
|
50
|
+
return "unauthorized";
|
|
51
|
+
case 403:
|
|
52
|
+
return "forbidden";
|
|
53
|
+
case 404:
|
|
54
|
+
return "not_found";
|
|
55
|
+
case 400:
|
|
56
|
+
case 422:
|
|
57
|
+
return "invalid_request";
|
|
58
|
+
case 409:
|
|
59
|
+
case 412:
|
|
60
|
+
return "conflict";
|
|
61
|
+
case 429:
|
|
62
|
+
return "rate_limited";
|
|
63
|
+
case 408:
|
|
64
|
+
case 504:
|
|
65
|
+
return "timeout";
|
|
66
|
+
case 502:
|
|
67
|
+
case 503:
|
|
68
|
+
return "unavailable";
|
|
69
|
+
default:
|
|
70
|
+
return "internal";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function classifyHostResponse(response) {
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
if (isMcpErrorCode(response.mcpErrorCode)) {
|
|
78
|
+
return response.mcpErrorCode;
|
|
79
|
+
}
|
|
80
|
+
return typeof response.status === "number" ? mcpErrorCodeFromHttpStatus(response.status) : "internal";
|
|
81
|
+
}
|
|
82
|
+
function classifyHostError(err) {
|
|
83
|
+
if (err === null || typeof err !== "object") {
|
|
84
|
+
return "internal";
|
|
85
|
+
}
|
|
86
|
+
const candidate = err;
|
|
87
|
+
if (isMcpErrorCode(candidate.mcpErrorCode)) {
|
|
88
|
+
return candidate.mcpErrorCode;
|
|
89
|
+
}
|
|
90
|
+
const status = typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
|
|
91
|
+
if (status !== void 0) {
|
|
92
|
+
return mcpErrorCodeFromHttpStatus(status);
|
|
93
|
+
}
|
|
94
|
+
if (candidate.name === "AbortError" || candidate.name === "TimeoutError") {
|
|
95
|
+
return "timeout";
|
|
96
|
+
}
|
|
97
|
+
return "internal";
|
|
98
|
+
}
|
|
99
|
+
|
|
4
100
|
// src/index.ts
|
|
5
101
|
var EXTENSION_RUNTIME_PACKAGE = "@ethisyscore/extension-runtime";
|
|
6
102
|
var BRIDGE = BRIDGE_VERSION;
|
|
7
103
|
|
|
8
|
-
export { BRIDGE, EXTENSION_RUNTIME_PACKAGE };
|
|
104
|
+
export { BRIDGE, EXTENSION_RUNTIME_PACKAGE, MCP_ERROR_CODES, McpToolError, classifyHostError, classifyHostResponse, isMcpErrorCode, isMcpToolError, isRetryableMcpErrorCode, mcpErrorCodeFromHttpStatus };
|
|
9
105
|
//# sourceMappingURL=index.js.map
|
|
10
106
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AAEO,IAAM,yBAAA,GAA4B;AAClC,IAAM,MAAA,GAAS","file":"index.js","sourcesContent":["import { BRIDGE_VERSION } from \"@ethisyscore/protocol\";\n\nexport const EXTENSION_RUNTIME_PACKAGE = \"@ethisyscore/extension-runtime\";\nexport const BRIDGE = BRIDGE_VERSION;\n\n// Re-export the canonical protocol constant under its original name so plugin\n// authors can `import { BRIDGE_VERSION } from \"@ethisyscore/extension-runtime\"`\n// without taking a direct dependency on `@ethisyscore/protocol`. The runtime\n// is the public API surface for plugins; protocol is an internal dep.\nexport { BRIDGE_VERSION };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/bridge/mcp-error.ts","../src/index.ts"],"names":[],"mappings":";;;;;;AAgDO,IAAM,eAAA,GAA2C;AAAA,EACpD,cAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,iBAAA;AAAA,EACA,UAAA;AAAA,EACA,cAAA;AAAA,EACA,SAAA;AAAA,EACA,aAAA;AAAA,EACA;AACJ;AAGO,SAAS,eAAe,KAAA,EAC/B;AACI,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAChB,eAAA,CAAsC,SAAS,KAAK,CAAA;AAChE;AASA,IAAM,SAAA,GAAqC,CAAC,cAAA,EAAgB,SAAA,EAAW,aAAa,CAAA;AAG7E,SAAS,wBAAwB,IAAA,EACxC;AACI,EAAA,OAAO,SAAA,CAAU,SAAS,IAAI,CAAA;AAClC;AASO,IAAM,YAAA,GAAN,cAA2B,KAAA,CAClC;AAAA;AAAA,EAEoB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAA,GAAiB,IAAA;AAAA,EAE1B,WAAA,CAAY,OAAA,EAAiB,IAAA,GAAqB,UAAA,EACzD;AACI,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EAChB;AAAA;AAAA,EAGA,IAAW,SAAA,GACX;AACI,IAAA,OAAO,uBAAA,CAAwB,KAAK,IAAI,CAAA;AAAA,EAC5C;AACJ;AAGO,SAAS,eAAe,KAAA,EAC/B;AACI,EAAA,OAAO,KAAA,YAAiB,YAAA,IAChB,KAAA,KAAU,IAAA,IACP,OAAO,KAAA,KAAU,QAAA,IAChB,KAAA,CAAuC,cAAA,KAAmB,IAAA,IAC3D,cAAA,CAAgB,KAAA,CAA6B,IAAI,CAAA;AAChE;AASO,SAAS,2BAA2B,MAAA,EAC3C;AACI,EAAA,QAAQ,MAAA;AACR,IACI,KAAK,GAAA;AACD,MAAA,OAAO,cAAA;AAAA,IACX,KAAK,GAAA;AACD,MAAA,OAAO,WAAA;AAAA,IACX,KAAK,GAAA;AACD,MAAA,OAAO,WAAA;AAAA,IACX,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACD,MAAA,OAAO,iBAAA;AAAA,IACX,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACD,MAAA,OAAO,UAAA;AAAA,IACX,KAAK,GAAA;AACD,MAAA,OAAO,cAAA;AAAA,IACX,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACD,MAAA,OAAO,SAAA;AAAA,IACX,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACD,MAAA,OAAO,aAAA;AAAA,IACX;AAII,MAAA,OAAO,UAAA;AAAA;AAEnB;AAcO,SAAS,qBAAqB,QAAA,EAKrC;AACI,EAAA,IAAI,SAAS,EAAA,EACb;AACI,IAAA,OAAO,MAAA;AAAA,EACX;AAIA,EAAA,IAAI,cAAA,CAAe,QAAA,CAAS,YAAY,CAAA,EACxC;AACI,IAAA,OAAO,QAAA,CAAS,YAAA;AAAA,EACpB;AAEA,EAAA,OAAO,OAAO,QAAA,CAAS,MAAA,KAAW,WAC5B,0BAAA,CAA2B,QAAA,CAAS,MAAM,CAAA,GAC1C,UAAA;AACV;AAkBO,SAAS,kBAAkB,GAAA,EAClC;AACI,EAAA,IAAI,GAAA,KAAQ,IAAA,IAAQ,OAAO,GAAA,KAAQ,QAAA,EACnC;AACI,IAAA,OAAO,UAAA;AAAA,EACX;AAEA,EAAA,MAAM,SAAA,GAAY,GAAA;AAOlB,EAAA,IAAI,cAAA,CAAe,SAAA,CAAU,YAAY,CAAA,EACzC;AACI,IAAA,OAAO,SAAA,CAAU,YAAA;AAAA,EACrB;AAEA,EAAA,MAAM,MAAA,GAAS,OAAO,SAAA,CAAU,MAAA,KAAW,QAAA,GACrC,SAAA,CAAU,MAAA,GACV,OAAO,SAAA,CAAU,UAAA,KAAe,QAAA,GAAW,SAAA,CAAU,UAAA,GAAa,MAAA;AAExE,EAAA,IAAI,WAAW,MAAA,EACf;AACI,IAAA,OAAO,2BAA2B,MAAM,CAAA;AAAA,EAC5C;AAEA,EAAA,IAAI,SAAA,CAAU,IAAA,KAAS,YAAA,IAAgB,SAAA,CAAU,SAAS,cAAA,EAC1D;AACI,IAAA,OAAO,SAAA;AAAA,EACX;AAEA,EAAA,OAAO,UAAA;AACX;;;ACvPO,IAAM,yBAAA,GAA4B;AAClC,IAAM,MAAA,GAAS","file":"index.js","sourcesContent":["/**\n * A machine-readable classification for an MCP call that failed, carried across the host/plugin\n * bridge alongside the human-readable message.\n *\n * ## Why this exists\n *\n * The bridge used to reduce every failure to `err.message`, a bare string. A plugin therefore could\n * not tell \"you lack permission for this tool\" from \"the host is unreachable\" without matching on\n * host error prose, and the practical consequence was that plugins swallowed *all* failures into a\n * successful empty result to keep a permission denial from looking like a crash. That turns a\n * transient outage into \"you have no connectors\" - a false statement about the user's data, and one\n * that also suppresses the query layer's retry.\n *\n * A closed vocabulary lets the plugin branch on the one case it wants to tolerate and rethrow the\n * rest.\n *\n * ## Deliberately small, and deliberately not HTTP\n *\n * These are the distinctions a *caller* acts on differently, not a mirror of any status enum. Codes\n * that would prompt the same handling are folded together, because a vocabulary nobody can apply is\n * just a wider surface to get wrong. `unavailable` and `timeout` stay separate only because retry\n * policy differs between them.\n */\nexport type McpErrorCode =\n /** The caller is not authenticated, or its token expired. Re-authentication may fix it. */\n | \"unauthorized\"\n /** Authenticated, but not permitted this tool or resource. Retrying will not help. */\n | \"forbidden\"\n /** The tool, resource, or addressed entity does not exist. */\n | \"not_found\"\n /** The arguments were rejected. A caller bug, or stale client-side validation. */\n | \"invalid_request\"\n /** A concurrency or state conflict - a row version, or a duplicate. */\n | \"conflict\"\n /** Throttled. Retry later, with backoff. */\n | \"rate_limited\"\n /** The call did not complete in time. Safe to retry only if the operation is idempotent. */\n | \"timeout\"\n /** The host or an upstream dependency is down. Retryable. */\n | \"unavailable\"\n /** Anything else, including an unclassifiable failure. The default - never a claim. */\n | \"internal\";\n\n/**\n * Every valid {@link McpErrorCode}. Used to validate a code arriving over the wire: an unknown\n * string is downgraded rather than trusted, so a newer host cannot make an older plugin branch on a\n * code it has never heard of.\n */\nexport const MCP_ERROR_CODES: readonly McpErrorCode[] = [\n \"unauthorized\",\n \"forbidden\",\n \"not_found\",\n \"invalid_request\",\n \"conflict\",\n \"rate_limited\",\n \"timeout\",\n \"unavailable\",\n \"internal\",\n];\n\n/** True when `value` is a code this build understands. */\nexport function isMcpErrorCode(value: unknown): value is McpErrorCode\n{\n return typeof value === \"string\"\n && (MCP_ERROR_CODES as readonly string[]).includes(value);\n}\n\n/**\n * Codes a caller can retry without changing anything about the request. Exposed so retry policy is\n * decided once here rather than re-derived, subtly differently, at each call site.\n *\n * `unauthorized` is absent on purpose: a retry only helps once something else has refreshed the\n * token, which is a different action from retrying.\n */\nconst RETRYABLE: readonly McpErrorCode[] = [\"rate_limited\", \"timeout\", \"unavailable\"];\n\n/** True when the failure is worth retrying as-is. */\nexport function isRetryableMcpErrorCode(code: McpErrorCode): boolean\n{\n return RETRYABLE.includes(code);\n}\n\n/**\n * An MCP call rejected by the host, carrying its {@link McpErrorCode}.\n *\n * `name` stays `\"McpHostError\"` - the string the bridge has always set - so code that matches on the\n * name keeps working. Prefer {@link isMcpToolError}, which survives a name change and works across\n * realm boundaries where `instanceof` does not.\n */\nexport class McpToolError extends Error\n{\n /** Machine-readable classification. `internal` when the host sent none. */\n public readonly code: McpErrorCode;\n\n /**\n * Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype\n * check because the error can be constructed in one bundle and inspected in another, where\n * `instanceof` compares two different class objects and answers false.\n */\n public readonly isMcpToolError = true as const;\n\n public constructor(message: string, code: McpErrorCode = \"internal\")\n {\n super(message);\n this.name = \"McpHostError\";\n this.code = code;\n }\n\n /** True when this failure is worth retrying unchanged. */\n public get retryable(): boolean\n {\n return isRetryableMcpErrorCode(this.code);\n }\n}\n\n/** True when `value` is an {@link McpToolError}, including one from another bundle. */\nexport function isMcpToolError(value: unknown): value is McpToolError\n{\n return value instanceof McpToolError\n || (value !== null\n && typeof value === \"object\"\n && (value as { isMcpToolError?: unknown }).isMcpToolError === true\n && isMcpErrorCode((value as { code?: unknown }).code));\n}\n\n/**\n * Maps an HTTP status onto a code. Split out because host MCP clients are commonly HTTP clients, so\n * a status is the classification most of them already have.\n *\n * Unrecognised statuses (including every 2xx and 3xx, which should not be reaching an error path)\n * yield `internal` rather than a guess.\n */\nexport function mcpErrorCodeFromHttpStatus(status: number): McpErrorCode\n{\n switch (status)\n {\n case 401:\n return \"unauthorized\";\n case 403:\n return \"forbidden\";\n case 404:\n return \"not_found\";\n case 400:\n case 422:\n return \"invalid_request\";\n case 409:\n case 412:\n return \"conflict\";\n case 429:\n return \"rate_limited\";\n case 408:\n case 504:\n return \"timeout\";\n case 502:\n case 503:\n return \"unavailable\";\n default:\n // Everything unmapped, 500 included. `internal` is the honest answer for a status this\n // vocabulary has no distinct handling for - inventing a closer-looking code would tell\n // the caller something the status does not actually say.\n return \"internal\";\n }\n}\n\n/**\n * Derives a code from a host MCP client's RETURNED failure, as opposed to a thrown one.\n *\n * Both paths exist and both have to be covered. A client that throws is handled by\n * {@link classifyHostError}; a client that reports failure as `{ ok: false, error, status }` - the\n * common shape for anything wrapping HTTP - comes through here. Covering only the throw path leaves\n * the majority of real failures arriving as `internal`, which is the same blindness the code was\n * added to remove.\n *\n * Returns `undefined` for a successful response, so a caller can spread it without putting a\n * meaningless code on the happy path.\n */\nexport function classifyHostResponse(response: {\n readonly ok: boolean;\n readonly status?: number;\n readonly mcpErrorCode?: unknown;\n}): McpErrorCode | undefined\n{\n if (response.ok)\n {\n return undefined;\n }\n\n // Same precedence as the thrown path: an explicit code from the client beats the transport's\n // status, and neither is ever inferred from the message.\n if (isMcpErrorCode(response.mcpErrorCode))\n {\n return response.mcpErrorCode;\n }\n\n return typeof response.status === \"number\"\n ? mcpErrorCodeFromHttpStatus(response.status)\n : \"internal\";\n}\n\n/**\n * Derives a code from an arbitrary thrown value, for the host side of the bridge.\n *\n * Precedence, most explicit first:\n *\n * 1. An `mcpErrorCode` property holding a known code. The intended contract: a host MCP client that\n * knows why a call failed says so directly.\n * 2. A numeric `status` / `statusCode`, mapped by {@link mcpErrorCodeFromHttpStatus}. Covers the\n * HTTP clients that already carry one without asking every host to adopt the field above.\n * 3. `name === \"AbortError\"` / `\"TimeoutError\"`, which is how the platform's own aborts surface.\n * 4. `internal`.\n *\n * **Never infers from the message.** Matching prose would make the classification depend on wording\n * nobody treats as a contract, and it would silently reclassify itself the day someone improves an\n * error string. An unclassifiable failure is `internal`, which is honest.\n */\nexport function classifyHostError(err: unknown): McpErrorCode\n{\n if (err === null || typeof err !== \"object\")\n {\n return \"internal\";\n }\n\n const candidate = err as {\n mcpErrorCode?: unknown;\n status?: unknown;\n statusCode?: unknown;\n name?: unknown;\n };\n\n if (isMcpErrorCode(candidate.mcpErrorCode))\n {\n return candidate.mcpErrorCode;\n }\n\n const status = typeof candidate.status === \"number\"\n ? candidate.status\n : typeof candidate.statusCode === \"number\" ? candidate.statusCode : undefined;\n\n if (status !== undefined)\n {\n return mcpErrorCodeFromHttpStatus(status);\n }\n\n if (candidate.name === \"AbortError\" || candidate.name === \"TimeoutError\")\n {\n return \"timeout\";\n }\n\n return \"internal\";\n}\n","import { BRIDGE_VERSION } from \"@ethisyscore/protocol\";\n\nexport const EXTENSION_RUNTIME_PACKAGE = \"@ethisyscore/extension-runtime\";\nexport const BRIDGE = BRIDGE_VERSION;\n\n// Re-export the canonical protocol constant under its original name so plugin\n// authors can `import { BRIDGE_VERSION } from \"@ethisyscore/extension-runtime\"`\n// without taking a direct dependency on `@ethisyscore/protocol`. The runtime\n// is the public API surface for plugins; protocol is an internal dep.\nexport { BRIDGE_VERSION };\n\n// MCP failure classification. Exported from the plugin entry point because the plugin is the side\n// that branches on it, and from the host entry point because a host MCP client can attach\n// `mcpErrorCode` to the errors it throws to control what the plugin sees.\nexport {\n McpToolError,\n isMcpToolError,\n isMcpErrorCode,\n isRetryableMcpErrorCode,\n mcpErrorCodeFromHttpStatus,\n classifyHostError,\n classifyHostResponse,\n MCP_ERROR_CODES,\n type McpErrorCode,\n} from \"./bridge/mcp-error\";\n"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A machine-readable classification for an MCP call that failed, carried across the host/plugin
|
|
3
|
+
* bridge alongside the human-readable message.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* The bridge used to reduce every failure to `err.message`, a bare string. A plugin therefore could
|
|
8
|
+
* not tell "you lack permission for this tool" from "the host is unreachable" without matching on
|
|
9
|
+
* host error prose, and the practical consequence was that plugins swallowed *all* failures into a
|
|
10
|
+
* successful empty result to keep a permission denial from looking like a crash. That turns a
|
|
11
|
+
* transient outage into "you have no connectors" - a false statement about the user's data, and one
|
|
12
|
+
* that also suppresses the query layer's retry.
|
|
13
|
+
*
|
|
14
|
+
* A closed vocabulary lets the plugin branch on the one case it wants to tolerate and rethrow the
|
|
15
|
+
* rest.
|
|
16
|
+
*
|
|
17
|
+
* ## Deliberately small, and deliberately not HTTP
|
|
18
|
+
*
|
|
19
|
+
* These are the distinctions a *caller* acts on differently, not a mirror of any status enum. Codes
|
|
20
|
+
* that would prompt the same handling are folded together, because a vocabulary nobody can apply is
|
|
21
|
+
* just a wider surface to get wrong. `unavailable` and `timeout` stay separate only because retry
|
|
22
|
+
* policy differs between them.
|
|
23
|
+
*/
|
|
24
|
+
type McpErrorCode =
|
|
25
|
+
/** The caller is not authenticated, or its token expired. Re-authentication may fix it. */
|
|
26
|
+
"unauthorized"
|
|
27
|
+
/** Authenticated, but not permitted this tool or resource. Retrying will not help. */
|
|
28
|
+
| "forbidden"
|
|
29
|
+
/** The tool, resource, or addressed entity does not exist. */
|
|
30
|
+
| "not_found"
|
|
31
|
+
/** The arguments were rejected. A caller bug, or stale client-side validation. */
|
|
32
|
+
| "invalid_request"
|
|
33
|
+
/** A concurrency or state conflict - a row version, or a duplicate. */
|
|
34
|
+
| "conflict"
|
|
35
|
+
/** Throttled. Retry later, with backoff. */
|
|
36
|
+
| "rate_limited"
|
|
37
|
+
/** The call did not complete in time. Safe to retry only if the operation is idempotent. */
|
|
38
|
+
| "timeout"
|
|
39
|
+
/** The host or an upstream dependency is down. Retryable. */
|
|
40
|
+
| "unavailable"
|
|
41
|
+
/** Anything else, including an unclassifiable failure. The default - never a claim. */
|
|
42
|
+
| "internal";
|
|
43
|
+
/**
|
|
44
|
+
* Every valid {@link McpErrorCode}. Used to validate a code arriving over the wire: an unknown
|
|
45
|
+
* string is downgraded rather than trusted, so a newer host cannot make an older plugin branch on a
|
|
46
|
+
* code it has never heard of.
|
|
47
|
+
*/
|
|
48
|
+
declare const MCP_ERROR_CODES: readonly McpErrorCode[];
|
|
49
|
+
/** True when `value` is a code this build understands. */
|
|
50
|
+
declare function isMcpErrorCode(value: unknown): value is McpErrorCode;
|
|
51
|
+
/** True when the failure is worth retrying as-is. */
|
|
52
|
+
declare function isRetryableMcpErrorCode(code: McpErrorCode): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* An MCP call rejected by the host, carrying its {@link McpErrorCode}.
|
|
55
|
+
*
|
|
56
|
+
* `name` stays `"McpHostError"` - the string the bridge has always set - so code that matches on the
|
|
57
|
+
* name keeps working. Prefer {@link isMcpToolError}, which survives a name change and works across
|
|
58
|
+
* realm boundaries where `instanceof` does not.
|
|
59
|
+
*/
|
|
60
|
+
declare class McpToolError extends Error {
|
|
61
|
+
/** Machine-readable classification. `internal` when the host sent none. */
|
|
62
|
+
readonly code: McpErrorCode;
|
|
63
|
+
/**
|
|
64
|
+
* Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
|
|
65
|
+
* check because the error can be constructed in one bundle and inspected in another, where
|
|
66
|
+
* `instanceof` compares two different class objects and answers false.
|
|
67
|
+
*/
|
|
68
|
+
readonly isMcpToolError: true;
|
|
69
|
+
constructor(message: string, code?: McpErrorCode);
|
|
70
|
+
/** True when this failure is worth retrying unchanged. */
|
|
71
|
+
get retryable(): boolean;
|
|
72
|
+
}
|
|
73
|
+
/** True when `value` is an {@link McpToolError}, including one from another bundle. */
|
|
74
|
+
declare function isMcpToolError(value: unknown): value is McpToolError;
|
|
75
|
+
/**
|
|
76
|
+
* Maps an HTTP status onto a code. Split out because host MCP clients are commonly HTTP clients, so
|
|
77
|
+
* a status is the classification most of them already have.
|
|
78
|
+
*
|
|
79
|
+
* Unrecognised statuses (including every 2xx and 3xx, which should not be reaching an error path)
|
|
80
|
+
* yield `internal` rather than a guess.
|
|
81
|
+
*/
|
|
82
|
+
declare function mcpErrorCodeFromHttpStatus(status: number): McpErrorCode;
|
|
83
|
+
/**
|
|
84
|
+
* Derives a code from a host MCP client's RETURNED failure, as opposed to a thrown one.
|
|
85
|
+
*
|
|
86
|
+
* Both paths exist and both have to be covered. A client that throws is handled by
|
|
87
|
+
* {@link classifyHostError}; a client that reports failure as `{ ok: false, error, status }` - the
|
|
88
|
+
* common shape for anything wrapping HTTP - comes through here. Covering only the throw path leaves
|
|
89
|
+
* the majority of real failures arriving as `internal`, which is the same blindness the code was
|
|
90
|
+
* added to remove.
|
|
91
|
+
*
|
|
92
|
+
* Returns `undefined` for a successful response, so a caller can spread it without putting a
|
|
93
|
+
* meaningless code on the happy path.
|
|
94
|
+
*/
|
|
95
|
+
declare function classifyHostResponse(response: {
|
|
96
|
+
readonly ok: boolean;
|
|
97
|
+
readonly status?: number;
|
|
98
|
+
readonly mcpErrorCode?: unknown;
|
|
99
|
+
}): McpErrorCode | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Derives a code from an arbitrary thrown value, for the host side of the bridge.
|
|
102
|
+
*
|
|
103
|
+
* Precedence, most explicit first:
|
|
104
|
+
*
|
|
105
|
+
* 1. An `mcpErrorCode` property holding a known code. The intended contract: a host MCP client that
|
|
106
|
+
* knows why a call failed says so directly.
|
|
107
|
+
* 2. A numeric `status` / `statusCode`, mapped by {@link mcpErrorCodeFromHttpStatus}. Covers the
|
|
108
|
+
* HTTP clients that already carry one without asking every host to adopt the field above.
|
|
109
|
+
* 3. `name === "AbortError"` / `"TimeoutError"`, which is how the platform's own aborts surface.
|
|
110
|
+
* 4. `internal`.
|
|
111
|
+
*
|
|
112
|
+
* **Never infers from the message.** Matching prose would make the classification depend on wording
|
|
113
|
+
* nobody treats as a contract, and it would silently reclassify itself the day someone improves an
|
|
114
|
+
* error string. An unclassifiable failure is `internal`, which is honest.
|
|
115
|
+
*/
|
|
116
|
+
declare function classifyHostError(err: unknown): McpErrorCode;
|
|
117
|
+
|
|
118
|
+
export { MCP_ERROR_CODES as M, type McpErrorCode as a, McpToolError as b, classifyHostError as c, classifyHostResponse as d, isMcpToolError as e, isRetryableMcpErrorCode as f, isMcpErrorCode as i, mcpErrorCodeFromHttpStatus as m };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A machine-readable classification for an MCP call that failed, carried across the host/plugin
|
|
3
|
+
* bridge alongside the human-readable message.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* The bridge used to reduce every failure to `err.message`, a bare string. A plugin therefore could
|
|
8
|
+
* not tell "you lack permission for this tool" from "the host is unreachable" without matching on
|
|
9
|
+
* host error prose, and the practical consequence was that plugins swallowed *all* failures into a
|
|
10
|
+
* successful empty result to keep a permission denial from looking like a crash. That turns a
|
|
11
|
+
* transient outage into "you have no connectors" - a false statement about the user's data, and one
|
|
12
|
+
* that also suppresses the query layer's retry.
|
|
13
|
+
*
|
|
14
|
+
* A closed vocabulary lets the plugin branch on the one case it wants to tolerate and rethrow the
|
|
15
|
+
* rest.
|
|
16
|
+
*
|
|
17
|
+
* ## Deliberately small, and deliberately not HTTP
|
|
18
|
+
*
|
|
19
|
+
* These are the distinctions a *caller* acts on differently, not a mirror of any status enum. Codes
|
|
20
|
+
* that would prompt the same handling are folded together, because a vocabulary nobody can apply is
|
|
21
|
+
* just a wider surface to get wrong. `unavailable` and `timeout` stay separate only because retry
|
|
22
|
+
* policy differs between them.
|
|
23
|
+
*/
|
|
24
|
+
type McpErrorCode =
|
|
25
|
+
/** The caller is not authenticated, or its token expired. Re-authentication may fix it. */
|
|
26
|
+
"unauthorized"
|
|
27
|
+
/** Authenticated, but not permitted this tool or resource. Retrying will not help. */
|
|
28
|
+
| "forbidden"
|
|
29
|
+
/** The tool, resource, or addressed entity does not exist. */
|
|
30
|
+
| "not_found"
|
|
31
|
+
/** The arguments were rejected. A caller bug, or stale client-side validation. */
|
|
32
|
+
| "invalid_request"
|
|
33
|
+
/** A concurrency or state conflict - a row version, or a duplicate. */
|
|
34
|
+
| "conflict"
|
|
35
|
+
/** Throttled. Retry later, with backoff. */
|
|
36
|
+
| "rate_limited"
|
|
37
|
+
/** The call did not complete in time. Safe to retry only if the operation is idempotent. */
|
|
38
|
+
| "timeout"
|
|
39
|
+
/** The host or an upstream dependency is down. Retryable. */
|
|
40
|
+
| "unavailable"
|
|
41
|
+
/** Anything else, including an unclassifiable failure. The default - never a claim. */
|
|
42
|
+
| "internal";
|
|
43
|
+
/**
|
|
44
|
+
* Every valid {@link McpErrorCode}. Used to validate a code arriving over the wire: an unknown
|
|
45
|
+
* string is downgraded rather than trusted, so a newer host cannot make an older plugin branch on a
|
|
46
|
+
* code it has never heard of.
|
|
47
|
+
*/
|
|
48
|
+
declare const MCP_ERROR_CODES: readonly McpErrorCode[];
|
|
49
|
+
/** True when `value` is a code this build understands. */
|
|
50
|
+
declare function isMcpErrorCode(value: unknown): value is McpErrorCode;
|
|
51
|
+
/** True when the failure is worth retrying as-is. */
|
|
52
|
+
declare function isRetryableMcpErrorCode(code: McpErrorCode): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* An MCP call rejected by the host, carrying its {@link McpErrorCode}.
|
|
55
|
+
*
|
|
56
|
+
* `name` stays `"McpHostError"` - the string the bridge has always set - so code that matches on the
|
|
57
|
+
* name keeps working. Prefer {@link isMcpToolError}, which survives a name change and works across
|
|
58
|
+
* realm boundaries where `instanceof` does not.
|
|
59
|
+
*/
|
|
60
|
+
declare class McpToolError extends Error {
|
|
61
|
+
/** Machine-readable classification. `internal` when the host sent none. */
|
|
62
|
+
readonly code: McpErrorCode;
|
|
63
|
+
/**
|
|
64
|
+
* Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
|
|
65
|
+
* check because the error can be constructed in one bundle and inspected in another, where
|
|
66
|
+
* `instanceof` compares two different class objects and answers false.
|
|
67
|
+
*/
|
|
68
|
+
readonly isMcpToolError: true;
|
|
69
|
+
constructor(message: string, code?: McpErrorCode);
|
|
70
|
+
/** True when this failure is worth retrying unchanged. */
|
|
71
|
+
get retryable(): boolean;
|
|
72
|
+
}
|
|
73
|
+
/** True when `value` is an {@link McpToolError}, including one from another bundle. */
|
|
74
|
+
declare function isMcpToolError(value: unknown): value is McpToolError;
|
|
75
|
+
/**
|
|
76
|
+
* Maps an HTTP status onto a code. Split out because host MCP clients are commonly HTTP clients, so
|
|
77
|
+
* a status is the classification most of them already have.
|
|
78
|
+
*
|
|
79
|
+
* Unrecognised statuses (including every 2xx and 3xx, which should not be reaching an error path)
|
|
80
|
+
* yield `internal` rather than a guess.
|
|
81
|
+
*/
|
|
82
|
+
declare function mcpErrorCodeFromHttpStatus(status: number): McpErrorCode;
|
|
83
|
+
/**
|
|
84
|
+
* Derives a code from a host MCP client's RETURNED failure, as opposed to a thrown one.
|
|
85
|
+
*
|
|
86
|
+
* Both paths exist and both have to be covered. A client that throws is handled by
|
|
87
|
+
* {@link classifyHostError}; a client that reports failure as `{ ok: false, error, status }` - the
|
|
88
|
+
* common shape for anything wrapping HTTP - comes through here. Covering only the throw path leaves
|
|
89
|
+
* the majority of real failures arriving as `internal`, which is the same blindness the code was
|
|
90
|
+
* added to remove.
|
|
91
|
+
*
|
|
92
|
+
* Returns `undefined` for a successful response, so a caller can spread it without putting a
|
|
93
|
+
* meaningless code on the happy path.
|
|
94
|
+
*/
|
|
95
|
+
declare function classifyHostResponse(response: {
|
|
96
|
+
readonly ok: boolean;
|
|
97
|
+
readonly status?: number;
|
|
98
|
+
readonly mcpErrorCode?: unknown;
|
|
99
|
+
}): McpErrorCode | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Derives a code from an arbitrary thrown value, for the host side of the bridge.
|
|
102
|
+
*
|
|
103
|
+
* Precedence, most explicit first:
|
|
104
|
+
*
|
|
105
|
+
* 1. An `mcpErrorCode` property holding a known code. The intended contract: a host MCP client that
|
|
106
|
+
* knows why a call failed says so directly.
|
|
107
|
+
* 2. A numeric `status` / `statusCode`, mapped by {@link mcpErrorCodeFromHttpStatus}. Covers the
|
|
108
|
+
* HTTP clients that already carry one without asking every host to adopt the field above.
|
|
109
|
+
* 3. `name === "AbortError"` / `"TimeoutError"`, which is how the platform's own aborts surface.
|
|
110
|
+
* 4. `internal`.
|
|
111
|
+
*
|
|
112
|
+
* **Never infers from the message.** Matching prose would make the classification depend on wording
|
|
113
|
+
* nobody treats as a contract, and it would silently reclassify itself the day someone improves an
|
|
114
|
+
* error string. An unclassifiable failure is `internal`, which is honest.
|
|
115
|
+
*/
|
|
116
|
+
declare function classifyHostError(err: unknown): McpErrorCode;
|
|
117
|
+
|
|
118
|
+
export { MCP_ERROR_CODES as M, type McpErrorCode as a, McpToolError as b, classifyHostError as c, classifyHostResponse as d, isMcpToolError as e, isRetryableMcpErrorCode as f, isMcpErrorCode as i, mcpErrorCodeFromHttpStatus as m };
|
package/dist/mock-host/cli.cjs
CHANGED
|
@@ -54,6 +54,74 @@ function createOffscreenCanvasTransfer(options) {
|
|
|
54
54
|
return { offscreen };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// src/bridge/mcp-error.ts
|
|
58
|
+
var MCP_ERROR_CODES = [
|
|
59
|
+
"unauthorized",
|
|
60
|
+
"forbidden",
|
|
61
|
+
"not_found",
|
|
62
|
+
"invalid_request",
|
|
63
|
+
"conflict",
|
|
64
|
+
"rate_limited",
|
|
65
|
+
"timeout",
|
|
66
|
+
"unavailable",
|
|
67
|
+
"internal"
|
|
68
|
+
];
|
|
69
|
+
function isMcpErrorCode(value) {
|
|
70
|
+
return typeof value === "string" && MCP_ERROR_CODES.includes(value);
|
|
71
|
+
}
|
|
72
|
+
function mcpErrorCodeFromHttpStatus(status) {
|
|
73
|
+
switch (status) {
|
|
74
|
+
case 401:
|
|
75
|
+
return "unauthorized";
|
|
76
|
+
case 403:
|
|
77
|
+
return "forbidden";
|
|
78
|
+
case 404:
|
|
79
|
+
return "not_found";
|
|
80
|
+
case 400:
|
|
81
|
+
case 422:
|
|
82
|
+
return "invalid_request";
|
|
83
|
+
case 409:
|
|
84
|
+
case 412:
|
|
85
|
+
return "conflict";
|
|
86
|
+
case 429:
|
|
87
|
+
return "rate_limited";
|
|
88
|
+
case 408:
|
|
89
|
+
case 504:
|
|
90
|
+
return "timeout";
|
|
91
|
+
case 502:
|
|
92
|
+
case 503:
|
|
93
|
+
return "unavailable";
|
|
94
|
+
default:
|
|
95
|
+
return "internal";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function classifyHostResponse(response) {
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
if (isMcpErrorCode(response.mcpErrorCode)) {
|
|
103
|
+
return response.mcpErrorCode;
|
|
104
|
+
}
|
|
105
|
+
return typeof response.status === "number" ? mcpErrorCodeFromHttpStatus(response.status) : "internal";
|
|
106
|
+
}
|
|
107
|
+
function classifyHostError(err) {
|
|
108
|
+
if (err === null || typeof err !== "object") {
|
|
109
|
+
return "internal";
|
|
110
|
+
}
|
|
111
|
+
const candidate = err;
|
|
112
|
+
if (isMcpErrorCode(candidate.mcpErrorCode)) {
|
|
113
|
+
return candidate.mcpErrorCode;
|
|
114
|
+
}
|
|
115
|
+
const status = typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
|
|
116
|
+
if (status !== void 0) {
|
|
117
|
+
return mcpErrorCodeFromHttpStatus(status);
|
|
118
|
+
}
|
|
119
|
+
if (candidate.name === "AbortError" || candidate.name === "TimeoutError") {
|
|
120
|
+
return "timeout";
|
|
121
|
+
}
|
|
122
|
+
return "internal";
|
|
123
|
+
}
|
|
124
|
+
|
|
57
125
|
// src/host/version-skew.ts
|
|
58
126
|
var EXTENSION_VERSION_HEADER = "x-extension-version";
|
|
59
127
|
var EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version";
|
|
@@ -480,7 +548,11 @@ var WorkerRemoteDomTransport = class {
|
|
|
480
548
|
type: "ethisys:mcp:invokeTool:result",
|
|
481
549
|
ok: result.ok,
|
|
482
550
|
data: result.data,
|
|
483
|
-
error: result.error
|
|
551
|
+
error: result.error,
|
|
552
|
+
// A client that REPORTS failure rather than throwing lands here, which is the common
|
|
553
|
+
// shape for anything wrapping HTTP. Classifying only the thrown path left these
|
|
554
|
+
// arriving as `internal`.
|
|
555
|
+
code: classifyHostResponse(result)
|
|
484
556
|
});
|
|
485
557
|
} catch (err) {
|
|
486
558
|
this.replyError(message.id, "ethisys:mcp:invokeTool:result", err);
|
|
@@ -510,7 +582,11 @@ var WorkerRemoteDomTransport = class {
|
|
|
510
582
|
type: "ethisys:mcp:getResource:result",
|
|
511
583
|
ok: result.ok,
|
|
512
584
|
data: result.data,
|
|
513
|
-
error: result.error
|
|
585
|
+
error: result.error,
|
|
586
|
+
// A client that REPORTS failure rather than throwing lands here, which is the common
|
|
587
|
+
// shape for anything wrapping HTTP. Classifying only the thrown path left these
|
|
588
|
+
// arriving as `internal`.
|
|
589
|
+
code: classifyHostResponse(result)
|
|
514
590
|
});
|
|
515
591
|
} catch (err) {
|
|
516
592
|
this.replyError(message.id, "ethisys:mcp:getResource:result", err);
|
|
@@ -541,7 +617,11 @@ var WorkerRemoteDomTransport = class {
|
|
|
541
617
|
type: "ethisys:mcp:uploadDocument:result",
|
|
542
618
|
ok: result.ok,
|
|
543
619
|
data: result.data,
|
|
544
|
-
error: result.error
|
|
620
|
+
error: result.error,
|
|
621
|
+
// A client that REPORTS failure rather than throwing lands here, which is the common
|
|
622
|
+
// shape for anything wrapping HTTP. Classifying only the thrown path left these
|
|
623
|
+
// arriving as `internal`.
|
|
624
|
+
code: classifyHostResponse(result)
|
|
545
625
|
});
|
|
546
626
|
} catch (err) {
|
|
547
627
|
this.replyError(message.id, "ethisys:mcp:uploadDocument:result", err);
|
|
@@ -549,7 +629,7 @@ var WorkerRemoteDomTransport = class {
|
|
|
549
629
|
}
|
|
550
630
|
replyError(id, type, err) {
|
|
551
631
|
const message = err instanceof Error ? err.message : "MCP request failed";
|
|
552
|
-
this.safePostMessage({ id, type, ok: false, error: message });
|
|
632
|
+
this.safePostMessage({ id, type, ok: false, error: message, code: classifyHostError(err) });
|
|
553
633
|
}
|
|
554
634
|
};
|
|
555
635
|
|