@stage-labs/metro 0.1.0-beta.31 → 0.1.0-beta.33

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.
Files changed (52) hide show
  1. package/dist/cli.js +7 -0
  2. package/dist/runtime.js +4 -1
  3. package/dist/serve.js +73 -0
  4. package/package.json +1 -1
  5. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -0
  6. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +5 -5
  7. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +16 -15
  8. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +9 -9
  9. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +1 -1
  10. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +8 -7
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +9 -5
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +2 -2
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +124 -73
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +16 -7
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +14 -14
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +8 -8
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +3 -3
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +12 -2
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +11 -4
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +144 -0
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +1 -1
  25. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +10 -0
  26. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +17 -17
  27. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
  28. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +4 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +9 -9
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +2 -5
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +120 -32
  37. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +15 -16
  38. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +9 -9
  39. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +31 -31
  40. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +250 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +2 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +16 -7
  44. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +48 -39
  45. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +7 -7
  46. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -1
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +22 -12
  48. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +4 -4
  49. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +1 -1
  50. package/runtime/runtime.json +1 -1
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
package/dist/cli.js CHANGED
@@ -10,11 +10,17 @@ import { launchClaude } from './claude.js';
10
10
  import { bedrock } from './bedrock.js';
11
11
  import { installPlugin, syncPluginServers } from './plugin.js';
12
12
  import { update } from './update.js';
13
+ import { serve } from './serve.js';
13
14
  import { currentVersion } from './version.js';
