@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.
@@ -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-Yyo92yXC.js"></script>
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 requestHeaders = request.headers ?? {};
1107
- const headers = {
1108
- "content-type": "application/json",
1109
- accept: "application/json, text/event-stream",
1110
- ...this.staticHeaders,
1111
- ...requestHeaders
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 requestHeaders = request.headers ?? {};
1319
- const headers = {
1320
- "content-type": "application/json",
1321
- ...this.staticHeaders,
1322
- ...requestHeaders
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 postFailure = isTimeout ? new Error(`upstream request POST timed out after ${String(this.requestTimeoutMs)}ms`) : asError;
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.1.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": "3.2.4"
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
+ }