@stage-labs/metro 0.1.0-beta.120 → 0.1.0-beta.121

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.120",
3
+ "version": "0.1.0-beta.121",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.120",
3
+ "version": "0.1.0-beta.121",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, the standing rules loaded at each session start, and a refresh skill for when the daemon could not keep the server list current. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
+ import { whyUnreachable } from './reach.js';
2
3
  import {
3
4
  ConnectorVerifyError,
4
5
  type OAuthTokens,
@@ -51,8 +52,8 @@ export async function registerClient(
51
52
  token_endpoint_auth_method: 'none',
52
53
  }),
53
54
  });
54
- } catch {
55
- throw refused('Metro could not reach that server to register.');
55
+ } catch (err) {
56
+ throw refused(`Metro could not reach that server to register: ${whyUnreachable(err)}`);
56
57
  }
57
58
  const body: unknown = await res.json().catch(() => null);
58
59
  if (!res.ok || !isRecord(body) || typeof body.client_id !== 'string')
@@ -126,8 +127,8 @@ async function postToken(
126
127
  },
127
128
  body: params.toString(),
128
129
  });
129
- } catch {
130
- throw refused('Metro could not reach that server to finish signing in.');
130
+ } catch (err) {
131
+ throw refused(`Metro could not reach that server to finish signing in: ${whyUnreachable(err)}`);
131
132
  }
132
133
  const body: unknown = await res.json().catch(() => null);
133
134
  if (!res.ok || !isRecord(body))
@@ -0,0 +1,27 @@
1
+ import { errMsg } from '@metro-labs/core/log';
2
+
3
+ const HINTS: Record<string, string> = {
4
+ ENOTFOUND: 'the name does not resolve',
5
+ EAI_AGAIN: 'the name did not resolve in time',
6
+ ECONNREFUSED: 'nothing is listening there',
7
+ ECONNRESET: 'the server dropped the connection',
8
+ ETIMEDOUT: 'the server did not answer in time',
9
+ ConnectionRefused: 'nothing is listening there',
10
+ CERT_HAS_EXPIRED: 'its certificate has expired',
11
+ DEPTH_ZERO_SELF_SIGNED_CERT: 'its certificate is self-signed',
12
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'its certificate could not be verified',
13
+ ERR_TLS_CERT_ALTNAME_INVALID: 'its certificate is for another name',
14
+ SELF_SIGNED_CERT_IN_CHAIN: 'its certificate chain is not trusted',
15
+ };
16
+
17
+ const codeOf = (err: unknown): string =>
18
+ err !== null && typeof err === 'object' && 'code' in err && typeof err.code === 'string' ? err.code : '';
19
+
20
+ export function whyUnreachable(err: unknown): string {
21
+ const inner = err instanceof Error && err.cause instanceof Error ? err.cause : err;
22
+ const message = errMsg(inner).trim();
23
+ const code = codeOf(inner) || codeOf(err);
24
+ const said = message !== '' && message !== 'fetch failed' ? message : code || 'no answer';
25
+ const hint = HINTS[code];
26
+ return hint === undefined ? said : `${said} (${hint})`;
27
+ }
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { whyUnreachable } from './reach.js';
2
3
  import type { RelayTarget } from './relay-target.js';
3
4
  import type { AgentIdentity } from '@metro-labs/http/api-http';
4
5
  import { ApiError } from '@metro-labs/http/api-error';
@@ -267,7 +268,7 @@ function dispatch(
267
268
  answer(res, err.status, { error: err.message });
268
269
  } else {
269
270
  log.warn({ err: errMsg(err), connector: connectorId }, 'relay: failed');
270
- answer(res, 502, { error: 'metro could not reach the connector' });
271
+ answer(res, 502, { error: `metro could not reach the connector: ${whyUnreachable(err)}` });
271
272
  if (!res.writableEnded) res.end();
272
273
  }
273
274
  closeIfBodyUnread(req, res);
@@ -1,4 +1,5 @@
1
1
  import { errMsg, log } from '@metro-labs/core/log';
2
+ import { whyUnreachable } from './reach.js';
2
3
  import {
3
4
  ConnectorNotMcp,
4
5
  ConnectorUnauthorized,
@@ -97,8 +98,8 @@ async function frame(
97
98
  },
98
99
  body: JSON.stringify(body),
99
100
  });
100
- } catch {
101
- throw refused(`Metro could not reach ${url.hostname}.`);
101
+ } catch (err) {
102
+ throw refused(`Metro could not reach ${url.hostname}: ${whyUnreachable(err)}`);
102
103
  }
103
104
  if (res.status >= 300 && res.status < 400) {
104
105
  await discard(res);
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.120"
2
+ "version": "0.1.0-beta.121"
3
3
  }