14
15
  const USAGE = `metro — the command line for your MCP connectors
15
16
 
16
17
  metro start <agent-id> [--detach]
17
18
  run that agent's stations on this machine
19
+ metro serve [--port <n>] [--tunnel]
20
+ run a daemon of your own: its agents and stations live in ~/.metro/agents
21
+ here, and metro.box manages it through the link it prints; --tunnel adds
22
+ a public https address through a Cloudflare quick tunnel (needs
23
+ cloudflared, no account) so the link works from anywhere
18
24
  metro stop [agent-id]
19
25
  stop every metro daemon on this machine, however it was started
20
26
  metro status <agent-id>
@@ -145,6 +151,7 @@ const COMMANDS = {
145
151
  return 0;
146
152
  },
147
153
  start: () => start(process.argv.slice(3)),
154
+ serve: () => serve(process.argv.slice(3)),
148
155
  stop: () => stopDaemon(process.argv.slice(3)),
149
156
  status: () => status(process.argv.slice(3)),
150
157
  logs: () => logs(process.argv.slice(3)),
package/dist/runtime.js CHANGED
@@ -6,7 +6,7 @@ import { existsSync } from 'node:fs';
6
6
  import { homedir } from 'node:os';
7
7
  import { metroUrl } from './store.js';
8
8
  const AGENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
9
- const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
9
+ export const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
10
10
  export class MissingRuntime extends Error {
11
11
  }
12
12
  export function runtimeDir() {
@@ -76,6 +76,9 @@ export function daemonPlan(opts) {
76
76
  }
77
77
  export function runDaemon(agentId, plan) {
78
78
  process.stderr.write(`Starting metro for ${agentId}\n`);
79
+ return spawnPlan(plan);
80
+ }
81
+ export function spawnPlan(plan) {
79
82
  const child = spawn(plan.command, plan.args, {
80
83
  cwd: plan.cwd,
81
84
  env: plan.env,
package/dist/serve.js ADDED
@@ -0,0 +1,73 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { findBun, localPort, runtimeDir, SERVER_ENTRY, spawnPlan, } from './runtime.js';
5
+ const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
6
+ const PORT_FLAG = /^--port=(.*)$/;
7
+ const USAGE = 'usage: metro serve [--port <n>] [--tunnel]';
8
+ function portOf(raw) {
9
+ const n = Number(raw);
10
+ if (raw === undefined || raw === '' || !Number.isInteger(n) || n < 1 || n > 65535)
11
+ throw new Error(`'${raw ?? ''}' is not a port — ${USAGE}`);
12
+ return n;
13
+ }
14
+ export function parseServeArgs(argv) {
15
+ let port = localPort();
16
+ let tunnel = false;
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const arg = argv[i] ?? '';
19
+ if (arg === '--tunnel') {
20
+ tunnel = true;
21
+ continue;
22
+ }
23
+ const inline = PORT_FLAG.exec(arg);
24
+ if (inline) {
25
+ port = portOf(inline[1]);
26
+ continue;
27
+ }
28
+ if (arg === '--port' || arg === '-p') {
29
+ port = portOf(argv[i + 1]);
30
+ i += 1;
31
+ continue;
32
+ }
33
+ throw new Error(`unknown argument '${arg}' — ${USAGE}`);
34
+ }
35
+ return { port, tunnel };
36
+ }
37
+ export function findCloudflared() {
38
+ const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
39
+ if (found.error !== undefined || found.status !== 0)
40
+ throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
41
+ 'Install it with: brew install cloudflared (macOS), or from\n' +
42
+ 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/downloads/');
43
+ }
44
+ export function serveStateDir() {
45
+ const xdg = process.env.XDG_CACHE_HOME?.trim();
46
+ const base = xdg === undefined || xdg === '' ? join(homedir(), '.cache') : xdg;
47
+ return join(base, 'metro', 'serve');
48
+ }
49
+ export function servePlan(opts) {
50
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !SCRUBBED.has(key)));
51
+ return {
52
+ command: findBun(),
53
+ args: [SERVER_ENTRY],
54
+ cwd: opts.dir,
55
+ env: {
56
+ ...env,
57
+ METRO_MODE: 'local',
58
+ METRO_WEBHOOK_PORT: String(opts.port),
59
+ METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
60
+ METRO_TRAINS_DIR: join(opts.dir, 'trains'),
61
+ METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
62
+ ...(opts.tunnel ? { METRO_TUNNEL: 'quick' } : {}),
63
+ },
64
+ };
65
+ }
66
+ export function serve(argv) {
67
+ const { port, tunnel } = parseServeArgs(argv);
68
+ if (tunnel)
69
+ findCloudflared();
70
+ const plan = servePlan({ dir: runtimeDir(), port, tunnel });
71
+ process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
72
+ return spawnPlan(plan);
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.31",
3
+ "version": "0.1.0-beta.33",
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": {
@@ -54,6 +54,7 @@
54
54
  "pino": "^9.5.0",
55
55
  "pino-pretty": "^13.1.3",
56
56
  "postgres": "^3.4.5",
57
+ "viem": "2.52.2",
57
58
  "zod": "^3.23.8"
58
59
  },
59
60
  "devDependencies": {
@@ -51,13 +51,13 @@ export interface AccountApiDeps {
51
51
  attachSessions: AttachSessionApi;
52
52
  prepareAccount: (input: AttachInput) => Promise<PreparedAccount>;
53
53
  attachAccount: (
54
- email: string,
54
+ subject: string,
55
55
  agentId: string,
56
56
  station: StationName,
57
57
  config: Record<string, unknown>,
58
58
  ) => Promise<AccountRef>;
59
59
  detachAccount: (
60
- email: string,
60
+ subject: string,
61
61
  agentId: string,
62
62
  station: StationName,
63
63
  accountId: string,
@@ -146,7 +146,7 @@ async function storeAccount(
146
146
  ): Promise<AccountRef> {
147
147
  try {
148
148
  return await deps.attachAccount(
149
- session.email,
149
+ session.subject,
150
150
  agentId,
151
151
  station,
152
152
  prepared.config,
@@ -158,7 +158,7 @@ async function storeAccount(
158
158
  }
159
159
 
160
160
  function ownerOf(session: ApiSession, agentId: string): AttachOwner {
161
- return { email: session.email, agentId };
161
+ return { subject: session.subject, agentId };
162
162
  }
163
163
 
164
164
  function asInput(body: unknown): Record<string, unknown> {
@@ -224,7 +224,7 @@ async function handleDetach(
224
224
  target: { station: StationName; accountId: string },
225
225
  ): Promise<void> {
226
226
  const ref = await deps.detachAccount(
227
- session.email,
227
+ session.subject,
228
228
  agentId,
229
229
  target.station,
230
230
  target.accountId,
@@ -30,18 +30,18 @@ const PREFIX = '/api/agents';
30
30
  const SERVER_NAME = 'metro';
31
31
 
32
32
  export interface AgentApiDeps extends AccountApiDeps {
33
- listAgents: (email: string, project: string) => Promise<AgentSummary[]>;
33
+ listAgents: (subject: string, project: string) => Promise<AgentSummary[]>;
34
34
  createAgent: (
35
- email: string,
35
+ subject: string,
36
36
  project: string,
37
37
  name: string,
38
38
  ) => Promise<CreatedAgent>;
39
39
  deleteAgent: (
40
- email: string,
40
+ subject: string,
41
41
  id: string,
42
42
  ) => Promise<DeletedAgent>;
43
43
  resetKey: (
44
- email: string,
44
+ subject: string,
45
45
  id: string,
46
46
  ) => Promise<ResetAgentKey>;
47
47
  gatherAccounts: (allowed: Set<string>) => Promise<{
@@ -49,8 +49,9 @@ export interface AgentApiDeps extends AccountApiDeps {
49
49
  unavailable: string[];
50
50
  }>;
51
51
  capabilities: () => Record<string, string[]>;
52
+ attachable?: string[];
52
53
  liveness: () => Map<string, { connected: boolean; lastSeenAt: number }>;
53
- releaseRuntime: (email: string, agentId: string) => Promise<void>;
54
+ releaseRuntime: (subject: string, agentId: string) => Promise<void>;
54
55
  runtimes: (agentIds: string[]) => Promise<Map<string, string>>;
55
56
  connectorIds: (agentIds: string[]) => Promise<Map<string, string[]>>;
56
57
  }
@@ -160,18 +161,18 @@ async function handleList(
160
161
  sendJson(req, res, 400, { error: 'a project is required' });
161
162
  return;
162
163
  }
163
- const list = await deps.listAgents(session.email, project);
164
+ const list = await deps.listAgents(session.subject, project);
164
165
  const live = deps.liveness();
165
166
  const held = await deps.runtimes(
166
167
  list.filter((a) => a.owned).map((a) => a.id),
167
168
  );
168
169
  const connectors = await deps.connectorIds(list.map((a) => a.id));
169
170
  const base = {
170
- email: session.email,
171
+ subject: session.subject,
171
172
  endpoint: mcpEndpoint(),
172
173
  agents: list.map((a) => agentPayload(a, live, held, connectors)),
173
174
  capabilities: deps.capabilities(),
174
- attachable: ATTACHABLE,
175
+ attachable: deps.attachable ?? ATTACHABLE,
175
176
  };
176
177
  if (!wantsAccounts(req)) {
177
178
  sendJson(req, res, 200, base);
@@ -190,7 +191,7 @@ async function handleRuntime(
190
191
  session: ApiSession,
191
192
  id: string,
192
193
  ): Promise<void> {
193
- await deps.releaseRuntime(session.email, id);
194
+ await deps.releaseRuntime(session.subject, id);
194
195
  log.info({ agent: id }, 'agent-api: runtime released');
195
196
  sendJson(req, res, 200, { agent: id, runtime: null });
196
197
  }
@@ -208,9 +209,9 @@ async function handleCreate(
208
209
  sendJson(req, res, 400, { error: 'a project is required' });
209
210
  return;
210
211
  }
211
- const created = await deps.createAgent(session.email, project, name as string);
212
+ const created = await deps.createAgent(session.subject, project, name as string);
212
213
  log.info(
213
- { agent: created.name, id: created.id, owner: session.email },
214
+ { agent: created.name, id: created.id, owner: session.subject },
214
215
  'agent-api: created agent',
215
216
  );
216
217
  sendJson(req, res, 201, {
@@ -227,9 +228,9 @@ async function handleResetKey(
227
228
  session: ApiSession,
228
229
  id: string,
229
230
  ): Promise<void> {
230
- const reset = await deps.resetKey(session.email, id);
231
+ const reset = await deps.resetKey(session.subject, id);
231
232
  log.info(
232
- { agent: reset.name, id: reset.id, owner: session.email },
233
+ { agent: reset.name, id: reset.id, owner: session.subject },
233
234
  'agent-api: reset agent key',
234
235
  );
235
236
  sendJson(req, res, 200, {
@@ -247,9 +248,9 @@ async function handleDelete(
247
248
  session: ApiSession,
248
249
  id: string,
249
250
  ): Promise<void> {
250
- const gone = await deps.deleteAgent(session.email, id);
251
+ const gone = await deps.deleteAgent(session.subject, id);
251
252
  log.info(
252
- { agent: gone.name, id: gone.id, owner: session.email },
253
+ { agent: gone.name, id: gone.id, owner: session.subject },
253
254
  'agent-api: deleted agent',
254
255
  );
255
256
  sendJson(req, res, 200, { id: gone.id, name: gone.name, deleted: true });
@@ -15,19 +15,19 @@ import type { AgentConnectors } from '../db/agent-connectors.js';
15
15
  const PREFIX = '/api/agents';
16
16
 
17
17
  export interface AgentConnectorApiDeps {
18
- agentConnectors: (email: string, agentId: string) => Promise<AgentConnectors>;
18
+ agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
19
19
  addConnector: (
20
- email: string,
20
+ subject: string,
21
21
  agentId: string,
22
22
  connectorId: string,
23
23
  ) => Promise<AgentConnectors>;
24
24
  removeConnector: (
25
- email: string,
25
+ subject: string,
26
26
  agentId: string,
27
27
  connectorId: string,
28
28
  ) => Promise<AgentConnectors>;
29
29
  mintCode: (
30
- email: string,
30
+ subject: string,
31
31
  agentId: string,
32
32
  ) => Promise<{ code: string; expiresAt: number; agent: string }>;
33
33
  }
@@ -73,7 +73,7 @@ async function handleConnectors(
73
73
  id: string,
74
74
  ): Promise<void> {
75
75
  if (req.method === 'GET') {
76
- sendJson(req, res, 200, await deps.agentConnectors(session.email, id));
76
+ sendJson(req, res, 200, await deps.agentConnectors(session.subject, id));
77
77
  return;
78
78
  }
79
79
  const connectorId = bodyField(await readJsonBody(req), 'connectorId');
@@ -81,7 +81,7 @@ async function handleConnectors(
81
81
  sendJson(req, res, 400, { error: 'connectorId is required' });
82
82
  return;
83
83
  }
84
- sendJson(req, res, 200, await deps.addConnector(session.email, id, connectorId));
84
+ sendJson(req, res, 200, await deps.addConnector(session.subject, id, connectorId));
85
85
  }
86
86
 
87
87
  async function handleCode(
@@ -91,8 +91,8 @@ async function handleCode(
91
91
  session: ApiSession,
92
92
  id: string,
93
93
  ): Promise<void> {
94
- const minted = await deps.mintCode(session.email, id);
95
- log.info({ agent: id, email: session.email }, 'agent-api: minted pairing code');
94
+ const minted = await deps.mintCode(session.subject, id);
95
+ log.info({ agent: id, subject: session.subject }, 'agent-api: minted pairing code');
96
96
  sendJson(req, res, 201, minted);
97
97
  }
98
98
 
@@ -111,7 +111,7 @@ async function route(
111
111
  req,
112
112
  res,
113
113
  200,
114
- await deps.removeConnector(session.email, tgt.id, tgt.connectorId),
114
+ await deps.removeConnector(session.subject, tgt.id, tgt.connectorId),
115
115
  );
116
116
  } catch (err) {
117
117
  apiFailure(req, res, err, 'agent-api');
@@ -1,7 +1,7 @@
1
1
  import { makeCodeStore } from './pair-codes.js';
2
2
 
3
3
  export interface AgentCode {
4
- email: string;
4
+ subject: string;
5
5
  agentId: string;
6
6
  }
7
7
 
@@ -14,7 +14,7 @@ const BODY_MAX = 4 * 1024;
14
14
  const DRAIN_MAX = 2 * BODY_MAX;
15
15
 
16
16
  export interface ApiSession {
17
- email: string;
17
+ subject: string;
18
18
  }
19
19
 
20
20
  export function cors(req: IncomingMessage): Record<string, string> {
@@ -22,6 +22,7 @@ export function cors(req: IncomingMessage): Record<string, string> {
22
22
  'access-control-allow-origin': req.headers.origin ?? '*',
23
23
  'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
24
24
  'access-control-allow-headers': 'Authorization, Content-Type',
25
+ 'access-control-allow-private-network': 'true',
25
26
  'access-control-max-age': '86400',
26
27
  vary: 'Origin',
27
28
  };
@@ -47,8 +48,8 @@ export function apiSession(req: IncomingMessage): ApiSession | null {
47
48
  const token = extractToken(req) ?? '';
48
49
  if (token === '') return null;
49
50
  try {
50
- const { email } = verifySession(token, secret);
51
- return { email: email.toLowerCase() };
51
+ const { subject } = verifySession(token, secret);
52
+ return { subject: subject.toLowerCase() };
52
53
  } catch {
53
54
  return null;
54
55
  }
@@ -61,7 +62,7 @@ export function runIdentity(req: IncomingMessage): RunClaims | null {
61
62
  if (token === '') return null;
62
63
  try {
63
64
  const claims = verifyRunToken(token, secret);
64
- return { ...claims, email: claims.email.toLowerCase() };
65
+ return { ...claims, subject: claims.subject.toLowerCase() };
65
66
  } catch {
66
67
  return null;
67
68
  }
@@ -77,12 +78,12 @@ export function agentIdentity(req: IncomingMessage): AgentIdentity | null {
77
78
  const token = extractToken(req) ?? '';
78
79
  if (token === '') return null;
79
80
  try {
80
- const { email, agentId } = verifyAgentToken(token, secret);
81
- return { email: email.toLowerCase(), agentId };
81
+ const { subject, agentId } = verifyAgentToken(token, secret);
82
+ return { subject: subject.toLowerCase(), agentId };
82
83
  } catch {
83
84
  const run = runIdentity(req);
84
85
  if (run === null) return null;
85
- return { email: run.email, agentId: run.agentId, runtimeId: run.runtimeId };
86
+ return { subject: run.subject, agentId: run.agentId, runtimeId: run.runtimeId };
86
87
  }
87
88
  }
88
89
 
@@ -14,7 +14,8 @@ import {
14
14
  import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
15
15
  import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
16
16
  import { errMsg, log } from './log.js';
17
- import { loadTunnelConfig, webhookPort } from './tunnel.js';
17
+ import { configuredTunnelHost, currentTunnelUrl, webhookPort } from './tunnel.js';
18
+ import { isLocalMode } from './paths.js';
18
19
 
19
20
  function authorized(req: IncomingMessage, name: string): boolean {
20
21
  const owner = attachmentOwner(name);
@@ -30,14 +31,17 @@ const DEFAULT_PUBLIC_BASE = 'https://mcp.metro.box';
30
31
  export function publicBaseUrl(): string | null {
31
32
  const env = process.env.METRO_PUBLIC_URL?.trim();
32
33
  if (env) return env.replace(/\/+$/, '');
33
- const host = loadTunnelConfig()?.hostname;
34
+ const live = currentTunnelUrl();
35
+ if (live !== null) return live;
36
+ const host = configuredTunnelHost();
34
37
  return host ? `https://${host}` : null;
