vibedate 0.7.1 → 0.8.0
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/chunk-JBADPOR5.js +15748 -0
- package/dist/{chunk-HDHJLZZG.js → chunk-S7MO5OSO.js} +7 -0
- package/dist/cli.js +32 -424
- package/dist/index.d.ts +4 -319
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +181 -1
- package/dist/mcp.js +8 -2
- package/dist/room-BStsMaeT.d.ts +320 -0
- package/package.json +1 -1
- package/dist/chunk-ZD6JBWEW.js +0 -63
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { VibeEvent } from '@pooriaarab/vibe-core';
|
|
2
|
+
|
|
3
|
+
type Frame = {
|
|
4
|
+
t: 'hello';
|
|
5
|
+
handle: string;
|
|
6
|
+
league: string;
|
|
7
|
+
harness: string;
|
|
8
|
+
verified?: boolean;
|
|
9
|
+
pubkey?: string;
|
|
10
|
+
nonce?: string;
|
|
11
|
+
sig?: string;
|
|
12
|
+
} | {
|
|
13
|
+
t: 'msg';
|
|
14
|
+
id: string;
|
|
15
|
+
text: string;
|
|
16
|
+
at: number;
|
|
17
|
+
} | {
|
|
18
|
+
t: 'typing';
|
|
19
|
+
} | {
|
|
20
|
+
t: 'bye';
|
|
21
|
+
} | {
|
|
22
|
+
t: 'media-start';
|
|
23
|
+
id: string;
|
|
24
|
+
mime: string;
|
|
25
|
+
size: number;
|
|
26
|
+
name: string;
|
|
27
|
+
} | {
|
|
28
|
+
t: 'media-chunk';
|
|
29
|
+
id: string;
|
|
30
|
+
seq: number;
|
|
31
|
+
b64: string;
|
|
32
|
+
} | {
|
|
33
|
+
t: 'media-end';
|
|
34
|
+
id: string;
|
|
35
|
+
} | {
|
|
36
|
+
t: 'rtc-offer';
|
|
37
|
+
sdp: string;
|
|
38
|
+
} | {
|
|
39
|
+
t: 'rtc-answer';
|
|
40
|
+
sdp: string;
|
|
41
|
+
} | {
|
|
42
|
+
t: 'rtc-ice';
|
|
43
|
+
candidate: string;
|
|
44
|
+
};
|
|
45
|
+
/** Convenience union of the three WebRTC signaling frame types (offer / answer
|
|
46
|
+
* / ice). Live A/V runs in the BROWSER via a native RTCPeerConnection; these
|
|
47
|
+
* frames only RELAY signaling over the P2P socket — no media bytes, no native
|
|
48
|
+
* WebRTC dependency in the CLI. */
|
|
49
|
+
type RtcFrame = Extract<Frame, {
|
|
50
|
+
t: `rtc-${string}`;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
interface ReceivedMedia {
|
|
54
|
+
readonly mime: string;
|
|
55
|
+
readonly name: string;
|
|
56
|
+
/** Temp file path holding the reassembled bytes. */
|
|
57
|
+
readonly path: string;
|
|
58
|
+
readonly size: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PeerLink {
|
|
62
|
+
/** The validated identity of the remote peer (from the hello handshake). */
|
|
63
|
+
readonly hello: PeerHello;
|
|
64
|
+
/** Send a line of text as a `msg` frame. */
|
|
65
|
+
send(text: string): void;
|
|
66
|
+
/** Read a file from disk and send it as a chunked media transfer. */
|
|
67
|
+
sendMedia(filePath: string, opts?: {
|
|
68
|
+
mime?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
size: number;
|
|
73
|
+
}>;
|
|
74
|
+
/** Relay one `rtc-*` signaling frame (offer / answer / ice) to the peer.
|
|
75
|
+
* Live media never touches this socket — only SDP / ICE strings do. */
|
|
76
|
+
sendSignal(frame: RtcFrame): void;
|
|
77
|
+
/** Register a callback for each incoming `msg` frame. */
|
|
78
|
+
onMessage(cb: (m: {
|
|
79
|
+
id: string;
|
|
80
|
+
text: string;
|
|
81
|
+
at: number;
|
|
82
|
+
}) => void): void;
|
|
83
|
+
/** Register a callback fired for each fully-reassembled incoming media file. */
|
|
84
|
+
onMedia(cb: (m: ReceivedMedia) => void): void;
|
|
85
|
+
/** Register a callback fired for each incoming `rtc-*` signaling frame. */
|
|
86
|
+
onSignal(cb: (f: RtcFrame) => void): void;
|
|
87
|
+
/** Register a callback fired once when the peer closes the link. */
|
|
88
|
+
onClose(cb: () => void): void;
|
|
89
|
+
/** Omegle "next": write a `bye` frame, then end the socket. */
|
|
90
|
+
close(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Namespace prefix so vibedating topics never collide with other DHT traffic. */
|
|
94
|
+
declare const TOPIC_PREFIX = "vibedate:";
|
|
95
|
+
/**
|
|
96
|
+
* Derive the 32-byte DHT topic for a league bucket. Deterministic: everyone in
|
|
97
|
+
* the same league anywhere in the world hashes to the same topic, which is the
|
|
98
|
+
* entire discovery mechanism. Pure.
|
|
99
|
+
*/
|
|
100
|
+
declare function leagueTopic(leagueName: string): Buffer;
|
|
101
|
+
/**
|
|
102
|
+
* The fields that ever leave the machine over a peer connection: handle, league,
|
|
103
|
+
* harness, and (optionally) the self-asserted usage-verification flag plus the
|
|
104
|
+
* identity proof (pubkey/nonce/sig). NEVER raw usage — no token totals, no logs.
|
|
105
|
+
* `verified`/`pubkey` are undefined for legacy peers that predate them; both
|
|
106
|
+
* `undefined` and `false` display as unverified (~).
|
|
107
|
+
*/
|
|
108
|
+
interface PeerHello {
|
|
109
|
+
readonly handle: string;
|
|
110
|
+
readonly league: string;
|
|
111
|
+
readonly harness: string;
|
|
112
|
+
/**
|
|
113
|
+
* Self-asserted: the sender's usage came from real local logs (see readUsage).
|
|
114
|
+
* Bound to the sender's key by the identity signature when `pubkey` is present.
|
|
115
|
+
*/
|
|
116
|
+
readonly verified?: boolean;
|
|
117
|
+
/** Raw ed25519 public key (64 hex) — the persistent identity this hello signs. */
|
|
118
|
+
readonly pubkey?: string;
|
|
119
|
+
/** Random per-hello nonce (hex) covered by the signature. */
|
|
120
|
+
readonly nonce?: string;
|
|
121
|
+
/** ed25519 signature (128 hex) over `handle|league|harness|verified|nonce`. */
|
|
122
|
+
readonly sig?: string;
|
|
123
|
+
/**
|
|
124
|
+
* LOCAL-DERIVED, never on the wire: true when this hello's signature verified
|
|
125
|
+
* against its pubkey (see classifyHelloIdentity). Marked 🔑 in the UI.
|
|
126
|
+
*/
|
|
127
|
+
readonly identityVerified?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** One-line privacy notice printed before joining the swarm. */
|
|
130
|
+
declare const LIVE_NOTICE = "live discovery: sharing only your handle + league + harness + verified flag + identity pubkey (never raw usage) with same-league peers on the public DHT";
|
|
131
|
+
/**
|
|
132
|
+
* Serialize a hello to the single JSON line sent on connect. Built key-by-key
|
|
133
|
+
* from the allowlist — even if a caller sneaks extra properties onto the
|
|
134
|
+
* object, they cannot leak into the wire format.
|
|
135
|
+
*/
|
|
136
|
+
declare function serializeHandshake(hello: PeerHello): string;
|
|
137
|
+
/**
|
|
138
|
+
* Parse one incoming handshake line. Returns `null` for anything malformed
|
|
139
|
+
* (bad JSON, non-object, missing/oversized handle or league). The result is
|
|
140
|
+
* constructed from an allowlist of keys, so any extra fields a peer sends —
|
|
141
|
+
* in particular any raw-usage field — are ignored and never retained.
|
|
142
|
+
*/
|
|
143
|
+
declare function parseHandshake(raw: string | Buffer): PeerHello | null;
|
|
144
|
+
/** A peer we've shaken hands with, persisted locally. */
|
|
145
|
+
interface StoredPeer extends PeerHello {
|
|
146
|
+
readonly firstSeenAt: string;
|
|
147
|
+
readonly lastSeenAt: string;
|
|
148
|
+
/** LOCAL metadata: when the last `msg` from this peer arrived (never on the wire). */
|
|
149
|
+
readonly lastMessageAt?: string;
|
|
150
|
+
}
|
|
151
|
+
/** Load persisted live peers, or `[]` if none/corrupt. Local-only data. */
|
|
152
|
+
declare function loadPeers(dir?: string): StoredPeer[];
|
|
153
|
+
/**
|
|
154
|
+
* Record a successfully handshaken peer, keyed by handle (a peer may reconnect
|
|
155
|
+
* from a different key). Returns whether this handle is NEW (first time seen).
|
|
156
|
+
*/
|
|
157
|
+
declare function recordPeer(hello: PeerHello, dir?: string, now?: Date): {
|
|
158
|
+
peer: StoredPeer;
|
|
159
|
+
isNew: boolean;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Stamp `lastMessageAt` on a stored peer (a `msg` just arrived from them).
|
|
163
|
+
* Local metadata only; never on the wire. Returns false when the handle isn't
|
|
164
|
+
* a known peer. Never throws — best-effort bookkeeping.
|
|
165
|
+
*/
|
|
166
|
+
declare function recordPeerMessage(handle: string, dir?: string, now?: Date): boolean;
|
|
167
|
+
/** Injection point for the match notification (defaults to vibe-core notify). */
|
|
168
|
+
type NotifySink = (event: VibeEvent) => void;
|
|
169
|
+
interface DiscoveryOptions {
|
|
170
|
+
/** What we broadcast. Must already be consent-gated by the caller. */
|
|
171
|
+
readonly hello: PeerHello;
|
|
172
|
+
/**
|
|
173
|
+
* Override the joined topic (tests pass a random one on an isolated DHT).
|
|
174
|
+
* Defaults to {@link leagueTopic}`(hello.league)`. Ignored when {@link topics}
|
|
175
|
+
* is set.
|
|
176
|
+
*/
|
|
177
|
+
readonly topic?: Buffer;
|
|
178
|
+
/**
|
|
179
|
+
* ALL topics to join on the one swarm (e.g. your league + adjacent leagues),
|
|
180
|
+
* so thin pools and cross-league friends still connect. Every topic is
|
|
181
|
+
* joined, refreshed, and left on close. Defaults to `[topic]` — i.e. a single
|
|
182
|
+
* own-league topic (the legacy behavior).
|
|
183
|
+
*/
|
|
184
|
+
readonly topics?: readonly Buffer[];
|
|
185
|
+
/**
|
|
186
|
+
* Predicate over an incoming peer's advertised league. Defaults to EXACT
|
|
187
|
+
* match against `hello.league` — the same privacy invariant as before. Widen
|
|
188
|
+
* it (e.g. ±1 adjacency via {@link leaguesWithin}) to accept cross-league
|
|
189
|
+
* peers that arrive on a shared topic.
|
|
190
|
+
*/
|
|
191
|
+
readonly acceptLeague?: (peerLeague: string) => boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Predicate over an incoming peer's advertised handle. A blocked peer's hello
|
|
194
|
+
* is DROPPED exactly like a wrong-league one — never recorded to peers.json,
|
|
195
|
+
* never notified, never handed to `onLink`/pairing. Default: nothing blocked.
|
|
196
|
+
* The CLI passes one backed by the persisted blocklist (~/.vibedating).
|
|
197
|
+
*/
|
|
198
|
+
readonly isBlocked?: (handle: string) => boolean;
|
|
199
|
+
/** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
|
|
200
|
+
readonly bootstrap?: ReadonlyArray<{
|
|
201
|
+
readonly host: string;
|
|
202
|
+
readonly port: number;
|
|
203
|
+
}>;
|
|
204
|
+
/** Where peers.json lives. Defaults to ~/.vibedating. */
|
|
205
|
+
readonly stateDir?: string;
|
|
206
|
+
/** Called after each accepted handshake; `isNew` = first time this handle is seen. */
|
|
207
|
+
readonly onPeer?: (peer: PeerHello, isNew: boolean) => void;
|
|
208
|
+
/**
|
|
209
|
+
* Called once per connection with a live {@link PeerLink} over the same socket
|
|
210
|
+
* (the hello was frame #1; subsequent frames flow to the link). Omit for the
|
|
211
|
+
* plain `discover` behavior (no live chat). Existing discovery behavior is
|
|
212
|
+
* unchanged when this is absent.
|
|
213
|
+
*/
|
|
214
|
+
readonly onLink?: (link: PeerLink) => void;
|
|
215
|
+
/** Match-notification sink (tests capture with a fake). Best-effort. */
|
|
216
|
+
readonly notify?: NotifySink;
|
|
217
|
+
}
|
|
218
|
+
interface DiscoverySession {
|
|
219
|
+
/** The primary (first) joined topic. See {@link topics} for the full set. */
|
|
220
|
+
readonly topic: Buffer;
|
|
221
|
+
/** Every topic this session joined (primary first). */
|
|
222
|
+
readonly topics: readonly Buffer[];
|
|
223
|
+
/** What we broadcast on every connection. */
|
|
224
|
+
readonly hello: PeerHello;
|
|
225
|
+
/** Live peer set, keyed by the remote's public key (hex). */
|
|
226
|
+
readonly peers: ReadonlyMap<string, PeerHello>;
|
|
227
|
+
/** Resolves when the first DHT announce/lookup round for every topic completes. */
|
|
228
|
+
readonly ready: Promise<unknown>;
|
|
229
|
+
/** Leave every topic and destroy the node. Idempotent. */
|
|
230
|
+
close(): Promise<void>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Join the swarm on the league topic and handshake with every peer that
|
|
234
|
+
* connects. CONSENT GATE LIVES WITH THE CALLER — never call this without the
|
|
235
|
+
* `share:live` grant (or an explicit `--live` opt-in in the same breath).
|
|
236
|
+
*/
|
|
237
|
+
declare function startDiscovery(opts: DiscoveryOptions): Promise<DiscoverySession>;
|
|
238
|
+
|
|
239
|
+
/** Namespace prefix so room topics never collide with league topics (or anything
|
|
240
|
+
* else on the DHT). Mirrors {@link p2p.TOPIC_PREFIX} for the 1:1 path. */
|
|
241
|
+
declare const ROOM_TOPIC_PREFIX = "vibedate-room:";
|
|
242
|
+
/**
|
|
243
|
+
* Derive the 32-byte DHT topic for a named room. Deterministic: everyone who
|
|
244
|
+
* joins the same room name anywhere in the world hashes to the same topic,
|
|
245
|
+
* which is the entire discovery mechanism. Pure (mirrors {@link p2p.leagueTopic}).
|
|
246
|
+
*/
|
|
247
|
+
declare function roomTopic(name: string): Buffer;
|
|
248
|
+
/** A room member = a connected, handshaken peer. Same shape as a live peer. */
|
|
249
|
+
type RoomMember = PeerHello;
|
|
250
|
+
/** One group chat message: a `msg` frame's payload tagged with the sender. */
|
|
251
|
+
interface RoomMessage {
|
|
252
|
+
/** Sender's handle (from the validated hello — UNTRUSTED display data). */
|
|
253
|
+
readonly from: string;
|
|
254
|
+
readonly id: string;
|
|
255
|
+
readonly text: string;
|
|
256
|
+
readonly at: number;
|
|
257
|
+
}
|
|
258
|
+
interface RoomOptions {
|
|
259
|
+
/** What we broadcast. Must already be consent-gated by the caller. */
|
|
260
|
+
readonly hello: PeerHello;
|
|
261
|
+
/** Room name → its own DHT topic (see {@link roomTopic}). */
|
|
262
|
+
readonly room: string;
|
|
263
|
+
/** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
|
|
264
|
+
readonly bootstrap?: ReadonlyArray<{
|
|
265
|
+
readonly host: string;
|
|
266
|
+
readonly port: number;
|
|
267
|
+
}>;
|
|
268
|
+
/** Where peers.json lives. Defaults to ~/.vibedating. */
|
|
269
|
+
readonly stateDir?: string;
|
|
270
|
+
/** Predicate over a blocked handle — a blocked peer's hello is dropped exactly
|
|
271
|
+
* like in 1:1 discovery. Default: nothing blocked. */
|
|
272
|
+
readonly isBlocked?: (handle: string) => boolean;
|
|
273
|
+
/** Match-notification sink (tests capture with a fake). Best-effort. */
|
|
274
|
+
readonly notify?: NotifySink;
|
|
275
|
+
}
|
|
276
|
+
interface RoomSession {
|
|
277
|
+
/** The room name. */
|
|
278
|
+
readonly room: string;
|
|
279
|
+
/** The joined DHT topic (see {@link roomTopic}). */
|
|
280
|
+
readonly topic: Buffer;
|
|
281
|
+
/** What we broadcast. */
|
|
282
|
+
readonly hello: PeerHello;
|
|
283
|
+
/** Live member set, keyed by handle (excludes self). Mirrors the live view
|
|
284
|
+
* semantics of {@link p2p.DiscoverySession.peers}. */
|
|
285
|
+
readonly members: ReadonlyMap<string, RoomMember>;
|
|
286
|
+
/** Resolves when the first DHT announce/lookup round completes. */
|
|
287
|
+
readonly ready: Promise<unknown>;
|
|
288
|
+
/**
|
|
289
|
+
* Broadcast a text message to ALL room members (fan-out: one `msg` frame per
|
|
290
|
+
* member's {@link PeerLink}). Returns the handles the message was sent to.
|
|
291
|
+
* Best-effort: a member whose link has just closed is skipped silently.
|
|
292
|
+
*/
|
|
293
|
+
broadcast(text: string): readonly string[];
|
|
294
|
+
/** Register a callback fired for each incoming group message (with sender). */
|
|
295
|
+
onMessage(cb: (m: RoomMessage) => void): void;
|
|
296
|
+
/** Register a callback fired whenever the member roster changes (join/leave). */
|
|
297
|
+
onRoster(cb: (members: readonly RoomMember[]) => void): void;
|
|
298
|
+
/** Register a callback fired for each incoming `rtc-*` signaling frame,
|
|
299
|
+
* tagged with the sender's handle (full-mesh video signaling). */
|
|
300
|
+
onSignal(cb: (from: string, frame: RtcFrame) => void): void;
|
|
301
|
+
/** Relay one `rtc-*` signaling frame to one member (by handle). */
|
|
302
|
+
sendSignal(handle: string, frame: RtcFrame): void;
|
|
303
|
+
/** The underlying {@link PeerLink} to a member (by handle), or undefined. */
|
|
304
|
+
linkFor(handle: string): PeerLink | undefined;
|
|
305
|
+
/** Leave the room and destroy the node. Idempotent. */
|
|
306
|
+
close(): Promise<void>;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Join (or create) a named room on the DHT and discover ALL members. CONSENT
|
|
310
|
+
* GATE LIVES WITH THE CALLER — never call this without the `share:live` grant
|
|
311
|
+
* (or an explicit opt-in in the same breath), exactly like
|
|
312
|
+
* {@link p2p.startDiscovery}.
|
|
313
|
+
*
|
|
314
|
+
* Resolves once the first DHT announce/lookup round completes; the returned
|
|
315
|
+
* session's {@link RoomSession.members} map + {@link RoomSession.onRoster}
|
|
316
|
+
* callback track every member that joins or leaves thereafter.
|
|
317
|
+
*/
|
|
318
|
+
declare function startRoom(opts: RoomOptions): Promise<RoomSession>;
|
|
319
|
+
|
|
320
|
+
export { type DiscoveryOptions as D, LIVE_NOTICE as L, type PeerHello as P, ROOM_TOPIC_PREFIX as R, type StoredPeer as S, TOPIC_PREFIX as T, type PeerLink as a, type DiscoverySession as b, type RoomMember as c, type RoomMessage as d, type RoomOptions as e, type RoomSession as f, loadPeers as g, recordPeerMessage as h, roomTopic as i, startDiscovery as j, startRoom as k, leagueTopic as l, parseHandshake as p, recordPeer as r, serializeHandshake as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibedate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/dist/chunk-ZD6JBWEW.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CANDIDATES,
|
|
3
|
-
loadProfile,
|
|
4
|
-
matches
|
|
5
|
-
} from "./chunk-HDHJLZZG.js";
|
|
6
|
-
|
|
7
|
-
// src/mcp.ts
|
|
8
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
-
function textBlock(text) {
|
|
11
|
-
return { type: "text", text };
|
|
12
|
-
}
|
|
13
|
-
var VERSION = "0.1.0";
|
|
14
|
-
async function runMcp() {
|
|
15
|
-
const mcp = new McpServer({ name: "vibedating", version: VERSION });
|
|
16
|
-
mcp.tool(
|
|
17
|
-
"profile",
|
|
18
|
-
"Your vibedating league, computed locally from your token usage. Raw usage never leaves the machine \u2014 only the league bucket is shared. Requires `vibedating connect` to have run.",
|
|
19
|
-
() => {
|
|
20
|
-
const p = loadProfile();
|
|
21
|
-
if (!p) {
|
|
22
|
-
return {
|
|
23
|
-
content: [
|
|
24
|
-
textBlock("Not connected. Run `vibedating connect` first to compute your league.")
|
|
25
|
-
]
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const lines = [
|
|
29
|
-
`handle: ${p.handle}`,
|
|
30
|
-
`harness: ${p.harness}`,
|
|
31
|
-
`league: ${p.league} League`,
|
|
32
|
-
`verified: ${p.verified ? "true (real local usage)" : "false (self-reported or demo)"}`,
|
|
33
|
-
"privacy: raw token usage is local-only; only the league bucket is shared."
|
|
34
|
-
];
|
|
35
|
-
return { content: [textBlock(lines.join("\n"))] };
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
mcp.tool(
|
|
39
|
-
"matches",
|
|
40
|
-
"Candidates in your league (same or adjacent tier) from the local seeded demo pool. No central directory in v0.",
|
|
41
|
-
() => {
|
|
42
|
-
const p = loadProfile();
|
|
43
|
-
if (!p) {
|
|
44
|
-
return {
|
|
45
|
-
content: [textBlock("Not connected. Run `vibedating connect` first.")]
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const list = matches(p.league, CANDIDATES);
|
|
49
|
-
if (list.length === 0) {
|
|
50
|
-
return { content: [textBlock(`No candidates in range for the ${p.league} League.`)] };
|
|
51
|
-
}
|
|
52
|
-
const body = [`Matches for ${p.league} League (${list.length}):`];
|
|
53
|
-
for (const c of list) body.push(`- ${c.handle} (${c.league} League)`);
|
|
54
|
-
return { content: [textBlock(body.join("\n"))] };
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
const transport = new StdioServerTransport();
|
|
58
|
-
await mcp.connect(transport);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export {
|
|
62
|
-
runMcp
|
|
63
|
-
};
|