@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.21
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 +103 -0
- package/dist/cli.js +134 -565
- package/dist/control.js +102 -0
- package/dist/prompt.js +10 -0
- package/dist/runtime.js +94 -0
- package/dist/store.js +70 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -22
- package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -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 +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -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 +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -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 +113 -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 +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -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 +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -78
- package/dist/channels/discord.js +0 -148
- package/dist/channels/telegram.js +0 -137
- package/dist/lib/address.js +0 -21
- package/dist/lib/codex-rc.js +0 -274
- package/dist/lib/dotenv.js +0 -31
- package/dist/log.js +0 -12
- package/dist/paths.js +0 -45
- package/dist/tail.js +0 -161
- package/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type {
|
|
3
|
+
StreamableHTTPServerTransport,
|
|
4
|
+
EventId,
|
|
5
|
+
} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import type { BoundedEventStore } from './event-store.js';
|
|
8
|
+
|
|
9
|
+
const STANDALONE_STREAM_ID = '_GET_stream';
|
|
10
|
+
const KEEPALIVE_MS = 15_000;
|
|
11
|
+
const RECONNECT_RETRY_MS = 15_000;
|
|
12
|
+
const COMMENT = ':\n\n';
|
|
13
|
+
const RETRY_DIRECTIVE = `retry: ${RECONNECT_RETRY_MS}\n\n`;
|
|
14
|
+
|
|
15
|
+
interface StreamEntry {
|
|
16
|
+
controller: { enqueue: (chunk: Uint8Array) => void };
|
|
17
|
+
encoder: { encode: (input: string) => Uint8Array };
|
|
18
|
+
cleanup: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WebTransport {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
_initialized?: boolean;
|
|
24
|
+
_streamMapping?: Map<string, StreamEntry>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AdaptedTransport {
|
|
28
|
+
_webStandardTransport?: WebTransport;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const web = (
|
|
32
|
+
transport: StreamableHTTPServerTransport,
|
|
33
|
+
): WebTransport | undefined =>
|
|
34
|
+
(transport as unknown as AdaptedTransport)._webStandardTransport;
|
|
35
|
+
|
|
36
|
+
export const isStandaloneGet = (req: IncomingMessage): boolean => {
|
|
37
|
+
if (req.method !== 'GET') return false;
|
|
38
|
+
const accept = req.headers.accept;
|
|
39
|
+
const value = Array.isArray(accept) ? accept.join(',') : (accept ?? '');
|
|
40
|
+
return value.includes('text/event-stream');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const headerValue = (
|
|
44
|
+
req: IncomingMessage,
|
|
45
|
+
name: string,
|
|
46
|
+
): string | undefined => {
|
|
47
|
+
const raw = req.headers[name];
|
|
48
|
+
if (Array.isArray(raw)) return raw[0];
|
|
49
|
+
return typeof raw === 'string' ? raw : undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface RawGetSink {
|
|
53
|
+
closed: boolean;
|
|
54
|
+
attach: (transport: StreamableHTTPServerTransport) => void;
|
|
55
|
+
close: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ValidateResult {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
status?: number;
|
|
61
|
+
message?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateStandaloneSession(
|
|
65
|
+
transport: StreamableHTTPServerTransport,
|
|
66
|
+
req: IncomingMessage,
|
|
67
|
+
): ValidateResult {
|
|
68
|
+
const inner = web(transport);
|
|
69
|
+
if (!inner) return { ok: false, status: 500, message: 'transport not ready' };
|
|
70
|
+
if (!inner._initialized)
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
status: 400,
|
|
74
|
+
message: 'Bad Request: Server not initialized',
|
|
75
|
+
};
|
|
76
|
+
const presented = headerValue(req, 'mcp-session-id');
|
|
77
|
+
if (!presented)
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
status: 400,
|
|
81
|
+
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
82
|
+
};
|
|
83
|
+
if (presented !== inner.sessionId)
|
|
84
|
+
return { ok: false, status: 404, message: 'Session not found' };
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ServeOpts {
|
|
89
|
+
transport: StreamableHTTPServerTransport;
|
|
90
|
+
eventStore: BoundedEventStore;
|
|
91
|
+
scope: Set<string>;
|
|
92
|
+
req: IncomingMessage;
|
|
93
|
+
res: ServerResponse;
|
|
94
|
+
log: (...a: unknown[]) => void;
|
|
95
|
+
registerSink: (sink: RawGetSink | undefined) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function serveStandaloneGet(opts: ServeOpts): Promise<void> {
|
|
99
|
+
const { transport, eventStore, scope, req, res, log, registerSink } = opts;
|
|
100
|
+
const sessionId = web(transport)?.sessionId;
|
|
101
|
+
|
|
102
|
+
res.writeHead(200, {
|
|
103
|
+
'content-type': 'text/event-stream',
|
|
104
|
+
'cache-control': 'no-cache, no-transform',
|
|
105
|
+
connection: 'keep-alive',
|
|
106
|
+
'x-accel-buffering': 'no',
|
|
107
|
+
...(sessionId ? { 'mcp-session-id': sessionId } : {}),
|
|
108
|
+
});
|
|
109
|
+
res.flushHeaders?.();
|
|
110
|
+
res.write(RETRY_DIRECTIVE);
|
|
111
|
+
res.write(COMMENT);
|
|
112
|
+
|
|
113
|
+
let active = transport;
|
|
114
|
+
const timers: { keepalive?: ReturnType<typeof setInterval> } = {};
|
|
115
|
+
const closeStream = (): void => {
|
|
116
|
+
if (sink.closed) return;
|
|
117
|
+
sink.closed = true;
|
|
118
|
+
if (timers.keepalive) clearInterval(timers.keepalive);
|
|
119
|
+
web(active)?._streamMapping?.delete(STANDALONE_STREAM_ID);
|
|
120
|
+
registerSink(undefined);
|
|
121
|
+
try {
|
|
122
|
+
res.end();
|
|
123
|
+
} catch {
|
|
124
|
+
log('raw-get-stream: end failed');
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const sink: RawGetSink = {
|
|
128
|
+
closed: false,
|
|
129
|
+
attach: (next: StreamableHTTPServerTransport): void => {
|
|
130
|
+
active = next;
|
|
131
|
+
const entry: StreamEntry = {
|
|
132
|
+
controller: {
|
|
133
|
+
enqueue: (chunk: Uint8Array): void => {
|
|
134
|
+
if (sink.closed) return;
|
|
135
|
+
if (res.destroyed || res.writableEnded) {
|
|
136
|
+
closeStream();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
res.write(Buffer.from(chunk));
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
encoder: { encode: (input: string): Uint8Array => Buffer.from(input) },
|
|
143
|
+
cleanup: closeStream,
|
|
144
|
+
};
|
|
145
|
+
web(next)?._streamMapping?.set(STANDALONE_STREAM_ID, entry);
|
|
146
|
+
},
|
|
147
|
+
close: closeStream,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const lastEventId = headerValue(req, 'last-event-id');
|
|
151
|
+
if (lastEventId) {
|
|
152
|
+
await eventStore.replayEventsAfter(lastEventId, {
|
|
153
|
+
scope,
|
|
154
|
+
send: (id: EventId, message: JSONRPCMessage): Promise<void> => {
|
|
155
|
+
if (!sink.closed)
|
|
156
|
+
res.write(`event: message\nid: ${id}\ndata: ${JSON.stringify(message)}\n\n`);
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
},
|
|
159
|
+
onWithheld: (id: EventId, line: string | undefined): void => {
|
|
160
|
+
log(
|
|
161
|
+
'raw-get-stream: withheld replay (frame outside the reconnecting session scope)',
|
|
162
|
+
'id',
|
|
163
|
+
id,
|
|
164
|
+
'line',
|
|
165
|
+
line ?? '(none)',
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sink.attach(transport);
|
|
172
|
+
registerSink(sink);
|
|
173
|
+
|
|
174
|
+
timers.keepalive = setInterval(() => {
|
|
175
|
+
if (sink.closed) return;
|
|
176
|
+
if (res.destroyed || res.writableEnded) {
|
|
177
|
+
closeStream();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
res.write(COMMENT);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
log('raw-get-stream: keepalive write failed', err);
|
|
184
|
+
closeStream();
|
|
185
|
+
}
|
|
186
|
+
}, KEEPALIVE_MS);
|
|
187
|
+
timers.keepalive.unref?.();
|
|
188
|
+
|
|
189
|
+
res.on('close', closeStream);
|
|
190
|
+
res.on('error', closeStream);
|
|
191
|
+
req.on('close', closeStream);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ChannelGetOpts extends ServeOpts {
|
|
195
|
+
previous: RawGetSink | undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function serveChannelGet(opts: ChannelGetOpts): Promise<boolean> {
|
|
199
|
+
const check = validateStandaloneSession(opts.transport, opts.req);
|
|
200
|
+
if (!check.ok) {
|
|
201
|
+
opts.res.writeHead(check.status ?? 400).end(check.message ?? 'bad request');
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (opts.previous && !opts.previous.closed) opts.previous.close();
|
|
205
|
+
await serveStandaloneGet(opts);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import { verifySession } from '../daemon/session.js';
|
|
4
|
+
import { agentIdForKey } from '../db/key-map.js';
|
|
5
|
+
|
|
6
|
+
export type RequestIdentity =
|
|
7
|
+
| { kind: 'agent'; agentId: string }
|
|
8
|
+
| { kind: 'google'; email: string; agentIds: string[] };
|
|
9
|
+
|
|
10
|
+
const storage = new AsyncLocalStorage<RequestIdentity>();
|
|
11
|
+
|
|
12
|
+
export function runWithIdentity<T>(
|
|
13
|
+
identity: RequestIdentity,
|
|
14
|
+
fn: () => Promise<T>,
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
return storage.run(identity, fn);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function currentIdentity(): RequestIdentity | undefined {
|
|
20
|
+
return storage.getStore();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthConfig {
|
|
24
|
+
sessionSecret: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function authConfigFromEnv(): AuthConfig {
|
|
28
|
+
return { sessionSecret: process.env.METRO_SESSION_SECRET?.trim() ?? '' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractToken(req: IncomingMessage): string | undefined {
|
|
32
|
+
const header = req.headers.authorization;
|
|
33
|
+
if (header?.startsWith('Bearer ')) {
|
|
34
|
+
const t = header.slice(7).trim();
|
|
35
|
+
if (t) return t;
|
|
36
|
+
}
|
|
37
|
+
const qt = new URL(req.url ?? '/', 'http://localhost').searchParams.get(
|
|
38
|
+
'token',
|
|
39
|
+
);
|
|
40
|
+
return qt ?? undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const looksLikeJwt = (token: string): boolean =>
|
|
44
|
+
/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token);
|
|
45
|
+
|
|
46
|
+
export function authenticate(
|
|
47
|
+
req: IncomingMessage,
|
|
48
|
+
cfg: AuthConfig,
|
|
49
|
+
): RequestIdentity | null {
|
|
50
|
+
const token = extractToken(req);
|
|
51
|
+
if (!token) return null;
|
|
52
|
+
|
|
53
|
+
const agentId = agentIdForKey(token);
|
|
54
|
+
if (agentId !== undefined) return { kind: 'agent', agentId };
|
|
55
|
+
|
|
56
|
+
if (cfg.sessionSecret !== '' && looksLikeJwt(token)) {
|
|
57
|
+
try {
|
|
58
|
+
const { email, agentIds } = verifySession(token, cfg.sessionSecret);
|
|
59
|
+
return { kind: 'google', email, agentIds };
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function allowedAgents(
|
|
68
|
+
identity: RequestIdentity | undefined,
|
|
69
|
+
): Set<string> {
|
|
70
|
+
if (identity?.kind === 'google') return new Set(identity.agentIds);
|
|
71
|
+
if (identity?.kind === 'agent') return new Set([identity.agentId]);
|
|
72
|
+
return new Set();
|
|
73
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { currentBusSeq } from '../daemon/events.js';
|
|
3
|
+
import { errMsg } from '../daemon/log.js';
|
|
4
|
+
import { newReplayLedger, type ReplayLedger } from '../channels/relay.js';
|
|
5
|
+
import { McpSession, channelLog } from './session.js';
|
|
6
|
+
import { agentsInScopeKey } from './session-route.js';
|
|
7
|
+
import { sessionScopeKey, type SessionOwnership } from './session-route.js';
|
|
8
|
+
import type { RequestIdentity } from './request-identity.js';
|
|
9
|
+
|
|
10
|
+
export const MAX_SESSIONS = 64;
|
|
11
|
+
export const SESSION_IDLE_MS = 10 * 60_000;
|
|
12
|
+
export const LEDGER_MAX = 256;
|
|
13
|
+
const SWEEP_MS = 60_000;
|
|
14
|
+
|
|
15
|
+
export interface AgentLiveness {
|
|
16
|
+
connected: boolean;
|
|
17
|
+
lastSeenAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SessionCapacityError extends Error {
|
|
21
|
+
constructor() {
|
|
22
|
+
super('too many live MCP sessions');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SessionRegistry {
|
|
27
|
+
private readonly byId = new Map<string, McpSession>();
|
|
28
|
+
private readonly byScope = new Map<string, McpSession>();
|
|
29
|
+
private readonly ledgers = new Map<string, ReplayLedger>();
|
|
30
|
+
private readonly sweeper: ReturnType<typeof setInterval>;
|
|
31
|
+
private readonly log: (...a: unknown[]) => void;
|
|
32
|
+
private started = false;
|
|
33
|
+
private floor = 0;
|
|
34
|
+
|
|
35
|
+
constructor(log: (...a: unknown[]) => void = channelLog) {
|
|
36
|
+
this.log = log;
|
|
37
|
+
this.sweeper = setInterval(() => {
|
|
38
|
+
this.sweep(Date.now());
|
|
39
|
+
}, SWEEP_MS);
|
|
40
|
+
this.sweeper.unref?.();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get size(): number {
|
|
44
|
+
return this.byId.size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get(sessionId: string): McpSession | undefined {
|
|
48
|
+
return this.byId.get(sessionId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
liveness(): Map<string, AgentLiveness> {
|
|
52
|
+
const out = new Map<string, AgentLiveness>();
|
|
53
|
+
for (const session of this.byId.values())
|
|
54
|
+
for (const id of agentsInScopeKey(session.scopeKey)) {
|
|
55
|
+
const prev = out.get(id);
|
|
56
|
+
out.set(id, {
|
|
57
|
+
connected: (prev?.connected ?? false) || session.streamAttached,
|
|
58
|
+
lastSeenAt: Math.max(prev?.lastSeenAt ?? 0, session.lastSeenAt),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
forScope(scopeKey: string): McpSession | undefined {
|
|
65
|
+
return this.byScope.get(scopeKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ownership(
|
|
69
|
+
presented: string | undefined,
|
|
70
|
+
scopeKey: string,
|
|
71
|
+
): SessionOwnership {
|
|
72
|
+
if (presented === undefined) return 'none';
|
|
73
|
+
const found = this.byId.get(presented);
|
|
74
|
+
if (!found) return 'none';
|
|
75
|
+
return found.scopeKey === scopeKey ? 'mine' : 'theirs';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
startInbound(): void {
|
|
79
|
+
if (this.started) return;
|
|
80
|
+
this.started = true;
|
|
81
|
+
this.floor = currentBusSeq();
|
|
82
|
+
for (const session of this.byId.values()) session.startChannel();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async create(
|
|
86
|
+
identity: RequestIdentity,
|
|
87
|
+
adoptId?: string,
|
|
88
|
+
): Promise<McpSession> {
|
|
89
|
+
const scopeKey = sessionScopeKey(identity);
|
|
90
|
+
const id = adoptId ?? randomUUID();
|
|
91
|
+
await this.byScope.get(scopeKey)?.close();
|
|
92
|
+
await this.byId.get(id)?.close();
|
|
93
|
+
await this.makeRoom();
|
|
94
|
+
const session = await McpSession.open({
|
|
95
|
+
id,
|
|
96
|
+
scopeKey,
|
|
97
|
+
adopted: adoptId !== undefined,
|
|
98
|
+
ledger: this.ledgerFor(scopeKey),
|
|
99
|
+
onClosed: (s) => {
|
|
100
|
+
this.forget(s);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.byId.set(id, session);
|
|
104
|
+
this.byScope.set(scopeKey, session);
|
|
105
|
+
if (this.started) session.startChannel();
|
|
106
|
+
this.log('session: opened', 'id', id, 'scope', scopeKey, 'live', this.byId.size);
|
|
107
|
+
return session;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async closeScope(scopeKey: string): Promise<boolean> {
|
|
111
|
+
const session = this.byScope.get(scopeKey);
|
|
112
|
+
if (!session) return false;
|
|
113
|
+
this.log('session: closed, credential rotated', 'id', session.id, 'scope', scopeKey);
|
|
114
|
+
await session.close();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async closeAll(): Promise<void> {
|
|
119
|
+
clearInterval(this.sweeper);
|
|
120
|
+
for (const session of [...this.byId.values()]) await session.close();
|
|
121
|
+
this.ledgers.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sweep(now: number): number {
|
|
125
|
+
let reaped = 0;
|
|
126
|
+
for (const session of [...this.byId.values()]) {
|
|
127
|
+
if (session.streamAttached) continue;
|
|
128
|
+
if (now - session.lastSeenAt <= SESSION_IDLE_MS) continue;
|
|
129
|
+
this.log('session: reaped idle', 'id', session.id, 'scope', session.scopeKey);
|
|
130
|
+
session.close().catch((err: unknown) => {
|
|
131
|
+
this.log('session: reap close failed', 'id', session.id, errMsg(err));
|
|
132
|
+
});
|
|
133
|
+
reaped += 1;
|
|
134
|
+
}
|
|
135
|
+
return reaped;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private forget(session: McpSession): void {
|
|
139
|
+
if (this.byId.get(session.id) === session) this.byId.delete(session.id);
|
|
140
|
+
if (this.byScope.get(session.scopeKey) === session)
|
|
141
|
+
this.byScope.delete(session.scopeKey);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async makeRoom(): Promise<void> {
|
|
145
|
+
if (this.byId.size < MAX_SESSIONS) return;
|
|
146
|
+
this.sweep(Date.now());
|
|
147
|
+
if (this.byId.size < MAX_SESSIONS) return;
|
|
148
|
+
const idle = [...this.byId.values()]
|
|
149
|
+
.filter((s) => !s.streamAttached)
|
|
150
|
+
.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
|
|
151
|
+
.at(0);
|
|
152
|
+
if (!idle) throw new SessionCapacityError();
|
|
153
|
+
await idle.close();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private ledgerFor(scopeKey: string): ReplayLedger {
|
|
157
|
+
const found = this.ledgers.get(scopeKey);
|
|
158
|
+
if (found) {
|
|
159
|
+
this.ledgers.delete(scopeKey);
|
|
160
|
+
this.ledgers.set(scopeKey, found);
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
const fresh = newReplayLedger(this.started ? this.floor : currentBusSeq());
|
|
164
|
+
this.ledgers.set(scopeKey, fresh);
|
|
165
|
+
while (this.ledgers.size > LEDGER_MAX) {
|
|
166
|
+
const oldest = this.ledgers.keys().next();
|
|
167
|
+
if (oldest.done) break;
|
|
168
|
+
this.ledgers.delete(oldest.value);
|
|
169
|
+
}
|
|
170
|
+
return fresh;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { allowedAgents, type RequestIdentity } from './request-identity.js';
|
|
2
|
+
|
|
3
|
+
export function sessionScopeKey(identity: RequestIdentity): string {
|
|
4
|
+
const ids = [...allowedAgents(identity)].sort();
|
|
5
|
+
if (ids.length > 0) return `${AGENTS_PREFIX}${ids.join(',')}`;
|
|
6
|
+
return `email:${identity.kind === 'google' ? identity.email : identity.agentId}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AGENTS_PREFIX = 'agents:';
|
|
10
|
+
|
|
11
|
+
export function agentsInScopeKey(scopeKey: string): string[] {
|
|
12
|
+
if (!scopeKey.startsWith(AGENTS_PREFIX)) return [];
|
|
13
|
+
return scopeKey.slice(AGENTS_PREFIX.length).split(',').filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SessionOwnership = 'none' | 'mine' | 'theirs';
|
|
17
|
+
|
|
18
|
+
export type SessionRoute =
|
|
19
|
+
| { kind: 'create'; adoptId?: string }
|
|
20
|
+
| { kind: 'use' }
|
|
21
|
+
| { kind: 'reject'; status: number; message: string };
|
|
22
|
+
|
|
23
|
+
export interface SessionRouteInput {
|
|
24
|
+
isInitialize: boolean;
|
|
25
|
+
presented: string | undefined;
|
|
26
|
+
ownership: SessionOwnership;
|
|
27
|
+
hasOwnSession: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NO_SESSION_HEADER = 'Bad Request: Mcp-Session-Id header is required';
|
|
31
|
+
const NOT_FOUND = 'Session not found';
|
|
32
|
+
|
|
33
|
+
export function routeSession(input: SessionRouteInput): SessionRoute {
|
|
34
|
+
if (input.isInitialize) return { kind: 'create' };
|
|
35
|
+
if (input.presented === undefined)
|
|
36
|
+
return input.hasOwnSession
|
|
37
|
+
? { kind: 'use' }
|
|
38
|
+
: { kind: 'reject', status: 400, message: NO_SESSION_HEADER };
|
|
39
|
+
if (input.ownership === 'mine') return { kind: 'use' };
|
|
40
|
+
if (input.ownership === 'theirs')
|
|
41
|
+
return { kind: 'reject', status: 404, message: NOT_FOUND };
|
|
42
|
+
return { kind: 'create', adoptId: input.presented };
|
|
43
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { InboundRelay } from '../channels/inbound.js';
|
|
4
|
+
import { ChannelRelay, type ReplayLedger } from '../channels/relay.js';
|
|
5
|
+
import { errMsg } from '../daemon/log.js';
|
|
6
|
+
import {
|
|
7
|
+
allowlistForLine,
|
|
8
|
+
senderMatchesAllowlist,
|
|
9
|
+
} from '../db/agent-map.js';
|
|
10
|
+
import { accountStationNames } from '../stations/registry.js';
|
|
11
|
+
import { MCP_INSTRUCTIONS } from './tool-schemas.js';
|
|
12
|
+
import { ChannelOwner } from './channel-owner.js';
|
|
13
|
+
import { BoundedEventStore } from './event-store.js';
|
|
14
|
+
import { registerPermissionRelay } from './permission-relay.js';
|
|
15
|
+
import { registerToolHandlers, toolSchemaSignature } from './tool-dispatch.js';
|
|
16
|
+
import { web, type RawGetSink } from './raw-get-stream.js';
|
|
17
|
+
import type { RequestIdentity } from './request-identity.js';
|
|
18
|
+
|
|
19
|
+
export const channelLog = (...a: unknown[]): void => {
|
|
20
|
+
console.error('[metro-mcp]', ...a);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getStations = (): Set<string> => new Set(accountStationNames());
|
|
24
|
+
|
|
25
|
+
const senderAllowed = (from: string, line: string): boolean => {
|
|
26
|
+
const allowlist = allowlistForLine(line);
|
|
27
|
+
return allowlist ? senderMatchesAllowlist(allowlist, from) : true;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function makeTransport(
|
|
31
|
+
id: string,
|
|
32
|
+
eventStore: BoundedEventStore,
|
|
33
|
+
adopted: boolean,
|
|
34
|
+
): StreamableHTTPServerTransport {
|
|
35
|
+
const t = new StreamableHTTPServerTransport({
|
|
36
|
+
sessionIdGenerator: () => id,
|
|
37
|
+
eventStore,
|
|
38
|
+
});
|
|
39
|
+
if (!adopted) return t;
|
|
40
|
+
const inner = web(t);
|
|
41
|
+
if (inner) {
|
|
42
|
+
inner.sessionId = id;
|
|
43
|
+
inner._initialized = true;
|
|
44
|
+
}
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SessionInit {
|
|
49
|
+
id: string;
|
|
50
|
+
scopeKey: string;
|
|
51
|
+
adopted: boolean;
|
|
52
|
+
ledger: ReplayLedger;
|
|
53
|
+
onClosed: (session: McpSession) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class McpSession {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly scopeKey: string;
|
|
59
|
+
readonly owner = new ChannelOwner();
|
|
60
|
+
readonly eventStore: BoundedEventStore;
|
|
61
|
+
readonly transport: StreamableHTTPServerTransport;
|
|
62
|
+
readonly server: Server;
|
|
63
|
+
readonly relay: InboundRelay;
|
|
64
|
+
readonly channel: ChannelRelay;
|
|
65
|
+
lastSeenAt = Date.now();
|
|
66
|
+
private sink: RawGetSink | undefined;
|
|
67
|
+
private unsubscribe: (() => void) | undefined;
|
|
68
|
+
private closed = false;
|
|
69
|
+
private issuedSchema: string | undefined;
|
|
70
|
+
private announcing = false;
|
|
71
|
+
private readonly onClosed: (session: McpSession) => void;
|
|
72
|
+
|
|
73
|
+
private constructor(init: SessionInit) {
|
|
74
|
+
this.id = init.id;
|
|
75
|
+
this.scopeKey = init.scopeKey;
|
|
76
|
+
this.issuedSchema = init.adopted ? undefined : toolSchemaSignature();
|
|
77
|
+
this.onClosed = init.onClosed;
|
|
78
|
+
this.eventStore = new BoundedEventStore({
|
|
79
|
+
scopeOf: () => this.owner.scope(),
|
|
80
|
+
});
|
|
81
|
+
this.transport = makeTransport(init.id, this.eventStore, init.adopted);
|
|
82
|
+
this.server = new Server(
|
|
83
|
+
{ name: 'metro', version: '0.1.0' },
|
|
84
|
+
{
|
|
85
|
+
capabilities: {
|
|
86
|
+
experimental: {
|
|
87
|
+
'claude/channel': {},
|
|
88
|
+
'claude/channel/permission': {},
|
|
89
|
+
},
|
|
90
|
+
tools: { listChanged: true },
|
|
91
|
+
},
|
|
92
|
+
instructions: MCP_INSTRUCTIONS,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
registerToolHandlers(this.server, {
|
|
96
|
+
markCurrent: (): void => {
|
|
97
|
+
this.issuedSchema = toolSchemaSignature();
|
|
98
|
+
},
|
|
99
|
+
deliver: (send): void => {
|
|
100
|
+
this.deliverSchemaNotice(send, 'request');
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.relay = new InboundRelay({
|
|
104
|
+
mcp: this.server,
|
|
105
|
+
log: channelLog,
|
|
106
|
+
getStations,
|
|
107
|
+
senderAllowed,
|
|
108
|
+
});
|
|
109
|
+
registerPermissionRelay({
|
|
110
|
+
mcp: this.server,
|
|
111
|
+
relay: this.relay,
|
|
112
|
+
owner: this.owner,
|
|
113
|
+
log: channelLog,
|
|
114
|
+
});
|
|
115
|
+
this.channel = new ChannelRelay({
|
|
116
|
+
relay: this.relay,
|
|
117
|
+
log: channelLog,
|
|
118
|
+
inScope: (line) => this.owner.inScope(line),
|
|
119
|
+
ledger: init.ledger,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static async open(init: SessionInit): Promise<McpSession> {
|
|
124
|
+
const session = new McpSession(init);
|
|
125
|
+
await session.server.connect(session.transport);
|
|
126
|
+
session.server.onclose = (): void => {
|
|
127
|
+
session.close().catch((err: unknown) => {
|
|
128
|
+
channelLog('session: close failed', 'id', init.id, errMsg(err));
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
return session;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get streamAttached(): boolean {
|
|
135
|
+
return this.sink !== undefined && !this.sink.closed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get currentSink(): RawGetSink | undefined {
|
|
139
|
+
return this.sink;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
touch(): void {
|
|
143
|
+
this.lastSeenAt = Date.now();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
startChannel(): void {
|
|
147
|
+
this.unsubscribe ??= this.channel.start();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
bindSink(sink: RawGetSink | undefined, identity: RequestIdentity): void {
|
|
151
|
+
this.sink = sink;
|
|
152
|
+
if (sink === undefined) {
|
|
153
|
+
this.owner.releaseStream();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.owner.bindStream(identity);
|
|
157
|
+
this.announceToolSchema();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private get schemaNoticeDue(): boolean {
|
|
161
|
+
return this.issuedSchema !== toolSchemaSignature();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private announceToolSchema(): void {
|
|
165
|
+
if (!this.streamAttached) return;
|
|
166
|
+
this.deliverSchemaNotice(
|
|
167
|
+
() => this.server.sendToolListChanged(),
|
|
168
|
+
'stream',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private deliverSchemaNotice(send: () => Promise<void>, via: string): void {
|
|
173
|
+
if (!this.schemaNoticeDue || this.announcing) return;
|
|
174
|
+
this.announcing = true;
|
|
175
|
+
channelLog(
|
|
176
|
+
'session: tool list changed',
|
|
177
|
+
'id',
|
|
178
|
+
this.id,
|
|
179
|
+
'scope',
|
|
180
|
+
this.scopeKey,
|
|
181
|
+
'via',
|
|
182
|
+
via,
|
|
183
|
+
);
|
|
184
|
+
send()
|
|
185
|
+
.then(() => {
|
|
186
|
+
this.issuedSchema = toolSchemaSignature();
|
|
187
|
+
this.announcing = false;
|
|
188
|
+
})
|
|
189
|
+
.catch((err: unknown) => {
|
|
190
|
+
this.announcing = false;
|
|
191
|
+
channelLog('session: tool list changed notice failed', errMsg(err));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dropStream(): void {
|
|
196
|
+
const sink = this.sink;
|
|
197
|
+
this.sink = undefined;
|
|
198
|
+
this.owner.releaseStream();
|
|
199
|
+
sink?.close();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async close(): Promise<void> {
|
|
203
|
+
if (this.closed) return;
|
|
204
|
+
this.closed = true;
|
|
205
|
+
this.onClosed(this);
|
|
206
|
+
this.dropStream();
|
|
207
|
+
this.unsubscribe?.();
|
|
208
|
+
this.unsubscribe = undefined;
|
|
209
|
+
await this.server.close().catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
}
|