35
38
  }
36
39
 
40
+ const servesLocally = (): boolean =>
41
+ (process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '' || isLocalMode();
42
+
37
43
  const localBase = (): string | null =>
38
- (process.env.METRO_RUN_TOKEN?.trim() ?? '') === ''
39
- ? null
40
- : `http://127.0.0.1:${String(webhookPort())}`;
44
+ servesLocally() ? `http://127.0.0.1:${String(webhookPort())}` : null;
41
45
 
42
46
  export const publicBaseOrDefault = (): string =>
43
47
  publicBaseUrl() ?? localBase() ?? DEFAULT_PUBLIC_BASE;
@@ -28,7 +28,7 @@ const MAX_PER_AGENT = 2;
28
28
  const MAX_TOTAL = 40;
29
29
 
30
30
  export interface AttachOwner {
31
- email: string;
31
+ subject: string;
32
32
  agentId: string;
33
33
  }
34
34
 
@@ -109,7 +109,7 @@ export class AttachSessions {
109
109
  private own(owner: AttachOwner, attachId: string): Session {
110
110
  const session = this.sessions.get(attachId);
111
111
  if (
112
- session?.owner.email !== owner.email ||
112
+ session?.owner.subject !== owner.subject ||
113
113
  session.owner.agentId !== owner.agentId
114
114
  )
115
115
  throw missing();