@gethelio/proxy 0.1.0 → 0.2.0
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/README.md +13 -14
- package/dist/cli.js +126 -55
- package/dist/dashboard-assets/assets/{index-Yyo92yXC.js → index-CAgnN6wV.js} +28 -28
- package/dist/dashboard-assets/index.html +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +89 -18
- package/package.json +16 -15
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="referrer" content="no-referrer" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<title>Helio Dashboard</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CAgnN6wV.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-DG3h7Cvn.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,7 @@ declare const helioConfigSchema: z.ZodObject<{
|
|
|
113
113
|
connect_timeout: z.ZodDefault<z.ZodString>;
|
|
114
114
|
request_timeout: z.ZodDefault<z.ZodString>;
|
|
115
115
|
forward_headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
116
|
+
headers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
116
117
|
}, z.core.$strip>;
|
|
117
118
|
listen: z.ZodPrefault<z.ZodObject<{
|
|
118
119
|
port: z.ZodDefault<z.ZodNumber>;
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,8 @@ var upstreamSchema = z.object({
|
|
|
37
37
|
args: z.array(z.string()).optional(),
|
|
38
38
|
connect_timeout: durationSchema.default("10s"),
|
|
39
39
|
request_timeout: durationSchema.default("30s"),
|
|
40
|
-
forward_headers: z.array(z.string().min(1)).default([])
|
|
40
|
+
forward_headers: z.array(z.string().min(1)).default([]),
|
|
41
|
+
headers: z.record(z.string(), z.string()).default({})
|
|
41
42
|
}).refine((data) => data.transport !== "stdio" || data.command !== void 0, {
|
|
42
43
|
message: '"command" is required when transport is "stdio"',
|
|
43
44
|
path: ["command"]
|
|
@@ -51,6 +52,22 @@ var upstreamSchema = z.object({
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
56
|
+
"mcp-session-id",
|
|
57
|
+
"mcp-protocol-version",
|
|
58
|
+
"content-type",
|
|
59
|
+
"content-length",
|
|
60
|
+
"host"
|
|
61
|
+
]);
|
|
62
|
+
for (const name of Object.keys(data.headers)) {
|
|
63
|
+
if (reserved.has(name.toLowerCase())) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: "custom",
|
|
66
|
+
path: ["headers", name],
|
|
67
|
+
message: `upstream.headers must not set reserved header "${name}"`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
54
71
|
});
|
|
55
72
|
var listenSchema = z.object({
|
|
56
73
|
port: z.number().int().min(1).max(65535).default(3e3),
|
|
@@ -1092,6 +1109,59 @@ async function parseUpstreamResponse(res) {
|
|
|
1092
1109
|
return { status: res.status, headers, body };
|
|
1093
1110
|
}
|
|
1094
1111
|
|
|
1112
|
+
// src/upstream/connection-error.ts
|
|
1113
|
+
var UPSTREAM_DOCS_URL = "https://github.com/gethelio/helio/blob/main/docs/getting-started.md";
|
|
1114
|
+
var UNREACHABLE_CODES = /* @__PURE__ */ new Set([
|
|
1115
|
+
"ECONNREFUSED",
|
|
1116
|
+
"ENOTFOUND",
|
|
1117
|
+
"EAI_AGAIN",
|
|
1118
|
+
"ECONNRESET",
|
|
1119
|
+
"EHOSTUNREACH",
|
|
1120
|
+
"ENETUNREACH",
|
|
1121
|
+
"ETIMEDOUT",
|
|
1122
|
+
"EPIPE",
|
|
1123
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
1124
|
+
"UND_ERR_SOCKET"
|
|
1125
|
+
]);
|
|
1126
|
+
function extractErrorCode(error) {
|
|
1127
|
+
let current = error;
|
|
1128
|
+
for (let depth = 0; depth < 5 && current != null; depth += 1) {
|
|
1129
|
+
if (typeof current === "object" && "code" in current) {
|
|
1130
|
+
const code = current.code;
|
|
1131
|
+
if (typeof code === "string") return code;
|
|
1132
|
+
}
|
|
1133
|
+
current = current.cause;
|
|
1134
|
+
}
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
function describeUnreachableUpstream(error, url) {
|
|
1138
|
+
const code = extractErrorCode(error);
|
|
1139
|
+
const isGenericFetchFailure = error instanceof TypeError && error.message === "fetch failed";
|
|
1140
|
+
if (code !== void 0) {
|
|
1141
|
+
if (!UNREACHABLE_CODES.has(code)) return null;
|
|
1142
|
+
} else if (!isGenericFetchFailure) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
const codeSuffix = code ? ` (${code})` : "";
|
|
1146
|
+
return new Error(
|
|
1147
|
+
`Upstream MCP server at ${url} is unreachable${codeSuffix} \u2014 is it running? Helio proxies an existing MCP server: set upstream.url in helio.yaml to a reachable server, or start the server it points at. See ${UPSTREAM_DOCS_URL}`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/upstream/merge-headers.ts
|
|
1152
|
+
function mergeUpstreamHeaders(base, forwarded, staticHeaders) {
|
|
1153
|
+
const out = {};
|
|
1154
|
+
const apply = (headers) => {
|
|
1155
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1156
|
+
out[name.toLowerCase()] = value;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
apply(base);
|
|
1160
|
+
apply(forwarded);
|
|
1161
|
+
apply(staticHeaders);
|
|
1162
|
+
return out;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1095
1165
|
// src/upstream/forwarder.ts
|
|
1096
1166
|
var UpstreamForwarder = class {
|
|
1097
1167
|
url;
|
|
@@ -1103,13 +1173,14 @@ var UpstreamForwarder = class {
|
|
|
1103
1173
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
1104
1174
|
}
|
|
1105
1175
|
async forward(request) {
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1176
|
+
const headers = mergeUpstreamHeaders(
|
|
1177
|
+
{
|
|
1178
|
+
"content-type": "application/json",
|
|
1179
|
+
accept: "application/json, text/event-stream"
|
|
1180
|
+
},
|
|
1181
|
+
request.headers ?? {},
|
|
1182
|
+
this.staticHeaders
|
|
1183
|
+
);
|
|
1113
1184
|
if (request.sessionId) {
|
|
1114
1185
|
headers["mcp-session-id"] = request.sessionId;
|
|
1115
1186
|
}
|
|
@@ -1139,7 +1210,7 @@ var UpstreamForwarder = class {
|
|
|
1139
1210
|
if (isTimeout) {
|
|
1140
1211
|
throw new Error(`upstream request timed out after ${String(this.requestTimeoutMs)}ms`);
|
|
1141
1212
|
}
|
|
1142
|
-
throw error;
|
|
1213
|
+
throw describeUnreachableUpstream(error, this.url) ?? error;
|
|
1143
1214
|
}
|
|
1144
1215
|
const durationMs = performance.now() - start;
|
|
1145
1216
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -1300,7 +1371,7 @@ var SseUpstreamForwarder = class {
|
|
|
1300
1371
|
);
|
|
1301
1372
|
return;
|
|
1302
1373
|
}
|
|
1303
|
-
reject(asError);
|
|
1374
|
+
reject(describeUnreachableUpstream(err, this.url) ?? asError);
|
|
1304
1375
|
}
|
|
1305
1376
|
});
|
|
1306
1377
|
});
|
|
@@ -1315,12 +1386,11 @@ var SseUpstreamForwarder = class {
|
|
|
1315
1386
|
};
|
|
1316
1387
|
if (request.id !== void 0) body["id"] = request.id;
|
|
1317
1388
|
if (request.params !== void 0) body["params"] = request.params;
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
};
|
|
1389
|
+
const headers = mergeUpstreamHeaders(
|
|
1390
|
+
{ "content-type": "application/json" },
|
|
1391
|
+
request.headers ?? {},
|
|
1392
|
+
this.staticHeaders
|
|
1393
|
+
);
|
|
1324
1394
|
if (request.sessionId) {
|
|
1325
1395
|
headers["mcp-session-id"] = request.sessionId;
|
|
1326
1396
|
}
|
|
@@ -1346,7 +1416,7 @@ var SseUpstreamForwarder = class {
|
|
|
1346
1416
|
`upstream notification POST timed out after ${String(this.requestTimeoutMs)}ms`
|
|
1347
1417
|
);
|
|
1348
1418
|
}
|
|
1349
|
-
throw asError;
|
|
1419
|
+
throw describeUnreachableUpstream(error, this.postUrl) ?? asError;
|
|
1350
1420
|
}
|
|
1351
1421
|
if (!res.ok) {
|
|
1352
1422
|
throw new Error(`upstream notification POST failed: HTTP ${String(res.status)}`);
|
|
@@ -1393,7 +1463,8 @@ var SseUpstreamForwarder = class {
|
|
|
1393
1463
|
const asError = error instanceof Error ? error : new Error(String(error));
|
|
1394
1464
|
const isTimeout = asError.name === "TimeoutError";
|
|
1395
1465
|
const isAborted = request.signal?.aborted === true;
|
|
1396
|
-
const
|
|
1466
|
+
const networkFailure = describeUnreachableUpstream(error, this.postUrl) ?? asError;
|
|
1467
|
+
const postFailure = isTimeout ? new Error(`upstream request POST timed out after ${String(this.requestTimeoutMs)}ms`) : networkFailure;
|
|
1397
1468
|
this.pending.reject(
|
|
1398
1469
|
requestId,
|
|
1399
1470
|
isAborted ? new Error("request aborted by downstream client") : postFailure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethelio/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open-source MCP governance proxy for AI agents",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,6 +32,19 @@
|
|
|
32
32
|
"types": "./dist/index.d.ts"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"build:dashboard": "pnpm --filter @gethelio/dashboard build",
|
|
38
|
+
"build:proxy": "tsup",
|
|
39
|
+
"bundle:dashboard-assets": "tsx scripts/bundle-dashboard-assets.ts",
|
|
40
|
+
"check:dashboard-assets": "tsx scripts/check-dashboard-assets.ts",
|
|
41
|
+
"build": "pnpm run build:dashboard && pnpm run build:proxy && pnpm run bundle:dashboard-assets && pnpm run check:dashboard-assets",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"benchmark": "tsx scripts/benchmark.ts",
|
|
46
|
+
"prepack": "pnpm run check:dashboard-assets"
|
|
47
|
+
},
|
|
35
48
|
"devDependencies": {
|
|
36
49
|
"@modelcontextprotocol/sdk": "1.28.0",
|
|
37
50
|
"@types/better-sqlite3": "7.6.13",
|
|
@@ -40,7 +53,7 @@
|
|
|
40
53
|
"@types/picomatch": "4.0.2",
|
|
41
54
|
"tsup": "8.5.1",
|
|
42
55
|
"tsx": "4.21.0",
|
|
43
|
-
"vitest": "
|
|
56
|
+
"vitest": "4.1.8"
|
|
44
57
|
},
|
|
45
58
|
"dependencies": {
|
|
46
59
|
"@hono/node-server": "1.19.13",
|
|
@@ -53,17 +66,5 @@
|
|
|
53
66
|
"picomatch": "4.0.4",
|
|
54
67
|
"safe-regex2": "5.0.0",
|
|
55
68
|
"zod": "4.3.6"
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"dev": "tsup --watch",
|
|
59
|
-
"build:dashboard": "pnpm --filter @gethelio/dashboard build",
|
|
60
|
-
"build:proxy": "tsup",
|
|
61
|
-
"bundle:dashboard-assets": "tsx scripts/bundle-dashboard-assets.ts",
|
|
62
|
-
"check:dashboard-assets": "tsx scripts/check-dashboard-assets.ts",
|
|
63
|
-
"build": "pnpm run build:dashboard && pnpm run build:proxy && pnpm run bundle:dashboard-assets && pnpm run check:dashboard-assets",
|
|
64
|
-
"test": "vitest run",
|
|
65
|
-
"test:watch": "vitest",
|
|
66
|
-
"typecheck": "tsc --noEmit",
|
|
67
|
-
"benchmark": "tsx scripts/benchmark.ts"
|
|
68
69
|
}
|
|
69
|
-
}
|
|
70
|
+
}
|