@stage-labs/metro 0.1.0-beta.120 → 0.1.0-beta.122
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 +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +5 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +12 -2
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.122",
|
|
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.
|
|
3
|
+
"version": "0.1.0-beta.122",
|
|
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(
|
|
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(
|
|
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:
|
|
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);
|
|
@@ -104,6 +104,7 @@ async function toAnthropic(
|
|
|
104
104
|
signal: watch.signal,
|
|
105
105
|
redirect: 'manual',
|
|
106
106
|
});
|
|
107
|
+
noteRefusal('anthropic', route.model, upstream);
|
|
107
108
|
noteUsageHeaders('anthropic', upstream.headers);
|
|
108
109
|
const scanner = new UsageScanner('anthropic');
|
|
109
110
|
await pipeResponse(upstream, res, watch, key === '' ? { scanner } : { ownCredential: true, scanner });
|
|
@@ -137,13 +138,22 @@ async function toOpenRouter(
|
|
|
137
138
|
signal: watch.signal,
|
|
138
139
|
redirect: 'manual',
|
|
139
140
|
});
|
|
141
|
+
noteRefusal('openrouter', route.model, upstream);
|
|
140
142
|
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner('openrouter') });
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
const thinkingOff = (body: Record<string, unknown>): boolean => isRecord(body.thinking) && body.thinking.type === 'disabled';
|
|
146
|
+
|
|
143
147
|
export function openrouterBody(body: Record<string, unknown>, model: string, zdr: boolean): Record<string, unknown> {
|
|
144
|
-
|
|
148
|
+
const sent: Record<string, unknown> = { ...body, model };
|
|
149
|
+
if (thinkingOff(body)) delete sent.thinking;
|
|
150
|
+
if (!zdr) return sent;
|
|
145
151
|
const provider = isRecord(body.provider) ? body.provider : {};
|
|
146
|
-
return { ...
|
|
152
|
+
return { ...sent, provider: { ...provider, zdr: true } };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function noteRefusal(provider: string, model: string, upstream: Response): void {
|
|
156
|
+
if (!upstream.ok) log.warn({ provider, model, status: upstream.status }, 'gateway: the provider refused the request');
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
async function dispatch(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): Promise<void> {
|
package/runtime/runtime.json
CHANGED