kill-switch-mcp 1.1.2 → 1.1.3
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/bin/kill-switch-mcp.js +5 -5
- package/dist/server.js +4947 -0
- package/package.json +4 -2
- package/src/api/index.ts +0 -188
- package/src/sdk/actions-helpers.ts +0 -450
- package/src/sdk/actions.ts +0 -3208
- package/src/sdk/formatter.ts +0 -263
- package/src/sdk/index.ts +0 -1313
- package/src/sdk/pathfinding.ts +0 -57
- package/src/sdk/types.ts +0 -584
- package/src/server.ts +0 -1088
- package/src/wallet/index.ts +0 -369
package/src/server.ts
DELETED
|
@@ -1,1088 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Kill Switch MCP Server
|
|
4
|
-
*
|
|
5
|
-
* This is the bridge between Claude Code and the Kill Switch tournament arena.
|
|
6
|
-
* It runs locally on the player's machine and connects to the game server.
|
|
7
|
-
*
|
|
8
|
-
* Tools:
|
|
9
|
-
* - login: Log in, set up, and connect to the game.
|
|
10
|
-
* - execute_code: Send commands to your bot during gameplay.
|
|
11
|
-
* - get_status: Check your bot's current state without executing code.
|
|
12
|
-
* - join_game: Join a free game mode queue.
|
|
13
|
-
* - wait_for_game_start: Wait for the game to begin.
|
|
14
|
-
* - disconnect_bot: Disconnect from the game.
|
|
15
|
-
* - setup_game_wallet: Set up a Tempo wallet access key for paid tournaments.
|
|
16
|
-
* - check_wallet: Check game wallet address and USDC balance.
|
|
17
|
-
* - tournament_schedule: View upcoming paid tournaments.
|
|
18
|
-
* - join_tournament: Join a paid tournament (deposits buy-in from game wallet).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
22
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
-
import {
|
|
24
|
-
CallToolRequestSchema,
|
|
25
|
-
ListToolsRequestSchema
|
|
26
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
-
import { existsSync } from 'fs';
|
|
28
|
-
import { readFile } from 'fs/promises';
|
|
29
|
-
import { join } from 'path';
|
|
30
|
-
import { botManager } from './api/index.js';
|
|
31
|
-
import { formatWorldState } from './sdk/formatter.js';
|
|
32
|
-
import {
|
|
33
|
-
hasGameWallet,
|
|
34
|
-
getWalletInfo,
|
|
35
|
-
generateAccessKey,
|
|
36
|
-
saveAccessKey,
|
|
37
|
-
getTournamentSchedule,
|
|
38
|
-
joinTournament,
|
|
39
|
-
} from './wallet/index.js';
|
|
40
|
-
|
|
41
|
-
// ── Configuration ──
|
|
42
|
-
// Server URL from CLI args or env. Defaults to localhost for local dev.
|
|
43
|
-
// In .mcp.json: { "args": ["run", "mcp/server.ts", "--server", "play.killswitch.gg"] }
|
|
44
|
-
const SERVER_URL = (() => {
|
|
45
|
-
const idx = process.argv.indexOf('--server');
|
|
46
|
-
if (idx !== -1 && process.argv[idx + 1]) return process.argv[idx + 1];
|
|
47
|
-
return process.env.KILL_SWITCH_SERVER || 'localhost';
|
|
48
|
-
})();
|
|
49
|
-
|
|
50
|
-
console.error(`[Kill Switch MCP] Server: ${SERVER_URL}`);
|
|
51
|
-
|
|
52
|
-
// ── Game Knowledge ──
|
|
53
|
-
// This description is what teaches Claude about the game. It's included in tool descriptions
|
|
54
|
-
// so Claude knows the full context without needing to read docs.
|
|
55
|
-
const GAME_DESCRIPTION = `Kill Switch is a Hunger Games-style battle royale where AI agents fight to the death.
|
|
56
|
-
|
|
57
|
-
YOU are the brain of your player's bot. The human talks to you about strategy, and you
|
|
58
|
-
execute it by sending code to the game via execute_code.
|
|
59
|
-
|
|
60
|
-
## How It Works
|
|
61
|
-
1. Call login to register and connect (lands you in the lobby)
|
|
62
|
-
2. Your bot spawns in Barbarian Village lobby with EMPTY inventory
|
|
63
|
-
3. Call join_game with mode "shorty" (or "longy" when available) to queue up
|
|
64
|
-
4. Chat with your human about strategy while waiting
|
|
65
|
-
5. Call wait_for_game_start — it blocks until the admin starts the match
|
|
66
|
-
6. When it returns, you're at the arena edge — run to the center to grab loot!
|
|
67
|
-
|
|
68
|
-
## Game Modes
|
|
69
|
-
- **SHORTY**: Quick battle royale at Draynor Manor. 50 atk/str/def, 99 HP. Loot & fight.
|
|
70
|
-
- **LONGY**: (Coming soon) Large survival map. Level 1 stats, skill up, craft, outlast.
|
|
71
|
-
|
|
72
|
-
## The Arena (Shorty)
|
|
73
|
-
- You spawn on the PERIMETER of Draynor Manor with nothing
|
|
74
|
-
- Items are scattered on the ground in a cornucopia pattern:
|
|
75
|
-
- Outer ring: bronze/iron weapons, leather armor, bread/meat
|
|
76
|
-
- Mid ring: mithril/adamant weapons, chainmail, trout/salmon
|
|
77
|
-
- Center: rune weapons, rune/adamant armor, lobster/swordfish
|
|
78
|
-
- Better items are closer to the center — but so is everyone else!
|
|
79
|
-
|
|
80
|
-
## PERMADEATH
|
|
81
|
-
- If you die, your agent is DEAD FOREVER. No respawns, no second chances.
|
|
82
|
-
- Only the last agent standing wins. Winners earn party hats!
|
|
83
|
-
|
|
84
|
-
## Key Actions
|
|
85
|
-
- bot.pickupItem(/item name/i) — pick up ground items (CRITICAL — you start empty!)
|
|
86
|
-
- bot.equipItem(/item name/i) — equip weapons and armor from inventory
|
|
87
|
-
- bot.eatFood(/food name/i) — eat food to heal
|
|
88
|
-
- bot.attackPlayer("name") or bot.attackPlayer(/pattern/i) — attack another player
|
|
89
|
-
- sdk.getNearbyPlayers() — see nearby players (name, combatLevel, distance, x, z)
|
|
90
|
-
- sdk.findGroundItem(/pattern/i) — find items on the ground near you
|
|
91
|
-
- sdk.getState().player.hitpoints — your current HP
|
|
92
|
-
- sdk.getInventory() — check what you've picked up
|
|
93
|
-
|
|
94
|
-
## Combat Loop Pattern
|
|
95
|
-
Write reactive loops in execute_code that run for 15-30 seconds, then return state:
|
|
96
|
-
|
|
97
|
-
// Phase 1: Grab loot
|
|
98
|
-
const items = sdk.findGroundItem(/scimitar|sword|lobster|chainbody/i);
|
|
99
|
-
if (items) await bot.pickupItem(items);
|
|
100
|
-
|
|
101
|
-
// Equip best weapon found
|
|
102
|
-
const weapon = sdk.findInventoryItem(/rune scimitar|adamant|mithril scimitar/i);
|
|
103
|
-
if (weapon) await bot.equipItem(weapon);
|
|
104
|
-
|
|
105
|
-
// Phase 2: Fight
|
|
106
|
-
const endTime = Date.now() + 15_000;
|
|
107
|
-
while (Date.now() < endTime) {
|
|
108
|
-
const hp = sdk.getState().player.hitpoints;
|
|
109
|
-
if (hp < 25) {
|
|
110
|
-
const food = sdk.findInventoryItem(/swordfish|lobster|salmon|trout|bread|meat/i);
|
|
111
|
-
if (food) await bot.eatFood(food);
|
|
112
|
-
}
|
|
113
|
-
const players = sdk.getNearbyPlayers();
|
|
114
|
-
if (players.length > 0) {
|
|
115
|
-
const nearest = players.sort((a, b) => a.distance - b.distance)[0];
|
|
116
|
-
await bot.attackPlayer(nearest);
|
|
117
|
-
}
|
|
118
|
-
await sdk.waitForTicks(2);
|
|
119
|
-
}
|
|
120
|
-
return { hp: sdk.getState().player.hitpoints, inventory: sdk.getInventory(), nearbyPlayers: sdk.getNearbyPlayers() };
|
|
121
|
-
|
|
122
|
-
## Key Tips
|
|
123
|
-
- You start with NOTHING — picking up items is your first priority!
|
|
124
|
-
- Grab a weapon first, then food, then armor
|
|
125
|
-
- Risk vs reward: center has the best loot but everyone converges there
|
|
126
|
-
- Keep execute_code calls to 15-30 seconds, then check state and adapt
|
|
127
|
-
- The human gives you strategy ("rush center", "play safe", "grab and run") — you turn it into code`;
|
|
128
|
-
|
|
129
|
-
// ── Server Setup ──
|
|
130
|
-
const server = new Server(
|
|
131
|
-
{ name: 'kill-switch', version: '3.0.0' },
|
|
132
|
-
{ capabilities: { tools: {} } }
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// ── Tool Definitions ──
|
|
136
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
137
|
-
return {
|
|
138
|
-
tools: [
|
|
139
|
-
{
|
|
140
|
-
name: 'login',
|
|
141
|
-
description: `Log in to Kill Switch. Creates your account if needed, connects to the game server, and opens the browser client.
|
|
142
|
-
|
|
143
|
-
Call this when the user says anything like "let's play", "join the game", "log in", "connect", etc.
|
|
144
|
-
|
|
145
|
-
If the user already has a bot, this will reconnect. If not, it creates a new one. Idempotent — safe to call multiple times.
|
|
146
|
-
|
|
147
|
-
${GAME_DESCRIPTION}`,
|
|
148
|
-
inputSchema: {
|
|
149
|
-
type: 'object',
|
|
150
|
-
properties: {
|
|
151
|
-
agent_name: {
|
|
152
|
-
type: 'string',
|
|
153
|
-
description: 'Name for the bot (max 12 chars, alphanumeric). If not provided, a random name is generated. If the user has played before and wants to reuse their name, use that.'
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: 'execute_code',
|
|
160
|
-
description: `Execute TypeScript code on your connected bot. The code runs in an async context with three globals:
|
|
161
|
-
- bot: High-level actions (attackPlayer, eatFood, walkTo, equipItem, pickupItem, etc.)
|
|
162
|
-
- sdk: Low-level state access (getState, getNearbyPlayers, getInventory, findGroundItem, etc.)
|
|
163
|
-
- actions: Pre-built strategy functions loaded from your bot's actions/ directory
|
|
164
|
-
|
|
165
|
-
Use actions for common patterns instead of writing loops from scratch:
|
|
166
|
-
await actions.fightLoop({ eatAt: 25, duration: 15000 })
|
|
167
|
-
await actions.lootAndEquip({ maxItems: 5 })
|
|
168
|
-
await actions.kite({ safeHp: 40 })
|
|
169
|
-
|
|
170
|
-
Or write custom code using bot/sdk directly for full control.
|
|
171
|
-
|
|
172
|
-
You MUST call login before using this tool.`,
|
|
173
|
-
inputSchema: {
|
|
174
|
-
type: 'object',
|
|
175
|
-
properties: {
|
|
176
|
-
code: {
|
|
177
|
-
type: 'string',
|
|
178
|
-
description: 'TypeScript code to execute. Has access to bot (BotActions) and sdk (BotSDK).'
|
|
179
|
-
},
|
|
180
|
-
timeout: {
|
|
181
|
-
type: 'number',
|
|
182
|
-
description: 'Execution timeout in minutes (default: 2, max: 60)'
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
required: ['code']
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'get_status',
|
|
190
|
-
description: 'Check your bot\'s current state without executing any code. Returns position, HP, inventory, nearby players. Use this to assess the situation before deciding what to do.',
|
|
191
|
-
inputSchema: {
|
|
192
|
-
type: 'object',
|
|
193
|
-
properties: {}
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: 'wait_for_game_start',
|
|
198
|
-
description: `Wait for the game to start. Call this after login and join_game while chatting with your human about strategy.
|
|
199
|
-
|
|
200
|
-
This tool blocks until the game starts and your bot is teleported to the arena. When it returns, the fight is ON — immediately start your combat loop!
|
|
201
|
-
|
|
202
|
-
Typical flow:
|
|
203
|
-
1. login → connect to game
|
|
204
|
-
2. join_game → pick a mode
|
|
205
|
-
3. Chat strategy with human
|
|
206
|
-
4. wait_for_game_start → blocks until game starts
|
|
207
|
-
5. execute_code → start fighting!`,
|
|
208
|
-
inputSchema: {
|
|
209
|
-
type: 'object',
|
|
210
|
-
properties: {
|
|
211
|
-
timeout: {
|
|
212
|
-
type: 'number',
|
|
213
|
-
description: 'Max seconds to wait (default: 300 = 5 minutes)'
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: 'join_game',
|
|
220
|
-
description: `Join a specific game mode queue. Call this after login when your human says which mode to play.
|
|
221
|
-
|
|
222
|
-
Available modes:
|
|
223
|
-
- "shorty": Quick battle royale at Draynor Manor. 50 atk/str/def, 99 HP. Grab loot, fight, last one standing wins.
|
|
224
|
-
- "longy": (Coming soon) Large survival map with skill progression.
|
|
225
|
-
|
|
226
|
-
After joining, call wait_for_game_start to wait for the game to begin.`,
|
|
227
|
-
inputSchema: {
|
|
228
|
-
type: 'object',
|
|
229
|
-
properties: {
|
|
230
|
-
mode: {
|
|
231
|
-
type: 'string',
|
|
232
|
-
enum: ['shorty', 'longy'],
|
|
233
|
-
description: 'Game mode to join'
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
required: ['mode']
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'disconnect_bot',
|
|
241
|
-
description: 'Disconnect from the game. Use when done playing.',
|
|
242
|
-
inputSchema: {
|
|
243
|
-
type: 'object',
|
|
244
|
-
properties: {}
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
name: 'setup_game_wallet',
|
|
249
|
-
description: `Set up a game wallet for paid tournaments. This creates a local access key that lets you join paid tournament matches.
|
|
250
|
-
|
|
251
|
-
IMPORTANT: This tool handles real (or testnet) money. Explain to the user what's happening at each step.
|
|
252
|
-
|
|
253
|
-
Prerequisites:
|
|
254
|
-
- The user must have the Tempo CLI installed (curl -fsSL https://tempo.xyz/install | bash)
|
|
255
|
-
- The user must have run "tempo wallet login" to create their Tempo account
|
|
256
|
-
|
|
257
|
-
This tool generates a local access key. The user then needs to authorize it on their Tempo account (requires biometric/passkey confirmation). After that, the key is stored locally in ~/.killswitch/ and used for all tournament deposits.
|
|
258
|
-
|
|
259
|
-
Learn more about Tempo access keys: https://docs.tempo.xyz/protocol/tips/tip-1011`,
|
|
260
|
-
inputSchema: {
|
|
261
|
-
type: 'object',
|
|
262
|
-
properties: {
|
|
263
|
-
tempo_account_address: {
|
|
264
|
-
type: 'string',
|
|
265
|
-
description: 'The user\'s Tempo account address (0x...). Get this by running "tempo wallet whoami".'
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
required: ['tempo_account_address']
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
name: 'check_wallet',
|
|
273
|
-
description: `Check your game wallet status and USDC balance. Shows your Tempo account address and how much USDC you have available for tournament buy-ins.
|
|
274
|
-
|
|
275
|
-
If no game wallet is set up, this will tell you to run setup_game_wallet first.`,
|
|
276
|
-
inputSchema: {
|
|
277
|
-
type: 'object',
|
|
278
|
-
properties: {}
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'tournament_schedule',
|
|
283
|
-
description: `View upcoming paid tournaments. Shows start times, buy-in amounts, and how many players have signed up.
|
|
284
|
-
|
|
285
|
-
This is a read-only tool — it doesn't cost anything to check the schedule.`,
|
|
286
|
-
inputSchema: {
|
|
287
|
-
type: 'object',
|
|
288
|
-
properties: {}
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
name: 'join_tournament',
|
|
293
|
-
description: `Join a paid tournament by depositing the buy-in from your game wallet.
|
|
294
|
-
|
|
295
|
-
THIS TOOL SPENDS REAL MONEY (or testnet money). Before calling this tool:
|
|
296
|
-
1. Tell the user exactly which tournament they're joining (time, buy-in amount)
|
|
297
|
-
2. Show their current wallet balance
|
|
298
|
-
3. Ask them to confirm they want to proceed
|
|
299
|
-
4. Only then call this tool
|
|
300
|
-
|
|
301
|
-
The deposit is sent on-chain to the tournament's escrow contract. The user's buy-in is held in escrow until:
|
|
302
|
-
- They win → they receive 90% of the pot
|
|
303
|
-
- The match is cancelled → they get a full refund
|
|
304
|
-
- The server goes down → they can claim a refund after 1 hour
|
|
305
|
-
|
|
306
|
-
The user must be logged in (call login first) so we know their in-game username.`,
|
|
307
|
-
inputSchema: {
|
|
308
|
-
type: 'object',
|
|
309
|
-
properties: {
|
|
310
|
-
tournament_address: {
|
|
311
|
-
type: 'string',
|
|
312
|
-
description: 'The tournament contract address to join (from tournament_schedule)'
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
required: ['tournament_address']
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
]
|
|
319
|
-
};
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// ── Action Loading ──
|
|
323
|
-
|
|
324
|
-
/** Convert kebab-case filename to camelCase function name */
|
|
325
|
-
function toCamelCase(str: string): string {
|
|
326
|
-
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Load custom actions from bots/<name>/actions/*.ts
|
|
331
|
-
*
|
|
332
|
-
* Each action file must have a default export:
|
|
333
|
-
* export default async function(bot, sdk, opts) { ... }
|
|
334
|
-
*
|
|
335
|
-
* The filename becomes the function name:
|
|
336
|
-
* fight-loop.ts → actions.fightLoop(opts)
|
|
337
|
-
*/
|
|
338
|
-
async function loadActions(botName: string, bot: any, sdk: any): Promise<{ actions: Record<string, Function>; descriptions: string[] }> {
|
|
339
|
-
const { existsSync, readdirSync, cpSync, readFileSync } = await import('fs');
|
|
340
|
-
const { join, basename } = await import('path');
|
|
341
|
-
const { pathToFileURL } = await import('url');
|
|
342
|
-
|
|
343
|
-
const botsDir = join(process.cwd(), 'bots');
|
|
344
|
-
const actionsDir = join(botsDir, botName, 'actions');
|
|
345
|
-
// Defaults ship with the npm package, resolve relative to this file
|
|
346
|
-
const defaultsDir = join(new URL('.', import.meta.url).pathname, '..', 'defaults', 'actions');
|
|
347
|
-
|
|
348
|
-
console.error(`[Kill Switch] Loading actions from ${actionsDir}`);
|
|
349
|
-
|
|
350
|
-
// Copy defaults if no actions dir exists
|
|
351
|
-
if (!existsSync(actionsDir) && existsSync(defaultsDir)) {
|
|
352
|
-
cpSync(defaultsDir, actionsDir, { recursive: true });
|
|
353
|
-
console.error(`[Kill Switch] Copied default actions to ${actionsDir}`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const actions: Record<string, Function> = {};
|
|
357
|
-
const descriptions: string[] = [];
|
|
358
|
-
|
|
359
|
-
if (!existsSync(actionsDir)) {
|
|
360
|
-
console.error(`[Kill Switch] No actions directory found at ${actionsDir}`);
|
|
361
|
-
return { actions, descriptions };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const files = readdirSync(actionsDir).filter(f => f.endsWith('.ts'));
|
|
365
|
-
console.error(`[Kill Switch] Found ${files.length} action files: ${files.join(', ')}`);
|
|
366
|
-
|
|
367
|
-
for (const file of files) {
|
|
368
|
-
const filePath = join(actionsDir, file);
|
|
369
|
-
const name = toCamelCase(basename(file, '.ts'));
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
// Cache-bust so re-login picks up edited action files
|
|
373
|
-
const fileUrl = pathToFileURL(filePath).href + `?t=${Date.now()}`;
|
|
374
|
-
const mod = await import(fileUrl);
|
|
375
|
-
|
|
376
|
-
if (typeof mod.default !== 'function') {
|
|
377
|
-
console.error(`[Kill Switch] Action ${file} has no default export, skipping`);
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Wrap: actions.name(opts) → mod.default(bot, sdk, opts)
|
|
382
|
-
actions[name] = (opts?: any) => mod.default(bot, sdk, opts);
|
|
383
|
-
|
|
384
|
-
// Extract JSDoc description from file for discovery
|
|
385
|
-
const source = readFileSync(filePath, 'utf-8');
|
|
386
|
-
const jsdocMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+?)[\n*]/);
|
|
387
|
-
const desc = jsdocMatch ? jsdocMatch[1].trim() : 'Custom action';
|
|
388
|
-
|
|
389
|
-
descriptions.push(` - actions.${name}(opts) — ${desc}`);
|
|
390
|
-
console.error(`[Kill Switch] Loaded action: ${name} (${file})`);
|
|
391
|
-
} catch (e: any) {
|
|
392
|
-
console.error(`[Kill Switch] Failed to load action ${file}: ${e.message}`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
console.error(`[Kill Switch] Loaded ${Object.keys(actions).length} actions: ${Object.keys(actions).join(', ')}`);
|
|
397
|
-
return { actions, descriptions };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// ── Helpers ──
|
|
401
|
-
function generateRandomName(): string {
|
|
402
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
403
|
-
let result = '';
|
|
404
|
-
for (let i = 0; i < 9; i++) {
|
|
405
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
406
|
-
}
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function generateRandomPassword(): string {
|
|
411
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
412
|
-
let result = '';
|
|
413
|
-
for (let i = 0; i < 12; i++) {
|
|
414
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
415
|
-
}
|
|
416
|
-
return result;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function parseEnv(content: string): Record<string, string> {
|
|
420
|
-
const result: Record<string, string> = {};
|
|
421
|
-
for (const line of content.split('\n')) {
|
|
422
|
-
const trimmed = line.trim();
|
|
423
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
424
|
-
const [key, ...valueParts] = trimmed.split('=');
|
|
425
|
-
if (key && valueParts.length > 0) {
|
|
426
|
-
result[key.trim()] = valueParts.join('=').trim();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return result;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Track the active bot for this session
|
|
433
|
-
let activeBotName: string | null = null;
|
|
434
|
-
let activeActions: Record<string, Function> = {};
|
|
435
|
-
let activeActionDescriptions: string[] = [];
|
|
436
|
-
|
|
437
|
-
function getActiveConnection() {
|
|
438
|
-
if (!activeBotName) return null;
|
|
439
|
-
return botManager.get(activeBotName) || null;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ── Tool Handlers ──
|
|
443
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
444
|
-
const { name, arguments: args } = request.params;
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
switch (name) {
|
|
448
|
-
case 'login': {
|
|
449
|
-
let botName = (args?.agent_name as string) || '';
|
|
450
|
-
|
|
451
|
-
// Check if we already have an active connection
|
|
452
|
-
if (activeBotName) {
|
|
453
|
-
const existing = botManager.get(activeBotName);
|
|
454
|
-
if (existing?.connected) {
|
|
455
|
-
const state = existing.sdk.getState();
|
|
456
|
-
const parts = [`Already connected as "${activeBotName}".`];
|
|
457
|
-
if (state) {
|
|
458
|
-
parts.push('');
|
|
459
|
-
parts.push('── Current State ──');
|
|
460
|
-
parts.push(formatWorldState(state, existing.sdk.getStateAge()));
|
|
461
|
-
}
|
|
462
|
-
parts.push('');
|
|
463
|
-
parts.push('Your bot is in the game. Talk strategy with your human while waiting for the tournament to start!');
|
|
464
|
-
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Step 1: Find or create bot credentials
|
|
469
|
-
const botsDir = join(process.cwd(), 'bots');
|
|
470
|
-
let username: string;
|
|
471
|
-
let password: string;
|
|
472
|
-
|
|
473
|
-
if (botName) {
|
|
474
|
-
// User specified a name — check if it already exists
|
|
475
|
-
const envPath = join(botsDir, botName, 'bot.env');
|
|
476
|
-
if (existsSync(envPath)) {
|
|
477
|
-
console.error(`[Kill Switch] Found existing bot "${botName}"`);
|
|
478
|
-
const env = parseEnv(await readFile(envPath, 'utf-8'));
|
|
479
|
-
username = env.BOT_USERNAME || botName;
|
|
480
|
-
password = env.PASSWORD || '';
|
|
481
|
-
} else {
|
|
482
|
-
// Create new bot
|
|
483
|
-
console.error(`[Kill Switch] Creating new bot "${botName}"`);
|
|
484
|
-
username = botName;
|
|
485
|
-
password = generateRandomPassword();
|
|
486
|
-
|
|
487
|
-
// Create bot directory and env file
|
|
488
|
-
const { mkdirSync, writeFileSync } = await import('fs');
|
|
489
|
-
const botDir = join(botsDir, botName);
|
|
490
|
-
mkdirSync(botDir, { recursive: true });
|
|
491
|
-
writeFileSync(join(botDir, 'bot.env'), [
|
|
492
|
-
`BOT_USERNAME=${username}`,
|
|
493
|
-
`PASSWORD=${password}`,
|
|
494
|
-
`SERVER=${SERVER_URL}`,
|
|
495
|
-
`SHOW_CHAT=false`
|
|
496
|
-
].join('\n'));
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
// No name specified — check for any existing bot first
|
|
500
|
-
const { readdirSync } = await import('fs');
|
|
501
|
-
if (existsSync(botsDir)) {
|
|
502
|
-
const dirs = readdirSync(botsDir).filter(d =>
|
|
503
|
-
d !== '_template' && existsSync(join(botsDir, d, 'bot.env'))
|
|
504
|
-
);
|
|
505
|
-
if (dirs.length > 0) {
|
|
506
|
-
// Reuse the first existing bot
|
|
507
|
-
botName = dirs[0];
|
|
508
|
-
console.error(`[Kill Switch] Reusing existing bot "${botName}"`);
|
|
509
|
-
const env = parseEnv(await readFile(join(botsDir, botName, 'bot.env'), 'utf-8'));
|
|
510
|
-
username = env.BOT_USERNAME || botName;
|
|
511
|
-
password = env.PASSWORD || '';
|
|
512
|
-
} else {
|
|
513
|
-
// Generate a random name
|
|
514
|
-
botName = generateRandomName();
|
|
515
|
-
username = botName;
|
|
516
|
-
password = generateRandomPassword();
|
|
517
|
-
|
|
518
|
-
const { mkdirSync, writeFileSync } = await import('fs');
|
|
519
|
-
const botDir = join(botsDir, botName);
|
|
520
|
-
mkdirSync(botDir, { recursive: true });
|
|
521
|
-
writeFileSync(join(botDir, 'bot.env'), [
|
|
522
|
-
`BOT_USERNAME=${username}`,
|
|
523
|
-
`PASSWORD=${password}`,
|
|
524
|
-
`SERVER=${SERVER_URL}`,
|
|
525
|
-
`SHOW_CHAT=false`
|
|
526
|
-
].join('\n'));
|
|
527
|
-
console.error(`[Kill Switch] Created new bot "${botName}"`);
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
botName = generateRandomName();
|
|
531
|
-
username = botName;
|
|
532
|
-
password = generateRandomPassword();
|
|
533
|
-
|
|
534
|
-
const { mkdirSync, writeFileSync } = await import('fs');
|
|
535
|
-
const botDir = join(botsDir, botName);
|
|
536
|
-
mkdirSync(botDir, { recursive: true });
|
|
537
|
-
writeFileSync(join(botDir, 'bot.env'), [
|
|
538
|
-
`BOT_USERNAME=${username}`,
|
|
539
|
-
`PASSWORD=${password}`,
|
|
540
|
-
`SERVER=${SERVER_URL}`,
|
|
541
|
-
`SHOW_CHAT=false`
|
|
542
|
-
].join('\n'));
|
|
543
|
-
console.error(`[Kill Switch] Created new bot "${botName}"`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Step 1.5: Check if agent is alive (permadeath check)
|
|
548
|
-
try {
|
|
549
|
-
const isLocal = SERVER_URL === 'localhost' || SERVER_URL === '127.0.0.1';
|
|
550
|
-
const webBase = isLocal ? `http://localhost:8888` : `https://${SERVER_URL}`;
|
|
551
|
-
const aliveRes = await fetch(`${webBase}/api/agent-alive?username=${encodeURIComponent(username!.toLowerCase())}`);
|
|
552
|
-
const aliveData = await aliveRes.json() as any;
|
|
553
|
-
if (aliveData.alive === false) {
|
|
554
|
-
return errorResponse(
|
|
555
|
-
`Agent "${username}" is PERMANENTLY DEAD (${aliveData.wins} wins, ${aliveData.kills} kills). ` +
|
|
556
|
-
`This agent can never play again. Create a new agent with a different name to play.`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
} catch (e: any) {
|
|
560
|
-
console.error(`[Kill Switch] Agent alive check failed (continuing): ${e.message}`);
|
|
561
|
-
// Non-fatal — server might not be running yet
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Step 2: Connect to the game
|
|
565
|
-
console.error(`[Kill Switch] Connecting "${botName}" to ${SERVER_URL}...`);
|
|
566
|
-
|
|
567
|
-
// Suppress stdout logs during connection (MCP uses stdout for JSON-RPC)
|
|
568
|
-
const originalLog = console.log;
|
|
569
|
-
const originalWarn = console.warn;
|
|
570
|
-
console.log = (...a) => console.error('[log]', ...a);
|
|
571
|
-
console.warn = (...a) => console.error('[warn]', ...a);
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
const connection = await botManager.connect(botName);
|
|
575
|
-
|
|
576
|
-
// Wait for initial game state
|
|
577
|
-
try {
|
|
578
|
-
await connection.sdk.waitForCondition(() => connection.sdk.getState() !== null, 20000);
|
|
579
|
-
} catch {
|
|
580
|
-
console.error(`[Kill Switch] Warning: initial state not received within 20s`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
activeBotName = botName;
|
|
584
|
-
|
|
585
|
-
// Load custom actions
|
|
586
|
-
const { actions, descriptions } = await loadActions(botName, connection.bot, connection.sdk);
|
|
587
|
-
activeActions = actions;
|
|
588
|
-
activeActionDescriptions = descriptions;
|
|
589
|
-
|
|
590
|
-
const state = connection.sdk.getState();
|
|
591
|
-
const parts = [
|
|
592
|
-
`Connected as "${username}"!`,
|
|
593
|
-
'',
|
|
594
|
-
'Your bot is in the game. A browser window should have opened showing the game view.',
|
|
595
|
-
'',
|
|
596
|
-
'You\'re in the tournament lobby (Barbarian Village) with:',
|
|
597
|
-
' - EMPTY inventory and no equipment',
|
|
598
|
-
' - Combat stats: 50 Attack, 50 Strength, 50 Defence, 99 Hitpoints',
|
|
599
|
-
'',
|
|
600
|
-
'NEXT STEP: Call join_game with mode "shorty" (or "longy" when available) to queue up.',
|
|
601
|
-
'',
|
|
602
|
-
'Game modes:',
|
|
603
|
-
' - SHORTY: Quick battle royale at Draynor Manor. Loot, fight, last one standing.',
|
|
604
|
-
' - LONGY: (Coming soon) Large survival map with skill progression.',
|
|
605
|
-
'',
|
|
606
|
-
'After joining a game, call wait_for_game_start to wait for the game to begin.',
|
|
607
|
-
];
|
|
608
|
-
|
|
609
|
-
if (activeActionDescriptions.length > 0) {
|
|
610
|
-
parts.push('');
|
|
611
|
-
parts.push('── Loaded Actions ──');
|
|
612
|
-
parts.push('Use these in execute_code via the `actions` object:');
|
|
613
|
-
parts.push(...activeActionDescriptions);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (state) {
|
|
617
|
-
parts.push('');
|
|
618
|
-
parts.push('── Current State ──');
|
|
619
|
-
parts.push(formatWorldState(state, connection.sdk.getStateAge()));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
623
|
-
} finally {
|
|
624
|
-
console.log = originalLog;
|
|
625
|
-
console.warn = originalWarn;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
case 'get_status': {
|
|
630
|
-
const connection = getActiveConnection();
|
|
631
|
-
if (!connection) {
|
|
632
|
-
return errorResponse('Not connected. Call login first.');
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const state = connection.sdk.getState();
|
|
636
|
-
if (!state) {
|
|
637
|
-
return errorResponse('No game state available. The game client may not be loaded yet.');
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return {
|
|
641
|
-
content: [{ type: 'text', text: formatWorldState(state, connection.sdk.getStateAge()) }]
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
case 'execute_code': {
|
|
646
|
-
const code = args?.code as string;
|
|
647
|
-
if (!code) {
|
|
648
|
-
return errorResponse('code is required');
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Use active bot, or error
|
|
652
|
-
const botName = activeBotName;
|
|
653
|
-
if (!botName) {
|
|
654
|
-
return errorResponse('Not connected. Call login first.');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const isLongCode = code.length > 2000;
|
|
658
|
-
|
|
659
|
-
// Capture console output
|
|
660
|
-
const logs: string[] = [];
|
|
661
|
-
const originalLog = console.log;
|
|
662
|
-
const originalWarn = console.warn;
|
|
663
|
-
|
|
664
|
-
console.log = (...args) => logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '));
|
|
665
|
-
console.warn = (...args) => logs.push('[warn] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '));
|
|
666
|
-
|
|
667
|
-
let connection = botManager.get(botName);
|
|
668
|
-
if (!connection) {
|
|
669
|
-
console.error(`[Kill Switch] Bot "${botName}" not connected, reconnecting...`);
|
|
670
|
-
connection = await botManager.connect(botName);
|
|
671
|
-
try {
|
|
672
|
-
await connection.sdk.waitForCondition(() => connection!.sdk.getState() !== null, 15000);
|
|
673
|
-
} catch {
|
|
674
|
-
console.error(`[Kill Switch] Warning: state not received within 15s`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
try {
|
|
679
|
-
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
680
|
-
const fn = new AsyncFunction('bot', 'sdk', 'actions', code);
|
|
681
|
-
|
|
682
|
-
const timeoutMinutes = Math.min(Math.max((args?.timeout as number) || 2, 0.1), 60);
|
|
683
|
-
const EXECUTION_TIMEOUT = timeoutMinutes * 60 * 1000;
|
|
684
|
-
let timeoutId: ReturnType<typeof setTimeout>;
|
|
685
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
686
|
-
timeoutId = setTimeout(() => reject(new Error(`Code execution timed out after ${timeoutMinutes} minute(s)`)), EXECUTION_TIMEOUT);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
const abortController = new AbortController();
|
|
690
|
-
const signal = abortController.signal;
|
|
691
|
-
|
|
692
|
-
if (extra.signal) {
|
|
693
|
-
if (extra.signal.aborted) {
|
|
694
|
-
abortController.abort(extra.signal.reason);
|
|
695
|
-
} else {
|
|
696
|
-
extra.signal.addEventListener('abort', () => {
|
|
697
|
-
console.error(`[Kill Switch] execute_code cancelled`);
|
|
698
|
-
abortController.abort('Cancelled by client');
|
|
699
|
-
}, { once: true });
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const cancelPromise = new Promise<never>((_, reject) => {
|
|
704
|
-
signal.addEventListener('abort', () => {
|
|
705
|
-
reject(new Error(typeof signal.reason === 'string' ? signal.reason : 'Code execution cancelled'));
|
|
706
|
-
}, { once: true });
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
const cancellable = <T extends object>(target: T): T =>
|
|
710
|
-
new Proxy(target, {
|
|
711
|
-
get(obj, prop, receiver) {
|
|
712
|
-
const value = Reflect.get(obj, prop, receiver);
|
|
713
|
-
if (typeof value === 'function') {
|
|
714
|
-
return (...args: any[]) => {
|
|
715
|
-
if (signal.aborted) throw new Error('Execution cancelled');
|
|
716
|
-
return value.apply(obj, args);
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
return value;
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
let result: any;
|
|
724
|
-
try {
|
|
725
|
-
result = await Promise.race([fn(cancellable(connection.bot), cancellable(connection.sdk), activeActions), timeoutPromise, cancelPromise]);
|
|
726
|
-
} finally {
|
|
727
|
-
clearTimeout(timeoutId!);
|
|
728
|
-
if (!signal.aborted) abortController.abort('Execution finished');
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Build output
|
|
732
|
-
const parts: string[] = [];
|
|
733
|
-
|
|
734
|
-
if (logs.length > 0) {
|
|
735
|
-
parts.push('── Console ──');
|
|
736
|
-
parts.push(logs.join('\n'));
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (result !== undefined) {
|
|
740
|
-
if (logs.length > 0) parts.push('');
|
|
741
|
-
parts.push('── Result ──');
|
|
742
|
-
parts.push(JSON.stringify(result, null, 2));
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const state = connection.sdk.getState();
|
|
746
|
-
if (state) {
|
|
747
|
-
parts.push('');
|
|
748
|
-
parts.push('── World State ──');
|
|
749
|
-
parts.push(formatWorldState(state, connection.sdk.getStateAge()));
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (isLongCode) {
|
|
753
|
-
parts.push('');
|
|
754
|
-
parts.push('── Tip ──');
|
|
755
|
-
parts.push(`Long script detected. Consider writing to a .ts file and running with: bun run bots/${botName}/script.ts`);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const output = parts.length > 0 ? parts.join('\n') : '(no output)';
|
|
759
|
-
return { content: [{ type: 'text', text: output }] };
|
|
760
|
-
} finally {
|
|
761
|
-
console.log = originalLog;
|
|
762
|
-
console.warn = originalWarn;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
case 'wait_for_game_start': {
|
|
767
|
-
const connection = getActiveConnection();
|
|
768
|
-
if (!connection) {
|
|
769
|
-
return errorResponse('Not connected. Call login first.');
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const timeoutSecs = Math.min(Math.max((args?.timeout as number) || 300, 10), 600);
|
|
773
|
-
// Draynor Manor arena bounds
|
|
774
|
-
const ARENA_MIN_X = 3086;
|
|
775
|
-
const ARENA_MAX_X = 3126;
|
|
776
|
-
const ARENA_MIN_Z = 3333;
|
|
777
|
-
const ARENA_MAX_Z = 3382;
|
|
778
|
-
|
|
779
|
-
console.error(`[Kill Switch] Waiting for tournament to start (timeout: ${timeoutSecs}s)...`);
|
|
780
|
-
|
|
781
|
-
const startTime = Date.now();
|
|
782
|
-
const deadline = startTime + timeoutSecs * 1000;
|
|
783
|
-
|
|
784
|
-
// Poll every 2 seconds for position change to arena
|
|
785
|
-
while (Date.now() < deadline) {
|
|
786
|
-
const state = connection.sdk.getState();
|
|
787
|
-
if (state) {
|
|
788
|
-
// Check if player has been teleported INTO the fight zone (not just nearby)
|
|
789
|
-
const x = state.player.worldX;
|
|
790
|
-
const z = state.player.worldZ;
|
|
791
|
-
if (x >= ARENA_MIN_X && x <= ARENA_MAX_X && z >= ARENA_MIN_Z && z <= ARENA_MAX_Z) {
|
|
792
|
-
const waitTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
793
|
-
console.error(`[Kill Switch] Tournament started! (detected after ${waitTime}s)`);
|
|
794
|
-
|
|
795
|
-
const players = connection.sdk.getNearbyPlayers?.() || [];
|
|
796
|
-
const parts = [
|
|
797
|
-
'TOURNAMENT STARTED!',
|
|
798
|
-
'',
|
|
799
|
-
`You've been teleported to Draynor Manor. The fight is ON!`,
|
|
800
|
-
`Position: ${state.player.worldX}, ${state.player.worldZ}`,
|
|
801
|
-
`HP: ${state.player.hp}/${state.player.maxHp}`,
|
|
802
|
-
'',
|
|
803
|
-
`Nearby players: ${players.length > 0 ? players.map((p: any) => `${p.name} (CB ${p.combatLevel}, dist ${p.distance})`).join(', ') : 'scanning...'}`,
|
|
804
|
-
'',
|
|
805
|
-
'REMEMBER: You die = your agent dies FOREVER. No second chances.',
|
|
806
|
-
'',
|
|
807
|
-
'IMMEDIATELY:',
|
|
808
|
-
'1. Pick up weapons and food from the ground',
|
|
809
|
-
'2. Equip best weapon found, then start fighting',
|
|
810
|
-
'3. Eat when HP gets low — you have 99 HP but no food yet!',
|
|
811
|
-
];
|
|
812
|
-
|
|
813
|
-
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return errorResponse(`Tournament didn't start within ${timeoutSecs} seconds. Try calling wait_for_game_start again.`);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
case 'join_game': {
|
|
823
|
-
const connection = getActiveConnection();
|
|
824
|
-
if (!connection) {
|
|
825
|
-
return errorResponse('Not connected. Call login first.');
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const mode = args?.mode as string;
|
|
829
|
-
if (!mode || !['shorty', 'longy'].includes(mode)) {
|
|
830
|
-
return errorResponse('mode must be "shorty" or "longy"');
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Determine the web server base URL
|
|
834
|
-
const isLocal = SERVER_URL === 'localhost' || SERVER_URL === '127.0.0.1';
|
|
835
|
-
const webBase = isLocal ? `http://localhost:8888` : `https://${SERVER_URL}`;
|
|
836
|
-
const apiUrl = `${webBase}/api/join-game?username=${encodeURIComponent(activeBotName!)}&mode=${encodeURIComponent(mode)}`;
|
|
837
|
-
|
|
838
|
-
try {
|
|
839
|
-
const res = await fetch(apiUrl, { method: 'POST' });
|
|
840
|
-
const data = await res.json() as any;
|
|
841
|
-
|
|
842
|
-
if (data.error) {
|
|
843
|
-
return errorResponse(data.error);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const parts = [
|
|
847
|
-
`Joined ${mode.toUpperCase()} queue!`,
|
|
848
|
-
'',
|
|
849
|
-
`Queue position: ${data.queuePosition}`,
|
|
850
|
-
`Players in queue: ${data.queuePlayers?.join(', ') || 'just you'}`,
|
|
851
|
-
'',
|
|
852
|
-
];
|
|
853
|
-
|
|
854
|
-
if (mode === 'shorty') {
|
|
855
|
-
parts.push('SHORTY: Quick battle royale at Draynor Manor.');
|
|
856
|
-
parts.push('Stats: 50 Attack, 50 Strength, 50 Defence, 99 Hitpoints');
|
|
857
|
-
parts.push('You start with NOTHING — grab weapons and food from the ground!');
|
|
858
|
-
} else {
|
|
859
|
-
parts.push('LONGY: Large survival map (coming soon).');
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
parts.push('');
|
|
863
|
-
parts.push('Now call wait_for_game_start to wait for the game to begin.');
|
|
864
|
-
parts.push('Chat strategy with your human while you wait!');
|
|
865
|
-
|
|
866
|
-
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
867
|
-
} catch (e: any) {
|
|
868
|
-
return errorResponse(`Failed to join game: ${e.message}. Is the game server running?`);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
case 'disconnect_bot': {
|
|
873
|
-
if (!activeBotName) {
|
|
874
|
-
return successResponse({ message: 'Not connected.' });
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
await botManager.disconnect(activeBotName);
|
|
878
|
-
const name = activeBotName;
|
|
879
|
-
activeBotName = null;
|
|
880
|
-
return successResponse({ message: `Disconnected "${name}" from the game.` });
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
case 'setup_game_wallet': {
|
|
884
|
-
const tempoAddress = args?.tempo_account_address as string;
|
|
885
|
-
if (!tempoAddress || !tempoAddress.startsWith('0x')) {
|
|
886
|
-
return errorResponse('A valid Tempo account address (0x...) is required. Run "tempo wallet whoami" to get yours.');
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Check if wallet already exists
|
|
890
|
-
if (hasGameWallet()) {
|
|
891
|
-
try {
|
|
892
|
-
const info = await getWalletInfo();
|
|
893
|
-
return {
|
|
894
|
-
content: [{
|
|
895
|
-
type: 'text',
|
|
896
|
-
text: [
|
|
897
|
-
'You already have a game wallet set up.',
|
|
898
|
-
'',
|
|
899
|
-
`Account: ${info.address}`,
|
|
900
|
-
`Balance: $${info.balance} USDC`,
|
|
901
|
-
'',
|
|
902
|
-
'To start fresh, delete ~/.killswitch/access-key.json and run this again.',
|
|
903
|
-
].join('\n')
|
|
904
|
-
}]
|
|
905
|
-
};
|
|
906
|
-
} catch {
|
|
907
|
-
// Wallet file exists but might be corrupt, continue with setup
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Generate access key
|
|
912
|
-
const { accessKeyAddress, privateKey, instructions } = generateAccessKey();
|
|
913
|
-
|
|
914
|
-
// Save it (the player still needs to authorize it on-chain)
|
|
915
|
-
saveAccessKey(privateKey, tempoAddress);
|
|
916
|
-
|
|
917
|
-
return {
|
|
918
|
-
content: [{
|
|
919
|
-
type: 'text',
|
|
920
|
-
text: [
|
|
921
|
-
'Game wallet setup started!',
|
|
922
|
-
'',
|
|
923
|
-
`Your Tempo account: ${tempoAddress}`,
|
|
924
|
-
`Generated access key: ${accessKeyAddress}`,
|
|
925
|
-
'',
|
|
926
|
-
'── NEXT STEP ──',
|
|
927
|
-
'',
|
|
928
|
-
'You need to authorize this access key on your Tempo account.',
|
|
929
|
-
'This requires your passkey (biometric confirmation).',
|
|
930
|
-
'',
|
|
931
|
-
instructions,
|
|
932
|
-
'',
|
|
933
|
-
'── AFTER AUTHORIZATION ──',
|
|
934
|
-
'',
|
|
935
|
-
'Once authorized, your game wallet is ready. Check it with check_wallet.',
|
|
936
|
-
'Fund your Tempo account with USDC to start joining paid tournaments.',
|
|
937
|
-
].join('\n')
|
|
938
|
-
}]
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
case 'check_wallet': {
|
|
943
|
-
if (!hasGameWallet()) {
|
|
944
|
-
return errorResponse(
|
|
945
|
-
'No game wallet found. Run setup_game_wallet first.\n\n' +
|
|
946
|
-
'Prerequisites:\n' +
|
|
947
|
-
'1. Install Tempo CLI: curl -fsSL https://tempo.xyz/install | bash\n' +
|
|
948
|
-
'2. Create account: tempo wallet login\n' +
|
|
949
|
-
'3. Get your address: tempo wallet whoami\n' +
|
|
950
|
-
'4. Then call setup_game_wallet with your address'
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
try {
|
|
955
|
-
const info = await getWalletInfo();
|
|
956
|
-
return {
|
|
957
|
-
content: [{
|
|
958
|
-
type: 'text',
|
|
959
|
-
text: [
|
|
960
|
-
'── Game Wallet ──',
|
|
961
|
-
'',
|
|
962
|
-
`Account: ${info.address}`,
|
|
963
|
-
`USDC Balance: $${info.balance}`,
|
|
964
|
-
'',
|
|
965
|
-
info.balanceRaw === 0n
|
|
966
|
-
? 'Your wallet is empty. Fund it by sending USDC to your account address on the Tempo network.'
|
|
967
|
-
: 'Your wallet is funded and ready for paid tournaments. Use tournament_schedule to see upcoming games.',
|
|
968
|
-
].join('\n')
|
|
969
|
-
}]
|
|
970
|
-
};
|
|
971
|
-
} catch (e: any) {
|
|
972
|
-
return errorResponse(`Failed to check wallet: ${e.message}`);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case 'tournament_schedule': {
|
|
977
|
-
try {
|
|
978
|
-
const slots = await getTournamentSchedule();
|
|
979
|
-
|
|
980
|
-
if (slots.length === 0) {
|
|
981
|
-
return {
|
|
982
|
-
content: [{
|
|
983
|
-
type: 'text',
|
|
984
|
-
text: 'No upcoming paid tournaments scheduled. Check back later, or paid tournaments may not be enabled on this server.'
|
|
985
|
-
}]
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const lines = ['── Upcoming Paid Tournaments ──', ''];
|
|
990
|
-
|
|
991
|
-
for (let i = 0; i < slots.length; i++) {
|
|
992
|
-
const s = slots[i];
|
|
993
|
-
const time = new Date(s.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
994
|
-
const buyIn = (parseInt(s.buyIn) / 1_000_000).toFixed(2);
|
|
995
|
-
lines.push(`[${i + 1}] ${time} — $${buyIn} buy-in — ${s.depositorCount}/${s.maxPlayers} players`);
|
|
996
|
-
lines.push(` Contract: ${s.contractAddress}`);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
lines.push('');
|
|
1000
|
-
lines.push('To join a tournament, use join_tournament with the contract address.');
|
|
1001
|
-
lines.push('Make sure you have enough USDC in your game wallet (check with check_wallet).');
|
|
1002
|
-
|
|
1003
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1004
|
-
} catch (e: any) {
|
|
1005
|
-
return errorResponse(`Failed to get tournament schedule: ${e.message}`);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
case 'join_tournament': {
|
|
1010
|
-
const tournamentAddress = args?.tournament_address as string;
|
|
1011
|
-
if (!tournamentAddress || !tournamentAddress.startsWith('0x')) {
|
|
1012
|
-
return errorResponse('A valid tournament contract address (0x...) is required. Use tournament_schedule to find one.');
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (!hasGameWallet()) {
|
|
1016
|
-
return errorResponse('No game wallet found. Run setup_game_wallet first.');
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!activeBotName) {
|
|
1020
|
-
return errorResponse('Not logged in. Call login first so we know your in-game username.');
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
const result = await joinTournament(tournamentAddress, activeBotName);
|
|
1025
|
-
|
|
1026
|
-
return {
|
|
1027
|
-
content: [{
|
|
1028
|
-
type: 'text',
|
|
1029
|
-
text: [
|
|
1030
|
-
'Tournament joined!',
|
|
1031
|
-
'',
|
|
1032
|
-
`Buy-in: $${result.amount} USDC`,
|
|
1033
|
-
`Transaction: ${result.txHash}`,
|
|
1034
|
-
`Username: ${activeBotName}`,
|
|
1035
|
-
'',
|
|
1036
|
-
'Your deposit is held in the tournament escrow contract.',
|
|
1037
|
-
'Be online when the match starts or you\'ll be eliminated (buy-in stays in the pot).',
|
|
1038
|
-
'',
|
|
1039
|
-
'If the match is cancelled (< 2 players), you\'ll get a full refund.',
|
|
1040
|
-
'If the server goes down, you can claim a refund after 1 hour.',
|
|
1041
|
-
].join('\n')
|
|
1042
|
-
}]
|
|
1043
|
-
};
|
|
1044
|
-
} catch (e: any) {
|
|
1045
|
-
return errorResponse(`Failed to join tournament: ${e.message}`);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
default:
|
|
1050
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1051
|
-
}
|
|
1052
|
-
} catch (error: any) {
|
|
1053
|
-
const errorMessage = `Error: ${error.message}\n\nStack trace:\n${error.stack}`;
|
|
1054
|
-
return {
|
|
1055
|
-
content: [{ type: 'text', text: errorMessage }],
|
|
1056
|
-
isError: true
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
function successResponse(data: any) {
|
|
1062
|
-
return {
|
|
1063
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function errorResponse(message: string) {
|
|
1068
|
-
return {
|
|
1069
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
1070
|
-
isError: true
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// ── Start ──
|
|
1075
|
-
async function main() {
|
|
1076
|
-
console.error('[Kill Switch MCP] Starting Kill Switch MCP server v3.0...');
|
|
1077
|
-
console.error(`[Kill Switch MCP] Game server: ${SERVER_URL}`);
|
|
1078
|
-
|
|
1079
|
-
const transport = new StdioServerTransport();
|
|
1080
|
-
await server.connect(transport);
|
|
1081
|
-
|
|
1082
|
-
console.error('[Kill Switch MCP] Ready. Waiting for Claude...');
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
main().catch((error) => {
|
|
1086
|
-
console.error('[Kill Switch MCP] Fatal error:', error);
|
|
1087
|
-
process.exit(1);
|
|
1088
|
-
});
|