@stage-labs/metro 0.1.0-beta.15 → 0.1.0-beta.151
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/dist/api.js +9 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +182 -0
- package/dist/cli.js +86 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +77 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +43 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +50 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +155 -0
- package/dist/service.js +242 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -28
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +40 -0
- package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
- package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
- package/runtime/marketplace/plugin/orchestrator.md +16 -0
- package/runtime/node_modules/@metro-labs/core/package.json +43 -0
- package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +108 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +364 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +185 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +40 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +200 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +30 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +380 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +354 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +263 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +313 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +157 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +304 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +299 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +108 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +59 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +101 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +259 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +187 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +247 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +182 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +201 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +257 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +353 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +232 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +242 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +186 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +370 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +210 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +58 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +80 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +261 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/http/package.json +29 -0
- package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +106 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +150 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +223 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +130 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +132 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +123 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -0
- package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +190 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +347 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +18 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +37 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -139
- package/dist/broker/claims.js +0 -144
- package/dist/broker/history-stream.js +0 -149
- package/dist/cli/config.js +0 -179
- package/dist/cli/index.js +0 -175
- package/dist/cli/messenger-api.js +0 -214
- package/dist/cli/messenger-transcribe.js +0 -43
- package/dist/cli/messenger-uploads.js +0 -116
- package/dist/cli/monitor-api.js +0 -205
- package/dist/cli/tail.js +0 -95
- package/dist/cli/util.js +0 -72
- package/dist/cli/webhook.js +0 -181
- package/dist/codex-rc/client.js +0 -225
- package/dist/codex-rc/protocol.js +0 -38
- package/dist/dispatcher/server.js +0 -122
- package/dist/dispatcher.js +0 -97
- package/dist/history.js +0 -143
- package/dist/ipc.js +0 -90
- package/dist/lines.js +0 -54
- package/dist/local-identity.js +0 -80
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -114
- package/dist/trains/protocol.js +0 -99
- package/dist/trains/supervisor.js +0 -210
- package/dist/tunnel.js +0 -102
- package/docs/broker.md +0 -166
- package/docs/monitor.md +0 -241
- package/docs/uri-scheme.md +0 -127
- package/examples/README.md +0 -32
- package/examples/telegram.ts +0 -121
- package/skills/metro/SKILL.md +0 -110
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { syncPluginServers } from './plugin-sync.js';
|
|
3
|
+
import { listRemoteTools, type RemoteTool } from './tools.js';
|
|
4
|
+
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
import {
|
|
7
|
+
connectorUrlText,
|
|
8
|
+
ConnectorVerifyError,
|
|
9
|
+
parseConnectorUrl,
|
|
10
|
+
verifyRemoteMcp,
|
|
11
|
+
type ConnectorAuth,
|
|
12
|
+
type OAuthAuth,
|
|
13
|
+
} from './verify.js';
|
|
14
|
+
import { oauthExpired, refreshOAuth } from './oauth.js';
|
|
15
|
+
import { advertisesOAuth } from './oauth-discovery.js';
|
|
16
|
+
import {
|
|
17
|
+
ConnectorError,
|
|
18
|
+
connectorAuth,
|
|
19
|
+
connectorClient,
|
|
20
|
+
connectorName,
|
|
21
|
+
readConfig,
|
|
22
|
+
stamp,
|
|
23
|
+
type ConnectorConfig,
|
|
24
|
+
} from './config.js';
|
|
25
|
+
import {
|
|
26
|
+
bearerHeaders,
|
|
27
|
+
fixedTarget,
|
|
28
|
+
unrefreshedTarget,
|
|
29
|
+
type RelayTarget,
|
|
30
|
+
} from './relay-target.js';
|
|
31
|
+
import {
|
|
32
|
+
connectorFromRow,
|
|
33
|
+
UNVERIFIED,
|
|
34
|
+
type Connector,
|
|
35
|
+
type ConnectorCheck,
|
|
36
|
+
type ConnectorInput,
|
|
37
|
+
type DeletedConnector,
|
|
38
|
+
type PendingConnectorInput,
|
|
39
|
+
} from './model.js';
|
|
40
|
+
import { agentsDir } from '../agents/files.js';
|
|
41
|
+
import { assertLocalOwner, LOCAL_PROJECT_ID } from '../agents/file-admin.js';
|
|
42
|
+
import { newId } from '@metro-labs/core/ids';
|
|
43
|
+
import type { LoadedConnector } from '../stations/materialize.js';
|
|
44
|
+
|
|
45
|
+
export interface LocalConnectorRow {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
url: string;
|
|
49
|
+
transport: 'http';
|
|
50
|
+
config: ConnectorConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FILE = 'connectors.json';
|
|
54
|
+
const filePath = (dir: string): string => join(dir, FILE);
|
|
55
|
+
|
|
56
|
+
const isRow = (v: unknown): v is { id: string; name: string; url: string; config?: unknown } =>
|
|
57
|
+
typeof v === 'object' &&
|
|
58
|
+
v !== null &&
|
|
59
|
+
typeof (v as { id?: unknown }).id === 'string' &&
|
|
60
|
+
typeof (v as { name?: unknown }).name === 'string' &&
|
|
61
|
+
typeof (v as { url?: unknown }).url === 'string';
|
|
62
|
+
|
|
63
|
+
const asList = (value: unknown): unknown[] =>
|
|
64
|
+
Array.isArray(value) ? value.map((item: unknown) => item) : [];
|
|
65
|
+
|
|
66
|
+
export function readLocalConnectors(dir = agentsDir()): LocalConnectorRow[] {
|
|
67
|
+
const raw = readJson<{ connectors?: unknown }>(filePath(dir), {}, {
|
|
68
|
+
warn: 'connectors.json: malformed, ignoring',
|
|
69
|
+
});
|
|
70
|
+
return asList(raw.connectors).filter(isRow).map((r) => ({
|
|
71
|
+
id: r.id,
|
|
72
|
+
name: r.name,
|
|
73
|
+
url: r.url,
|
|
74
|
+
transport: 'http' as const,
|
|
75
|
+
config: readConfig(r.config),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeRows(dir: string, rows: LocalConnectorRow[]): void {
|
|
80
|
+
writeSecure(filePath(dir), `${JSON.stringify({ version: 1, connectors: rows }, null, 2)}\n`);
|
|
81
|
+
if (dir === agentsDir()) syncPluginServers(rows);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const missing = (): ConnectorError => new ConnectorError('no such connector', 404);
|
|
85
|
+
|
|
86
|
+
function ownedRows(subject: string, project: string, dir: string): LocalConnectorRow[] {
|
|
87
|
+
assertLocalOwner(subject, dir);
|
|
88
|
+
if (project !== LOCAL_PROJECT_ID) throw new ConnectorError('no such project', 404);
|
|
89
|
+
return readLocalConnectors(dir);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rowOrThrow(subject: string, id: string, dir: string): LocalConnectorRow {
|
|
93
|
+
assertLocalOwner(subject, dir);
|
|
94
|
+
const row = readLocalConnectors(dir).find((r) => r.id === id);
|
|
95
|
+
if (row === undefined) throw missing();
|
|
96
|
+
return row;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function replace(dir: string, row: LocalConnectorRow): Connector {
|
|
100
|
+
writeRows(dir, readLocalConnectors(dir).map((r) => (r.id === row.id ? row : r)));
|
|
101
|
+
return connectorFromRow(row);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function oauthCapable(url: URL, auth: ConnectorAuth): Promise<boolean> {
|
|
105
|
+
if (auth.kind === 'oauth') return true;
|
|
106
|
+
if (auth.kind === 'header') return false;
|
|
107
|
+
return advertisesOAuth(url);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function localImportConnectors(
|
|
111
|
+
rows: LoadedConnector[],
|
|
112
|
+
dir = agentsDir(),
|
|
113
|
+
mode: 'append' | 'overwrite' = 'overwrite',
|
|
114
|
+
): number {
|
|
115
|
+
const current = readLocalConnectors(dir);
|
|
116
|
+
const fresh =
|
|
117
|
+
mode === 'overwrite'
|
|
118
|
+
? rows
|
|
119
|
+
: rows.filter((row) => !current.some((r) => r.id === row.id || r.name === row.name));
|
|
120
|
+
const imported: LocalConnectorRow[] = fresh.map((row) => ({
|
|
121
|
+
id: row.id,
|
|
122
|
+
name: row.name,
|
|
123
|
+
url: row.url,
|
|
124
|
+
transport: 'http',
|
|
125
|
+
config: readConfig(row.config),
|
|
126
|
+
}));
|
|
127
|
+
const ids = new Set(imported.map((r) => r.id));
|
|
128
|
+
const names = new Set(imported.map((r) => r.name));
|
|
129
|
+
writeRows(dir, [...current.filter((r) => !ids.has(r.id) && !names.has(r.name)), ...imported]);
|
|
130
|
+
return imported.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function localListConnectors(
|
|
134
|
+
subject: string,
|
|
135
|
+
project: string,
|
|
136
|
+
dir = agentsDir(),
|
|
137
|
+
): Promise<Connector[]> {
|
|
138
|
+
return Promise.resolve(ownedRows(subject, project, dir).map(connectorFromRow));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function localGetConnector(subject: string, id: string, dir = agentsDir()): Promise<Connector> {
|
|
142
|
+
return Promise.resolve(connectorFromRow(rowOrThrow(subject, id, dir)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertNameFree(name: string, exceptId: string | null, dir: string): void {
|
|
146
|
+
if (readLocalConnectors(dir).some((r) => r.id !== exceptId && r.name === name))
|
|
147
|
+
throw new ConnectorError(`a connector named '${name}' already exists on this daemon`, 409);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function insert(dir: string, name: string, url: URL, config: ConnectorConfig): Connector {
|
|
151
|
+
assertNameFree(name, null, dir);
|
|
152
|
+
const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), transport: 'http', config };
|
|
153
|
+
writeRows(dir, [...readLocalConnectors(dir), row]);
|
|
154
|
+
return connectorFromRow(row);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function localCreateConnector(
|
|
158
|
+
subject: string,
|
|
159
|
+
project: string,
|
|
160
|
+
input: ConnectorInput,
|
|
161
|
+
dir = agentsDir(),
|
|
162
|
+
): Promise<Connector> {
|
|
163
|
+
ownedRows(subject, project, dir);
|
|
164
|
+
const name = connectorName(input.name);
|
|
165
|
+
const url = parseConnectorUrl(input.url);
|
|
166
|
+
const auth = connectorAuth(input.header, input.value);
|
|
167
|
+
const client = connectorClient(input.clientId, input.clientSecret);
|
|
168
|
+
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
169
|
+
return insert(dir, name, url, {
|
|
170
|
+
auth,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
verified,
|
|
173
|
+
oauth: await oauthCapable(url, auth),
|
|
174
|
+
client,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function localCreatePendingConnector(
|
|
179
|
+
subject: string,
|
|
180
|
+
project: string,
|
|
181
|
+
input: PendingConnectorInput,
|
|
182
|
+
dir = agentsDir(),
|
|
183
|
+
): Promise<Connector> {
|
|
184
|
+
ownedRows(subject, project, dir);
|
|
185
|
+
const name = connectorName(input.name);
|
|
186
|
+
const url = parseConnectorUrl(input.url);
|
|
187
|
+
const client = connectorClient(input.clientId, input.clientSecret);
|
|
188
|
+
return Promise.resolve(
|
|
189
|
+
insert(dir, name, url, { auth: { kind: 'none' }, createdAt: new Date().toISOString(), verified: UNVERIFIED, oauth: true, client }),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function localReconnectConnector(
|
|
194
|
+
subject: string,
|
|
195
|
+
id: string,
|
|
196
|
+
auth: OAuthAuth,
|
|
197
|
+
dir = agentsDir(),
|
|
198
|
+
): Promise<Connector> {
|
|
199
|
+
const row = rowOrThrow(subject, id, dir);
|
|
200
|
+
const verified = stamp(await verifyRemoteMcp(parseConnectorUrl(row.url), auth));
|
|
201
|
+
return replace(dir, { ...row, config: { ...row.config, auth, verified, oauth: true } });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const signedOut = (row: LocalConnectorRow): LocalConnectorRow => ({ ...row, config: { ...row.config, auth: { kind: 'none' }, oauth: true } });
|
|
205
|
+
|
|
206
|
+
export async function localDisconnectConnector(subject: string, id: string, dir = agentsDir()): Promise<Connector> {
|
|
207
|
+
const row = rowOrThrow(subject, id, dir);
|
|
208
|
+
if (row.config.auth.kind !== 'oauth') throw new ConnectorError('that connector is not signed in', 400);
|
|
209
|
+
return Promise.resolve(replace(dir, signedOut(row)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function localMarkSignedOut(id: string, dir = agentsDir()): void {
|
|
213
|
+
const row = readLocalConnectors(dir).find((r) => r.id === id);
|
|
214
|
+
if (row?.config.auth.kind !== 'oauth') return;
|
|
215
|
+
log.info({ id, name: row.name }, 'local relay: the vendor refused the token after a refresh; the connector is signed out');
|
|
216
|
+
replace(dir, signedOut(row));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function freshAuth(auth: ConnectorAuth, resource: string): Promise<ConnectorAuth> {
|
|
220
|
+
if (auth.kind !== 'oauth' || !oauthExpired(auth)) return auth;
|
|
221
|
+
return refreshOAuth(auth, resource);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function localVerifyConnector(subject: string, id: string, dir = agentsDir()): Promise<ConnectorCheck> {
|
|
225
|
+
const row = rowOrThrow(subject, id, dir);
|
|
226
|
+
try {
|
|
227
|
+
const url = parseConnectorUrl(row.url);
|
|
228
|
+
const auth = await freshAuth(row.config.auth, url.toString());
|
|
229
|
+
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
230
|
+
const oauth = row.config.oauth || (await oauthCapable(url, auth));
|
|
231
|
+
replace(dir, { ...row, config: { ...row.config, auth, verified, oauth } });
|
|
232
|
+
return { id: row.id, name: row.name, ok: true, verified };
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!(err instanceof ConnectorVerifyError)) throw err;
|
|
235
|
+
return { id: row.id, name: row.name, ok: false, reason: err.message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function localConnectorTools(subject: string, id: string, dir = agentsDir()): Promise<RemoteTool[]> {
|
|
240
|
+
const row = rowOrThrow(subject, id, dir);
|
|
241
|
+
const url = parseConnectorUrl(row.url);
|
|
242
|
+
const auth = await freshAuth(row.config.auth, url.toString());
|
|
243
|
+
if (auth !== row.config.auth) replace(dir, { ...row, config: { ...row.config, auth } });
|
|
244
|
+
return listRemoteTools(url, auth);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function localRenameConnector(subject: string, id: string, raw: string, dir = agentsDir()): Promise<Connector> {
|
|
248
|
+
const name = connectorName(raw);
|
|
249
|
+
const row = rowOrThrow(subject, id, dir);
|
|
250
|
+
assertNameFree(name, id, dir);
|
|
251
|
+
return Promise.resolve(replace(dir, { ...row, name }));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function localDeleteConnector(subject: string, id: string, dir = agentsDir()): Promise<DeletedConnector> {
|
|
255
|
+
const row = rowOrThrow(subject, id, dir);
|
|
256
|
+
writeRows(dir, readLocalConnectors(dir).filter((r) => r.id !== id));
|
|
257
|
+
return Promise.resolve({ id: row.id, name: row.name });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const inflight = new Map<string, Promise<OAuthAuth>>();
|
|
261
|
+
|
|
262
|
+
function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Promise<OAuthAuth> {
|
|
263
|
+
const running = inflight.get(row.id);
|
|
264
|
+
if (running !== undefined) return running;
|
|
265
|
+
const job = refreshOAuth(auth, parseConnectorUrl(row.url).toString())
|
|
266
|
+
.then((fresh) => {
|
|
267
|
+
replace(dir, { ...row, config: { ...row.config, auth: fresh } });
|
|
268
|
+
return fresh;
|
|
269
|
+
})
|
|
270
|
+
.finally(() => {
|
|
271
|
+
inflight.delete(row.id);
|
|
272
|
+
});
|
|
273
|
+
inflight.set(row.id, job);
|
|
274
|
+
return job;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boolean, dir: string): Promise<RelayTarget> {
|
|
278
|
+
if (!force && !oauthExpired(auth)) return { kind: 'ok', url: row.url, headers: bearerHeaders(auth.accessToken) };
|
|
279
|
+
try {
|
|
280
|
+
const fresh = await refreshOnce(row, auth, dir);
|
|
281
|
+
return { kind: 'ok', url: row.url, headers: bearerHeaders(fresh.accessToken) };
|
|
282
|
+
} catch (err) {
|
|
283
|
+
log.warn({ id: row.id, err: errMsg(err) }, 'local relay: token refresh failed');
|
|
284
|
+
return unrefreshedTarget(row.url, auth, force);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function localRelayTarget(
|
|
289
|
+
connectorId: string,
|
|
290
|
+
force: boolean,
|
|
291
|
+
dir = agentsDir(),
|
|
292
|
+
): Promise<RelayTarget> {
|
|
293
|
+
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
294
|
+
if (row === undefined) return { kind: 'missing' };
|
|
295
|
+
parseConnectorUrl(row.url);
|
|
296
|
+
const auth = row.config.auth;
|
|
297
|
+
if (auth.kind === 'oauth') return oauthTarget(row, auth, force, dir);
|
|
298
|
+
return fixedTarget(row.url, auth, force);
|
|
299
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
+
import { whyUnreachable } from './reach.js';
|
|
4
|
+
import { refused } from './url.js';
|
|
5
|
+
import { authHeaders, type ConnectorAuth } from './verify.js';
|
|
6
|
+
|
|
7
|
+
export interface RemoteTool {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
readOnly: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TIMEOUT_MS = 15_000;
|
|
14
|
+
const MAX_PAGES = 10;
|
|
15
|
+
const ACCEPT = 'application/json, text/event-stream';
|
|
16
|
+
|
|
17
|
+
function dataOf(text: string, contentType: string): string {
|
|
18
|
+
if (!contentType.includes('text/event-stream')) return text;
|
|
19
|
+
for (const line of text.replace(/\r\n/g, '\n').split('\n')) if (line.startsWith('data:')) return line.slice(5).trim();
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function post(url: URL, auth: ConnectorAuth, session: Map<string, string>, body: unknown): Promise<Response> {
|
|
24
|
+
try {
|
|
25
|
+
return await fetch(url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
redirect: 'manual',
|
|
28
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
29
|
+
headers: { 'content-type': 'application/json', accept: ACCEPT, ...authHeaders(auth), ...Object.fromEntries(session) },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw refused(`Metro could not reach ${url.hostname}: ${whyUnreachable(err)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resultOf(text: string, contentType: string): Record<string, unknown> | null {
|
|
38
|
+
let parsed: unknown = null;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(dataOf(text, contentType));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function rpc(url: URL, auth: ConnectorAuth, session: Map<string, string>, body: unknown): Promise<Record<string, unknown> | null> {
|
|
48
|
+
const res = await post(url, auth, session, body);
|
|
49
|
+
const id = res.headers.get('mcp-session-id');
|
|
50
|
+
if (id !== null && id !== '') session.set('mcp-session-id', id);
|
|
51
|
+
const text = await res.text().catch(() => '');
|
|
52
|
+
if (res.status === 401 || res.status === 403) throw refused(`${url.hostname} rejected the credential; connect it again`);
|
|
53
|
+
if (!res.ok) throw refused(`${url.hostname} answered ${String(res.status)}`);
|
|
54
|
+
return resultOf(text, res.headers.get('content-type') ?? '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toTool(raw: unknown): RemoteTool | null {
|
|
58
|
+
if (!isRecord(raw) || typeof raw.name !== 'string') return null;
|
|
59
|
+
const annotations = isRecord(raw.annotations) ? raw.annotations : {};
|
|
60
|
+
const title = typeof raw.title === 'string' && raw.title !== '' ? raw.title : raw.name;
|
|
61
|
+
return { name: title, description: typeof raw.description === 'string' ? raw.description : '', readOnly: annotations.readOnlyHint === true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function endSession(url: URL, auth: ConnectorAuth, session: Map<string, string>): Promise<void> {
|
|
65
|
+
if (!session.has('mcp-session-id')) return;
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(url, { method: 'DELETE', redirect: 'manual', signal: AbortSignal.timeout(TIMEOUT_MS), headers: { ...authHeaders(auth), ...Object.fromEntries(session) } });
|
|
68
|
+
await res.body?.cancel();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.debug({ host: url.hostname, err: errMsg(err) }, 'connector-tools: the remote declined to end the session');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toolsOf = (result: Record<string, unknown>): RemoteTool[] =>
|
|
75
|
+
Array.isArray(result.tools) ? result.tools.flatMap((raw: unknown) => toTool(raw) ?? []) : [];
|
|
76
|
+
|
|
77
|
+
function nextCursor(result: Record<string, unknown>, current: string): string | null {
|
|
78
|
+
const next = result.nextCursor;
|
|
79
|
+
return typeof next !== 'string' || next === '' || next === current ? null : next;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function walkTools(url: URL, auth: ConnectorAuth, session: Map<string, string>): Promise<RemoteTool[]> {
|
|
83
|
+
const tools: RemoteTool[] = [];
|
|
84
|
+
let cursor: string | null = '';
|
|
85
|
+
for (let page = 0; page < MAX_PAGES && cursor !== null; page += 1) {
|
|
86
|
+
const result = await rpc(url, auth, session, { jsonrpc: '2.0', id: 2 + page, method: 'tools/list', params: cursor === '' ? {} : { cursor } });
|
|
87
|
+
if (result === null) break;
|
|
88
|
+
tools.push(...toolsOf(result));
|
|
89
|
+
cursor = nextCursor(result, cursor);
|
|
90
|
+
}
|
|
91
|
+
return tools;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function listRemoteTools(url: URL, auth: ConnectorAuth): Promise<RemoteTool[]> {
|
|
95
|
+
const session = new Map<string, string>();
|
|
96
|
+
const init = await rpc(url, auth, session, {
|
|
97
|
+
jsonrpc: '2.0',
|
|
98
|
+
id: 1,
|
|
99
|
+
method: 'initialize',
|
|
100
|
+
params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'metro', version: '0.1.0' } },
|
|
101
|
+
});
|
|
102
|
+
if (init === null || typeof init.protocolVersion !== 'string') throw refused(`${url.hostname} answered, but it does not speak MCP.`);
|
|
103
|
+
session.set('mcp-protocol-version', init.protocolVersion);
|
|
104
|
+
await rpc(url, auth, session, { jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
105
|
+
const tools = await walkTools(url, auth, session);
|
|
106
|
+
await endSession(url, auth, session);
|
|
107
|
+
return tools;
|
|
108
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
2
|
+
|
|
3
|
+
export class ConnectorVerifyError extends ApiError {}
|
|
4
|
+
|
|
5
|
+
export class ConnectorUnauthorized extends ConnectorVerifyError {}
|
|
6
|
+
|
|
7
|
+
export const POLICY_MESSAGE =
|
|
8
|
+
'Metro connects from its own server, so it cannot reach a URL on your machine. localhost and private addresses are not usable as connectors.';
|
|
9
|
+
|
|
10
|
+
let localAllowed = false;
|
|
11
|
+
|
|
12
|
+
export function allowLocalConnectors(on: boolean): void {
|
|
13
|
+
localAllowed = on;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
17
|
+
const PRIVATE_SUFFIX = /(?:^|\.)(?:localhost|local|internal|flycast)$/;
|
|
18
|
+
|
|
19
|
+
export function refused(message: string): ConnectorVerifyError {
|
|
20
|
+
return new ConnectorVerifyError(message, 400);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hostAllowed(host: string): boolean {
|
|
24
|
+
if (host === '' || host.startsWith('[')) return false;
|
|
25
|
+
if (IPV4.test(host) || !host.includes('.')) return false;
|
|
26
|
+
return !PRIVATE_SUFFIX.test(host);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function schemeAllowed(url: URL): boolean {
|
|
30
|
+
return url.protocol === 'https:' || (localAllowed && url.protocol === 'http:');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function refuseShape(url: URL): void {
|
|
34
|
+
if (!schemeAllowed(url)) throw refused('a connector url must start with https://');
|
|
35
|
+
if (url.username !== '' || url.password !== '')
|
|
36
|
+
throw refused('a connector url must not carry a user:password');
|
|
37
|
+
if (url.hash !== '')
|
|
38
|
+
throw refused('a connector url must not carry a #fragment');
|
|
39
|
+
if (!localAllowed && !hostAllowed(url.hostname.toLowerCase())) throw refused(POLICY_MESSAGE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseConnectorUrl(raw: unknown): URL {
|
|
43
|
+
const text = typeof raw === 'string' ? raw.trim() : '';
|
|
44
|
+
if (text === '') throw refused('a connector url is required');
|
|
45
|
+
let url: URL;
|
|
46
|
+
try {
|
|
47
|
+
url = new URL(text);
|
|
48
|
+
} catch {
|
|
49
|
+
throw refused('that is not a valid url');
|
|
50
|
+
}
|
|
51
|
+
refuseShape(url);
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function connectorUrlText(url: URL): string {
|
|
56
|
+
const text = url.toString();
|
|
57
|
+
if (url.pathname !== '/' || url.search !== '') return text;
|
|
58
|
+
return text.endsWith('/') ? text.slice(0, -1) : text;
|
|
59
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import { whyUnreachable } from './reach.js';
|
|
3
|
+
import { ConnectorUnauthorized, refused } from './url.js';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
ConnectorUnauthorized,
|
|
7
|
+
ConnectorVerifyError,
|
|
8
|
+
connectorUrlText,
|
|
9
|
+
parseConnectorUrl,
|
|
10
|
+
} from './url.js';
|
|
11
|
+
|
|
12
|
+
export interface OAuthTokens {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
expiresAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OAuthAuth extends OAuthTokens {
|
|
19
|
+
kind: 'oauth';
|
|
20
|
+
issuer: string;
|
|
21
|
+
tokenEndpoint: string;
|
|
22
|
+
clientId: string;
|
|
23
|
+
clientSecret?: string;
|
|
24
|
+
scope?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ConnectorAuth =
|
|
28
|
+
| { kind: 'none' }
|
|
29
|
+
| { kind: 'header'; name: string; value: string }
|
|
30
|
+
| OAuthAuth;
|
|
31
|
+
|
|
32
|
+
export interface VerifiedServer {
|
|
33
|
+
server: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface VerifiedRecord extends VerifiedServer {
|
|
37
|
+
at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PROTOCOL_VERSION = '2025-11-25';
|
|
41
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
42
|
+
|
|
43
|
+
const INITIALIZE = {
|
|
44
|
+
jsonrpc: '2.0',
|
|
45
|
+
id: 1,
|
|
46
|
+
method: 'initialize',
|
|
47
|
+
params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'metro', version: '0.1.0' } },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function authHeaders(auth: ConnectorAuth): Record<string, string> {
|
|
51
|
+
if (auth.kind === 'header') return { [auth.name]: auth.value };
|
|
52
|
+
if (auth.kind === 'oauth') return { authorization: `Bearer ${auth.accessToken}` };
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function payloadOf(text: string, contentType: string): string {
|
|
57
|
+
if (!contentType.includes('text/event-stream')) return text;
|
|
58
|
+
for (const line of text.replace(/\r\n/g, '\n').split('\n')) if (line.startsWith('data:')) return line.slice(5).trim();
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function serverNameOf(url: URL, text: string, contentType: string): string {
|
|
63
|
+
let parsed: unknown;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(payloadOf(text, contentType));
|
|
66
|
+
} catch {
|
|
67
|
+
throw refused(`${url.hostname} answered, but it does not speak MCP.`);
|
|
68
|
+
}
|
|
69
|
+
const result = isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
|
|
70
|
+
if (result === null || typeof result.protocolVersion !== 'string') throw refused(`${url.hostname} answered, but it does not speak MCP.`);
|
|
71
|
+
const info = isRecord(result.serverInfo) ? result.serverInfo : {};
|
|
72
|
+
const title = typeof info.title === 'string' ? info.title : '';
|
|
73
|
+
if (title !== '') return title;
|
|
74
|
+
return typeof info.name === 'string' ? info.name : url.hostname;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function post(url: URL, auth: ConnectorAuth): Promise<Response> {
|
|
78
|
+
try {
|
|
79
|
+
return await fetch(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
redirect: 'manual',
|
|
82
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
83
|
+
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...authHeaders(auth) },
|
|
84
|
+
body: JSON.stringify(INITIALIZE),
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw refused(`Metro could not reach ${url.hostname}: ${whyUnreachable(err)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function verifyRemoteMcp(url: URL, auth: ConnectorAuth): Promise<VerifiedServer> {
|
|
92
|
+
const res = await post(url, auth);
|
|
93
|
+
const text = await res.text().catch(() => '');
|
|
94
|
+
if (res.status >= 300 && res.status < 400) throw refused('that url redirects — use the url it redirects to');
|
|
95
|
+
if (res.status === 401 || res.status === 403)
|
|
96
|
+
throw new ConnectorUnauthorized(auth.kind === 'none' ? `${url.hostname} requires authorization.` : `${url.hostname} rejected that credential.`, 400);
|
|
97
|
+
if (res.status === 404 || res.status === 405)
|
|
98
|
+
throw refused(`${url.hostname} did not accept an MCP initialize over HTTP POST. If it is a legacy SSE server, Metro cannot verify it yet.`);
|
|
99
|
+
if (!res.ok) throw refused(`${url.hostname} answered ${String(res.status)} instead of an MCP initialize.`);
|
|
100
|
+
return { server: serverNameOf(url, text, res.headers.get('content-type') ?? '') };
|
|
101
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { ID_RE } from '@metro-labs/core/ids';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolveCachedAttachment } from '@metro-labs/core/stations/attachments';
|
|
5
|
+
import { writeSecure } from '@metro-labs/core/secure-fs';
|
|
6
|
+
|
|
7
|
+
const GRANT_SUFFIX = '.grant';
|
|
8
|
+
|
|
9
|
+
export interface AttachmentGrant {
|
|
10
|
+
token: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
mintedAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function newAttachmentToken(prefix = 'at'): string {
|
|
16
|
+
return `${prefix}_${randomBytes(32).toString('base64url')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseGrant(raw: string): AttachmentGrant | undefined {
|
|
20
|
+
let parsed: unknown;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (typeof parsed !== 'object' || parsed === null) return undefined;
|
|
27
|
+
const { token, agentId, mintedAt } = parsed as Record<string, unknown>;
|
|
28
|
+
if (typeof token !== 'string' || token === '') return undefined;
|
|
29
|
+
if (typeof agentId !== 'string' || !ID_RE.test(agentId)) return undefined;
|
|
30
|
+
return {
|
|
31
|
+
token,
|
|
32
|
+
agentId,
|
|
33
|
+
mintedAt: typeof mintedAt === 'number' ? mintedAt : 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readGrant(path: string): AttachmentGrant | undefined {
|
|
38
|
+
try {
|
|
39
|
+
return parseGrant(readFileSync(`${path}${GRANT_SUFFIX}`, 'utf8'));
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function issueGrant(
|
|
46
|
+
path: string,
|
|
47
|
+
agentId: string,
|
|
48
|
+
prefix?: string,
|
|
49
|
+
): string {
|
|
50
|
+
const existing = readGrant(path);
|
|
51
|
+
if (existing?.agentId === agentId) return existing.token;
|
|
52
|
+
const grant: AttachmentGrant = {
|
|
53
|
+
token: newAttachmentToken(prefix),
|
|
54
|
+
agentId,
|
|
55
|
+
mintedAt: Date.now(),
|
|
56
|
+
};
|
|
57
|
+
writeSecure(`${path}${GRANT_SUFFIX}`, `${JSON.stringify(grant)}\n`);
|
|
58
|
+
return grant.token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sameToken(presented: string, stored: string): boolean {
|
|
62
|
+
const a = Buffer.from(presented, 'utf8');
|
|
63
|
+
const b = Buffer.from(stored, 'utf8');
|
|
64
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function grantAllowsPath(
|
|
68
|
+
path: string,
|
|
69
|
+
owner: string,
|
|
70
|
+
presented: string,
|
|
71
|
+
): boolean {
|
|
72
|
+
if (presented === '') return false;
|
|
73
|
+
const grant = readGrant(path);
|
|
74
|
+
if (grant === undefined) return false;
|
|
75
|
+
if (grant.agentId !== owner) return false;
|
|
76
|
+
return sameToken(presented, grant.token);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readAttachmentGrant(name: string): AttachmentGrant | undefined {
|
|
80
|
+
const path = resolveCachedAttachment(name);
|
|
81
|
+
return path === null ? undefined : readGrant(path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function issueAttachmentGrant(
|
|
85
|
+
name: string,
|
|
86
|
+
agentId: string,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
const path = resolveCachedAttachment(name);
|
|
89
|
+
return path === null ? undefined : issueGrant(path, agentId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function grantAllows(
|
|
93
|
+
name: string,
|
|
94
|
+
owner: string,
|
|
95
|
+
presented: string,
|
|
96
|
+
): boolean {
|
|
97
|
+
const path = resolveCachedAttachment(name);
|
|
98
|
+
return path === null ? false : grantAllowsPath(path, owner, presented);
|
|
99
|
+
}
|