rentline-sandbox 0.1.2

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/server.js ADDED
@@ -0,0 +1,844 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ ListPromptsRequestSchema,
10
+ GetPromptRequestSchema,
11
+ ListResourcesRequestSchema,
12
+ ReadResourceRequestSchema
13
+ } from "@modelcontextprotocol/sdk/types.js";
14
+ import { readFileSync, existsSync } from "fs";
15
+ import { join, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+
18
+ // src/client.ts
19
+ async function request(opts, method, path, body) {
20
+ const url = `${opts.apiUrl.replace(/\/$/, "")}${path}`;
21
+ const headers = {
22
+ "Content-Type": "application/json"
23
+ };
24
+ if (opts.apiKey) {
25
+ headers["X-API-Key"] = opts.apiKey;
26
+ }
27
+ const res = await fetch(url, {
28
+ method,
29
+ headers,
30
+ body: body !== void 0 ? JSON.stringify(body) : void 0
31
+ });
32
+ if (!res.ok) {
33
+ let detail = res.statusText;
34
+ try {
35
+ const j = await res.json();
36
+ detail = j.detail ?? JSON.stringify(j);
37
+ } catch {
38
+ }
39
+ throw new Error(`[${res.status}] ${detail}`);
40
+ }
41
+ if (res.status === 204) return void 0;
42
+ return res.json();
43
+ }
44
+ function createClient(opts) {
45
+ const r = (method, path, body) => request(opts, method, path, body);
46
+ return {
47
+ // ── Health ───────────────────────────────────────────────────────────────
48
+ health: () => r("GET", "/health"),
49
+ // ── Games ────────────────────────────────────────────────────────────────
50
+ listGames: () => r("GET", "/api/sandbox/games"),
51
+ getGame: (id) => r("GET", `/api/sandbox/games/${id}`),
52
+ createGame: (body) => r("POST", "/api/sandbox/games", body),
53
+ joinGame: (id, body) => r("POST", `/api/sandbox/games/${id}/join`, body),
54
+ leaveGame: (id) => r("DELETE", `/api/sandbox/games/${id}/leave`),
55
+ markReady: (id) => r("POST", `/api/sandbox/games/${id}/ready`),
56
+ advanceTurn: (id) => r("POST", `/api/sandbox/games/${id}/advance-turn`),
57
+ getFeed: (id, params) => {
58
+ const q = new URLSearchParams();
59
+ if (params?.turn !== void 0) q.set("turn", String(params.turn));
60
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
61
+ const qs = q.toString() ? `?${q.toString()}` : "";
62
+ return r("GET", `/api/sandbox/games/${id}/feed${qs}`);
63
+ },
64
+ getLeaderboard: (id) => r("GET", `/api/sandbox/games/${id}/leaderboard`),
65
+ getGlobalLeaderboard: (limit = 50) => r("GET", `/api/sandbox/leaderboard?limit=${limit}`),
66
+ // ── Portfolio / debt ─────────────────────────────────────────────────────
67
+ getPortfolio: (gameId, playerId) => r("GET", `/api/sandbox/games/${gameId}/portfolio/${playerId}`),
68
+ getDebt: (gameId, playerId) => r("GET", `/api/sandbox/games/${gameId}/debt/${playerId}`),
69
+ // ── Trading ──────────────────────────────────────────────────────────────
70
+ trade: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/trade`, body),
71
+ // ── Mortgage ─────────────────────────────────────────────────────────────
72
+ originateMortgage: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/mortgage`, body),
73
+ refi: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/refi`, body),
74
+ helocDraw: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/heloc/draw`, body),
75
+ helocRepay: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/heloc/repay`, body),
76
+ prepayPrincipal: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/prepay-principal`, body),
77
+ improveProperty: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/improve-property`, body),
78
+ originatePaceLien: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/pace-lien`, body),
79
+ // ── Fed ──────────────────────────────────────────────────────────────────
80
+ getFedHistory: (gameId) => r("GET", `/api/sandbox/games/${gameId}/fed`),
81
+ // ── Property pool (admin) ────────────────────────────────────────────────
82
+ listProperties: (activeOnly = true) => r("GET", `/api/sandbox/properties?active_only=${activeOnly}`),
83
+ syncProperties: () => r("POST", "/api/sandbox/properties/sync"),
84
+ mintTusdc: (gameId, playerId, amount) => r("POST", `/api/sandbox/games/${gameId}/mint-tusdc`, { player_id: playerId, amount }),
85
+ // ── Bots ─────────────────────────────────────────────────────────────────
86
+ addBot: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/bots`, body),
87
+ removeBot: (gameId, botPlayerId) => r("DELETE", `/api/sandbox/games/${gameId}/bots/${botPlayerId}`),
88
+ // ── Autonomous mode ───────────────────────────────────────────────────────
89
+ startAutonomous: (gameId, delaySeconds) => r("POST", `/api/sandbox/games/${gameId}/autonomous`, { delay_seconds: delaySeconds ?? 30 }),
90
+ stopAutonomous: (gameId) => r("DELETE", `/api/sandbox/games/${gameId}/autonomous`),
91
+ // ── Agent delegation ──────────────────────────────────────────────────────
92
+ setDelegate: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/delegate`, body),
93
+ // ── API keys ──────────────────────────────────────────────────────────────
94
+ createApiKey: (name = "CLI key") => r("POST", "/api/sandbox/api-keys", { name }),
95
+ // ── Market & Intel ────────────────────────────────────────────────────────
96
+ getMarketSummary: (gameId) => r("GET", `/api/sandbox/games/${gameId}/market-summary`),
97
+ getPlayerActions: (gameId, playerId, limit, turn) => r("GET", `/api/sandbox/games/${gameId}/players/${playerId}/actions${limit || turn ? `?${new URLSearchParams({ ...limit ? { limit: String(limit) } : {}, ...turn !== void 0 ? { turn: String(turn) } : {} })}` : ""}`),
98
+ spectate: (gameId) => r("GET", `/api/sandbox/games/${gameId}/spectate`),
99
+ createGameFromPreset: (body) => r("POST", "/api/sandbox/games/from-preset", body)
100
+ };
101
+ }
102
+
103
+ // src/tools.ts
104
+ var ALL_TOOLS = [
105
+ // ── GAME ─────────────────────────────────────────────────────────────────
106
+ {
107
+ name: "list_games",
108
+ title: "List Open Games",
109
+ category: "game",
110
+ description: "List all open sandbox game rooms (status: lobby, trading, or advancing).",
111
+ inputSchema: { type: "object", properties: {}, required: [] }
112
+ },
113
+ {
114
+ name: "get_game",
115
+ title: "Get Game State",
116
+ category: "game",
117
+ description: "Get full game state including players, property pool, current turn, Fed rate, and LTV settings.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: { game_id: { type: "string", description: "Game ID" } },
121
+ required: ["game_id"]
122
+ }
123
+ },
124
+ {
125
+ name: "create_game",
126
+ title: "Create Game",
127
+ category: "game",
128
+ description: "Create a new game room. Configure mortgage rules (LTV, rate type, amortizing), Fed meeting schedule, and starting balance. Returns the game with its invite_code for sharing.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ name: { type: "string", description: "Game room name" },
133
+ display_name: { type: "string", description: "Your display name in this game" },
134
+ max_turns: { type: "integer", description: "Max turns (default 12)", minimum: 3, maximum: 50 },
135
+ starting_balance_usdc: { type: "number", description: "Starting tUSDC per player (default 100000)" },
136
+ ltv_limit: { type: "number", description: "Max loan-to-value ratio 0.0-0.95 (default 0.70)" },
137
+ default_rate_type: { type: "string", enum: ["fixed", "arm"], description: "Default mortgage rate type" },
138
+ amortizing: { type: "boolean", description: "Amortizing mortgages (default: interest-only)" },
139
+ fed_meeting_interval: { type: "integer", description: "Fed meetings every N turns (0=disabled, default 6)" },
140
+ fed_rate_current: { type: "number", description: "Starting Fed funds rate (default 0.055 = 5.5%)" },
141
+ property_ids: { type: "array", items: { type: "string" }, description: "Specific property IDs to include (default: all active)" },
142
+ bots: {
143
+ type: "array",
144
+ description: "Bot players to add at creation. Each item: {display_name, strategy?, personality?}",
145
+ items: {
146
+ type: "object",
147
+ properties: {
148
+ display_name: { type: "string" },
149
+ strategy: { type: "string", enum: ["aggressive", "conservative", "balanced", "momentum", "income"] },
150
+ personality: { type: "string" }
151
+ },
152
+ required: ["display_name"]
153
+ }
154
+ },
155
+ auto_advance: {
156
+ type: "boolean",
157
+ description: "Start autonomous mode immediately after game creation (default false)"
158
+ },
159
+ auto_advance_delay_seconds: {
160
+ type: "integer",
161
+ description: "Seconds between automatic turn advances when auto_advance=true (5\u20133600, default 30)",
162
+ minimum: 5,
163
+ maximum: 3600
164
+ }
165
+ },
166
+ required: ["name", "display_name"]
167
+ }
168
+ },
169
+ {
170
+ name: "join_game",
171
+ title: "Join Game",
172
+ category: "game",
173
+ description: "Join an open game room using an invite code. Returns your player_id.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ game_id: { type: "string" },
178
+ invite_code: { type: "string", description: "8-character invite code from the host" },
179
+ display_name: { type: "string", description: "Your display name in this game" }
180
+ },
181
+ required: ["game_id", "invite_code", "display_name"]
182
+ }
183
+ },
184
+ {
185
+ name: "mark_ready",
186
+ title: "Mark Ready",
187
+ category: "game",
188
+ description: "Toggle your ready state. When all players are ready, the host can advance the turn.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: { game_id: { type: "string" } },
192
+ required: ["game_id"]
193
+ }
194
+ },
195
+ {
196
+ name: "advance_turn",
197
+ title: "Advance Turn (Host)",
198
+ category: "game",
199
+ description: "Advance the game by one turn. Runs all engine phases: Fed meeting check \u2192 macro events \u2192 rent collection \u2192 random events \u2192 market move \u2192 debt service \u2192 distribute yield \u2192 open trade window. Host only.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: { game_id: { type: "string" } },
203
+ required: ["game_id"]
204
+ }
205
+ },
206
+ {
207
+ name: "get_feed",
208
+ title: "Get Turn Feed",
209
+ category: "game",
210
+ description: "Get the event feed for a game. Shows Fed decisions, macro events (recession, disaster, tax hike, etc.), rent payments, price moves, debt service, and defaults. Optionally filter by turn.",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {
214
+ game_id: { type: "string" },
215
+ turn: { type: "integer", description: "Filter by turn number (optional)" },
216
+ limit: { type: "integer", description: "Max events to return (default 30)", maximum: 200 }
217
+ },
218
+ required: ["game_id"]
219
+ }
220
+ },
221
+ // ── MARKET ───────────────────────────────────────────────────────────────
222
+ {
223
+ name: "list_properties",
224
+ title: "List Property Pool",
225
+ category: "market",
226
+ description: "List all active properties in the sandbox pool with prices, rents, and cap rates.",
227
+ inputSchema: { type: "object", properties: {}, required: [] }
228
+ },
229
+ {
230
+ name: "get_fed_history",
231
+ title: "Get Fed Decision History",
232
+ category: "market",
233
+ description: "Get the FOMC decision history for a game: all rate hikes, cuts, and holds with basis point moves, rate levels, and statement flavour text. Fed meets every N turns on a predictable schedule.",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: { game_id: { type: "string" } },
237
+ required: ["game_id"]
238
+ }
239
+ },
240
+ // ── TRADE ────────────────────────────────────────────────────────────────
241
+ {
242
+ name: "buy_tokens",
243
+ title: "Buy Property Tokens",
244
+ category: "trade",
245
+ description: "Buy fractional property tokens at the current market price (all-cash, no mortgage). Game must be in 'trading' status.",
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ game_id: { type: "string" },
250
+ property_id: { type: "string", description: "Property ID from the game pool" },
251
+ tokens: { type: "number", description: "Number of tokens to buy (fractions allowed)", exclusiveMinimum: 0 }
252
+ },
253
+ required: ["game_id", "property_id", "tokens"]
254
+ }
255
+ },
256
+ {
257
+ name: "sell_tokens",
258
+ title: "Sell Property Tokens",
259
+ category: "trade",
260
+ description: "Sell fractional property tokens back to the pool at the current market price.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ game_id: { type: "string" },
265
+ property_id: { type: "string" },
266
+ tokens: { type: "number", exclusiveMinimum: 0 }
267
+ },
268
+ required: ["game_id", "property_id", "tokens"]
269
+ }
270
+ },
271
+ // ── DEBT ─────────────────────────────────────────────────────────────────
272
+ {
273
+ name: "originate_mortgage",
274
+ title: "Originate Acquisition Mortgage",
275
+ category: "debt",
276
+ description: "Buy property tokens using an acquisition mortgage. You pay down_payment + closing_costs in cash; the rest is financed up to the game's LTV limit. Supports fixed-rate and ARM.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ game_id: { type: "string" },
281
+ property_id: { type: "string" },
282
+ tokens_to_buy: { type: "number", exclusiveMinimum: 0 },
283
+ rate_type: { type: "string", enum: ["fixed", "arm"], description: "Rate type (default: game default)" }
284
+ },
285
+ required: ["game_id", "property_id", "tokens_to_buy"]
286
+ }
287
+ },
288
+ {
289
+ name: "refi_mortgage",
290
+ title: "Refinance Mortgage",
291
+ category: "debt",
292
+ description: "Refinance the existing first lien. cash_out_amount=0 for rate-and-term (just reset rate); cash_out_amount>0 for cash-out refi (net proceeds after closing costs credited to balance). New rate uses current Fed rate + spread.",
293
+ inputSchema: {
294
+ type: "object",
295
+ properties: {
296
+ game_id: { type: "string" },
297
+ property_id: { type: "string" },
298
+ cash_out_amount: { type: "number", description: "Cash to extract (0 = rate-and-term refi)", minimum: 0 },
299
+ new_rate_type: { type: "string", enum: ["fixed", "arm"] }
300
+ },
301
+ required: ["game_id", "property_id"]
302
+ }
303
+ },
304
+ {
305
+ name: "heloc_draw",
306
+ title: "Draw HELOC",
307
+ category: "debt",
308
+ description: "Draw from a HELOC (home equity line of credit) against a property you own. Opens a new HELOC if none exists. Credit limit = (current_price \xD7 LTV) - first_lien_balance. Proceeds credited to your tUSDC balance immediately.",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ game_id: { type: "string" },
313
+ property_id: { type: "string" },
314
+ draw_amount: { type: "number", exclusiveMinimum: 0 }
315
+ },
316
+ required: ["game_id", "property_id", "draw_amount"]
317
+ }
318
+ },
319
+ {
320
+ name: "heloc_repay",
321
+ title: "Repay HELOC",
322
+ category: "debt",
323
+ description: "Repay drawn HELOC balance, reducing the outstanding balance and future monthly interest cost.",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ game_id: { type: "string" },
328
+ property_id: { type: "string" },
329
+ repay_amount: { type: "number", exclusiveMinimum: 0 }
330
+ },
331
+ required: ["game_id", "property_id", "repay_amount"]
332
+ }
333
+ },
334
+ {
335
+ name: "prepay_principal",
336
+ title: "Prepay Principal",
337
+ category: "debt",
338
+ description: "Make a partial or full principal prepayment against an active mortgage. Reduces the loan balance immediately and recalculates the monthly payment. Works for interest-only, amortizing, HELOC, PACE, and mechanics lien.",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ game_id: { type: "string" },
343
+ property_id: { type: "string" },
344
+ amount: { type: "number", exclusiveMinimum: 0, description: "Amount to prepay against principal" },
345
+ mortgage_type: {
346
+ type: "string",
347
+ enum: ["first_lien", "acquisition", "refi", "heloc", "pace", "mechanics_lien"],
348
+ description: "Which lien to target."
349
+ }
350
+ },
351
+ required: ["game_id", "property_id", "amount"]
352
+ }
353
+ },
354
+ {
355
+ name: "improve_property",
356
+ title: "Improve Property",
357
+ category: "debt",
358
+ description: "Fund a property grade upgrade out of pocket (cash-funded). Cost = steps \xD7 upgrade_cost_pct \xD7 price. Grade improves immediately, boosting rent and appreciation. May trigger a mechanics lien if cash runs low.",
359
+ inputSchema: {
360
+ type: "object",
361
+ properties: {
362
+ game_id: { type: "string" },
363
+ property_id: { type: "string" },
364
+ target_grade: {
365
+ type: "string",
366
+ enum: ["A", "B", "C", "D"],
367
+ description: "Target grade. Must be higher than current grade."
368
+ }
369
+ },
370
+ required: ["game_id", "property_id", "target_grade"]
371
+ }
372
+ },
373
+ {
374
+ name: "originate_pace_lien",
375
+ title: "Originate PACE Lien",
376
+ category: "debt",
377
+ description: "Finance a property grade upgrade via a PACE lien (no down payment). Grade and price improve immediately. Loan is serviced via debt service each turn at base_rate + pace_spread.",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ game_id: { type: "string" },
382
+ property_id: { type: "string" },
383
+ target_grade: {
384
+ type: "string",
385
+ enum: ["A", "B", "C", "D"],
386
+ description: "Target grade after PACE-funded improvement."
387
+ }
388
+ },
389
+ required: ["game_id", "property_id", "target_grade"]
390
+ }
391
+ },
392
+ {
393
+ name: "get_debt",
394
+ title: "Get Debt Summary",
395
+ category: "debt",
396
+ description: "List all mortgages (acquisition, refi, HELOC) for a player: current balances, rates, monthly payments, LTV, and arrears status.",
397
+ inputSchema: {
398
+ type: "object",
399
+ properties: {
400
+ game_id: { type: "string" },
401
+ player_id: { type: "string" }
402
+ },
403
+ required: ["game_id", "player_id"]
404
+ }
405
+ },
406
+ // ── INTEL ────────────────────────────────────────────────────────────────
407
+ {
408
+ name: "get_portfolio",
409
+ title: "Get Portfolio",
410
+ category: "intel",
411
+ description: "Get a player's full portfolio: token holdings with P&L, unrealised gains, total yield received, cash balance, total debt, gross asset value, NAV, and leverage ratio.",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ game_id: { type: "string" },
416
+ player_id: { type: "string" }
417
+ },
418
+ required: ["game_id", "player_id"]
419
+ }
420
+ },
421
+ {
422
+ name: "get_leaderboard",
423
+ title: "Get Leaderboard",
424
+ category: "intel",
425
+ description: "Get the leaderboard for a game (ranked by NAV = cash + holdings value - debt). Omit game_id to get the all-time global leaderboard across all completed games.",
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ game_id: { type: "string", description: "Game ID (omit for global leaderboard)" },
430
+ limit: { type: "integer", description: "Max entries for global leaderboard (default 50)" }
431
+ },
432
+ required: []
433
+ }
434
+ },
435
+ // ── BOTS ─────────────────────────────────────────────────────────────────
436
+ {
437
+ name: "add_bot",
438
+ title: "Add Bot Player",
439
+ category: "game",
440
+ description: "Add an LLM-driven bot player to a game that is still in lobby status. The bot will automatically make investment decisions each turn after advance_turn is called. Requires being the game host.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ game_id: { type: "string" },
445
+ display_name: { type: "string", description: "Bot's display name, e.g. 'Warren Buffett'" },
446
+ strategy: {
447
+ type: "string",
448
+ enum: ["aggressive", "conservative", "balanced", "momentum", "income"],
449
+ description: "Investment strategy persona for the bot (default: balanced)"
450
+ },
451
+ personality: {
452
+ type: "string",
453
+ maxLength: 80,
454
+ description: "Optional flavour description for the bot's character (max 80 chars)"
455
+ }
456
+ },
457
+ required: ["game_id", "display_name"]
458
+ }
459
+ },
460
+ {
461
+ name: "remove_bot",
462
+ title: "Remove Bot Player",
463
+ category: "game",
464
+ description: "Remove a bot player from a game that is still in lobby status. Requires being the game host.",
465
+ inputSchema: {
466
+ type: "object",
467
+ properties: {
468
+ game_id: { type: "string" },
469
+ bot_player_id: { type: "string", description: "The player_id of the bot to remove" }
470
+ },
471
+ required: ["game_id", "bot_player_id"]
472
+ }
473
+ },
474
+ // ── AUTONOMOUS MODE ───────────────────────────────────────────────────────
475
+ {
476
+ name: "start_autonomous",
477
+ title: "Start Autonomous Mode",
478
+ category: "game",
479
+ description: "Enable autonomous mode on a game. The API will automatically advance turns at the specified interval until the game completes \u2014 no manual advance-turn calls needed. Perfect for all-bot games. Requires being the game host.",
480
+ inputSchema: {
481
+ type: "object",
482
+ properties: {
483
+ game_id: { type: "string" },
484
+ delay_seconds: {
485
+ type: "integer",
486
+ description: "Seconds between automatic turn advances (5\u20133600, default 30)",
487
+ minimum: 5,
488
+ maximum: 3600
489
+ }
490
+ },
491
+ required: ["game_id"]
492
+ }
493
+ },
494
+ {
495
+ name: "stop_autonomous",
496
+ title: "Stop Autonomous Mode",
497
+ category: "game",
498
+ description: "Disable autonomous mode. The game pauses and waits for manual advance-turn calls. Requires being the game host.",
499
+ inputSchema: {
500
+ type: "object",
501
+ properties: {
502
+ game_id: { type: "string" }
503
+ },
504
+ required: ["game_id"]
505
+ }
506
+ },
507
+ // ── DELEGATION ────────────────────────────────────────────────────────────
508
+ {
509
+ name: "set_delegate",
510
+ title: "Set Agent Delegation",
511
+ category: "game",
512
+ description: "Opt in (or out) of agent delegation. When enabled, an LLM agent will act on your behalf during any turn where you have not traded, taken a mortgage, or marked ready before the turn advances \u2014 in both autonomous and host-manual advance modes.",
513
+ inputSchema: {
514
+ type: "object",
515
+ properties: {
516
+ game_id: { type: "string" },
517
+ agent_delegate: {
518
+ type: "boolean",
519
+ description: "true = opt in to delegation; false = opt out"
520
+ },
521
+ delegate_strategy: {
522
+ type: "string",
523
+ enum: ["aggressive", "conservative", "balanced", "momentum", "income", "value_add"],
524
+ description: "Bot strategy to use when acting as your delegate (default: balanced)"
525
+ }
526
+ },
527
+ required: ["game_id", "agent_delegate"]
528
+ }
529
+ },
530
+ // ── MARKET & INTEL ────────────────────────────────────────────────────────
531
+ {
532
+ name: "get_market_summary",
533
+ title: "Get Market Summary",
534
+ category: "market",
535
+ description: "Snapshot of all properties in a game: grade, price, rent, live cap rate, price delta this turn, vacancy status, and mechanics lien info. Sorted by cap rate descending.",
536
+ inputSchema: {
537
+ type: "object",
538
+ properties: { game_id: { type: "string" } },
539
+ required: ["game_id"]
540
+ }
541
+ },
542
+ {
543
+ name: "get_player_actions",
544
+ title: "Get Player Actions",
545
+ category: "intel",
546
+ description: "Human-readable transaction timeline for a player in a game. Shows buys, sells, debt service, rent, improvements, and more.",
547
+ inputSchema: {
548
+ type: "object",
549
+ properties: {
550
+ game_id: { type: "string" },
551
+ player_id: { type: "string" },
552
+ limit: { type: "integer", minimum: 1, maximum: 200, description: "Max transactions (default 50)" },
553
+ turn: { type: "integer", description: "Filter to a specific turn (optional)" }
554
+ },
555
+ required: ["game_id", "player_id"]
556
+ }
557
+ },
558
+ {
559
+ name: "spectate",
560
+ title: "Spectate Game",
561
+ category: "intel",
562
+ description: "Public game snapshot \u2014 no auth required. Returns leaderboard with tiers, recent feed events, and current property prices.",
563
+ inputSchema: {
564
+ type: "object",
565
+ properties: { game_id: { type: "string" } },
566
+ required: ["game_id"]
567
+ }
568
+ },
569
+ {
570
+ name: "create_game_from_preset",
571
+ title: "Create Game from Preset",
572
+ category: "game",
573
+ description: "Create a game using a named preset configuration. Presets: 'quick' (6 turns, high volatility), 'standard' (12 turns default), 'leveraged' (ARM 80% LTV amortizing), 'distressed' (D/F properties only, judgment liens), 'long_run' (120 turns, 10-year monthly).",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ preset: {
578
+ type: "string",
579
+ enum: ["quick", "standard", "leveraged", "distressed", "long_run"],
580
+ description: "Preset name"
581
+ },
582
+ name: { type: "string", description: "Game room name" },
583
+ display_name: { type: "string", description: "Your display name in this game" },
584
+ starting_balance_usdc: { type: "number", description: "Optional override for starting balance" }
585
+ },
586
+ required: ["preset", "name", "display_name"]
587
+ }
588
+ }
589
+ ];
590
+
591
+ // src/server.ts
592
+ var __filename = fileURLToPath(import.meta.url);
593
+ var __dirname = dirname(__filename);
594
+ function readSkill() {
595
+ for (const p of [join(__dirname, "../SKILL.md"), join(__dirname, "SKILL.md")]) {
596
+ if (existsSync(p)) return readFileSync(p, "utf-8");
597
+ }
598
+ return "# Rentline Sandbox\n\nReal estate investment simulation game engine.";
599
+ }
600
+ var INSTRUCTIONS = `
601
+ You are connected to the Rentline Sandbox game engine \u2014 a turn-based real estate investment simulation.
602
+
603
+ KEY CONCEPTS:
604
+ - Players compete over a pool of tokenised properties using simulated tUSDC
605
+ - Each turn = 1 month. Properties generate rent, prices drift, and macro events fire
606
+ - Fed meetings occur every N turns (configurable) with hike/cut/hold outcomes \u2014 affects ARM rates and new mortgage originations
607
+ - Macro events: recession, housing boom, natural disaster, policy change, tax hike, interest rate moves, rent control, insurance crisis
608
+ - Debt strategies: acquisition mortgage (LTV-limited), cash-out refi, HELOC draw/repay. Fixed and ARM rates
609
+ - NAV = cash balance + (token holdings \xD7 current prices) - outstanding debt
610
+
611
+ WHEN TO USE TOOLS:
612
+ - User wants to play/observe a game \u2192 list_games, get_game, get_feed
613
+ - User wants to buy a property \u2192 buy_tokens (cash) or originate_mortgage (leveraged)
614
+ - User wants to extract equity \u2192 refi_mortgage (cash-out) or heloc_draw
615
+ - User wants to check their position \u2192 get_portfolio, get_debt
616
+ - User wants the scoreboard \u2192 get_leaderboard
617
+ - User wants to see macro/Fed events \u2192 get_feed, get_fed_history
618
+ - User is the host and wants to advance \u2192 advance_turn
619
+
620
+ All tools require a game_id. Most debt/portfolio tools also require a player_id (from join_game or get_game).
621
+ `.trim();
622
+ function ok(data) {
623
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
624
+ }
625
+ function err(msg) {
626
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
627
+ }
628
+ async function startServer() {
629
+ const apiUrl = process.env.SANDBOX_API_URL ?? "http://localhost:6532";
630
+ const apiKey = process.env.SANDBOX_API_KEY;
631
+ const client = createClient({ apiUrl, apiKey });
632
+ const server = new Server(
633
+ { name: "rentline-sandbox", version: "0.1.0" },
634
+ {
635
+ capabilities: {
636
+ tools: { listChanged: false },
637
+ prompts: { listChanged: false },
638
+ resources: { listChanged: false }
639
+ },
640
+ instructions: INSTRUCTIONS
641
+ }
642
+ );
643
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
644
+ tools: ALL_TOOLS.map((t) => ({
645
+ name: t.name,
646
+ title: t.title,
647
+ description: t.description,
648
+ inputSchema: t.inputSchema
649
+ }))
650
+ }));
651
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
652
+ const { name, arguments: args } = req.params;
653
+ const a = args ?? {};
654
+ try {
655
+ switch (name) {
656
+ // ── Game ──────────────────────────────────────────────────────────
657
+ case "list_games":
658
+ return ok(await client.listGames());
659
+ case "get_game":
660
+ return ok(await client.getGame(a.game_id));
661
+ case "create_game":
662
+ return ok(await client.createGame({
663
+ name: a.name,
664
+ display_name: a.display_name,
665
+ max_turns: a.max_turns,
666
+ starting_balance_usdc: a.starting_balance_usdc,
667
+ ltv_limit: a.ltv_limit,
668
+ default_rate_type: a.default_rate_type,
669
+ amortizing: a.amortizing,
670
+ fed_meeting_interval: a.fed_meeting_interval,
671
+ fed_rate_current: a.fed_rate_current,
672
+ property_ids: a.property_ids,
673
+ bots: a.bots,
674
+ auto_advance: a.auto_advance,
675
+ auto_advance_delay_seconds: a.auto_advance_delay_seconds
676
+ }));
677
+ case "join_game":
678
+ return ok(await client.joinGame(a.game_id, {
679
+ invite_code: a.invite_code,
680
+ display_name: a.display_name
681
+ }));
682
+ case "mark_ready":
683
+ return ok(await client.markReady(a.game_id));
684
+ case "advance_turn":
685
+ return ok(await client.advanceTurn(a.game_id));
686
+ case "get_feed":
687
+ return ok(await client.getFeed(a.game_id, {
688
+ turn: a.turn,
689
+ limit: a.limit ?? 30
690
+ }));
691
+ // ── Market ────────────────────────────────────────────────────────
692
+ case "list_properties":
693
+ return ok(await client.listProperties());
694
+ case "get_fed_history":
695
+ return ok(await client.getFedHistory(a.game_id));
696
+ // ── Trade ─────────────────────────────────────────────────────────
697
+ case "buy_tokens":
698
+ return ok(await client.trade(a.game_id, {
699
+ property_id: a.property_id,
700
+ direction: "buy",
701
+ tokens: a.tokens
702
+ }));
703
+ case "sell_tokens":
704
+ return ok(await client.trade(a.game_id, {
705
+ property_id: a.property_id,
706
+ direction: "sell",
707
+ tokens: a.tokens
708
+ }));
709
+ // ── Debt ──────────────────────────────────────────────────────────
710
+ case "originate_mortgage":
711
+ return ok(await client.originateMortgage(a.game_id, {
712
+ property_id: a.property_id,
713
+ tokens_to_buy: a.tokens_to_buy,
714
+ rate_type: a.rate_type
715
+ }));
716
+ case "refi_mortgage":
717
+ return ok(await client.refi(a.game_id, {
718
+ property_id: a.property_id,
719
+ cash_out_amount: a.cash_out_amount ?? 0,
720
+ new_rate_type: a.new_rate_type
721
+ }));
722
+ case "heloc_draw":
723
+ return ok(await client.helocDraw(a.game_id, {
724
+ property_id: a.property_id,
725
+ draw_amount: a.draw_amount
726
+ }));
727
+ case "heloc_repay":
728
+ return ok(await client.helocRepay(a.game_id, {
729
+ property_id: a.property_id,
730
+ repay_amount: a.repay_amount
731
+ }));
732
+ case "prepay_principal":
733
+ return ok(await client.prepayPrincipal(a.game_id, {
734
+ property_id: a.property_id,
735
+ amount: a.amount,
736
+ mortgage_type: a.mortgage_type
737
+ }));
738
+ case "improve_property":
739
+ return ok(await client.improveProperty(a.game_id, {
740
+ property_id: a.property_id,
741
+ target_grade: a.target_grade
742
+ }));
743
+ case "originate_pace_lien":
744
+ return ok(await client.originatePaceLien(a.game_id, {
745
+ property_id: a.property_id,
746
+ target_grade: a.target_grade
747
+ }));
748
+ case "get_debt":
749
+ return ok(await client.getDebt(a.game_id, a.player_id));
750
+ // ── Intel ─────────────────────────────────────────────────────────
751
+ case "get_portfolio":
752
+ return ok(await client.getPortfolio(a.game_id, a.player_id));
753
+ case "get_leaderboard":
754
+ return a.game_id ? ok(await client.getLeaderboard(a.game_id)) : ok(await client.getGlobalLeaderboard(a.limit ?? 50));
755
+ // ── Bots ──────────────────────────────────────────────────────────
756
+ case "add_bot":
757
+ return ok(await client.addBot(a.game_id, {
758
+ display_name: a.display_name,
759
+ strategy: a.strategy,
760
+ personality: a.personality
761
+ }));
762
+ case "remove_bot":
763
+ return ok(await client.removeBot(a.game_id, a.bot_player_id));
764
+ // ── Autonomous mode ────────────────────────────────────────────────
765
+ case "start_autonomous":
766
+ return ok(await client.startAutonomous(
767
+ a.game_id,
768
+ a.delay_seconds
769
+ ));
770
+ case "stop_autonomous":
771
+ return ok(await client.stopAutonomous(a.game_id));
772
+ // ── Delegation ─────────────────────────────────────────────────────
773
+ case "set_delegate":
774
+ return ok(await client.setDelegate(a.game_id, {
775
+ agent_delegate: a.agent_delegate,
776
+ delegate_strategy: a.delegate_strategy
777
+ }));
778
+ // ── Market & Intel ─────────────────────────────────────────────────
779
+ case "get_market_summary":
780
+ return ok(await client.getMarketSummary(a.game_id));
781
+ case "get_player_actions":
782
+ return ok(await client.getPlayerActions(
783
+ a.game_id,
784
+ a.player_id,
785
+ a.limit,
786
+ a.turn
787
+ ));
788
+ case "spectate":
789
+ return ok(await client.spectate(a.game_id));
790
+ case "create_game_from_preset":
791
+ return ok(await client.createGameFromPreset({
792
+ preset: a.preset,
793
+ name: a.name,
794
+ display_name: a.display_name,
795
+ starting_balance_usdc: a.starting_balance_usdc
796
+ }));
797
+ default:
798
+ return err(`Unknown tool: ${name}`);
799
+ }
800
+ } catch (e) {
801
+ return err(e instanceof Error ? e.message : String(e));
802
+ }
803
+ });
804
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
805
+ prompts: [{
806
+ name: "sandbox-context",
807
+ description: "Inject Rentline Sandbox game mechanics context into the conversation"
808
+ }]
809
+ }));
810
+ server.setRequestHandler(GetPromptRequestSchema, async () => ({
811
+ messages: [{ role: "user", content: { type: "text", text: INSTRUCTIONS } }]
812
+ }));
813
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
814
+ resources: [
815
+ {
816
+ uri: "sandbox://skill.md",
817
+ name: "Rentline Sandbox SKILL.md",
818
+ description: "Full skill manifest: triggers, setup, tool reference, game mechanics",
819
+ mimeType: "text/markdown"
820
+ },
821
+ {
822
+ uri: "sandbox://tools",
823
+ name: "Tool definitions",
824
+ description: "All tool names, descriptions, and input schemas",
825
+ mimeType: "application/json"
826
+ }
827
+ ]
828
+ }));
829
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
830
+ const { uri } = req.params;
831
+ if (uri === "sandbox://skill.md") {
832
+ return { contents: [{ uri, mimeType: "text/markdown", text: readSkill() }] };
833
+ }
834
+ if (uri === "sandbox://tools") {
835
+ return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(ALL_TOOLS, null, 2) }] };
836
+ }
837
+ throw new Error(`Unknown resource: ${uri}`);
838
+ });
839
+ const transport = new StdioServerTransport();
840
+ await server.connect(transport);
841
+ }
842
+ export {
843
+ startServer
844
+ };