mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
bring back mcp
This commit is contained in:
1 parent
70193f11a5
commit
c7fcd63866
12 files changed
+1162
-48
No files matched your search
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"rs-agent": {
|
||||
"command": "bun",
|
||||
"args": ["run", "mcp/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,31 +27,45 @@ This automatically creates:
|
||||
- `bots/{username}/lab_log.md` - Session notes template
|
||||
- `bots/{username}/script.ts` - Ready-to-run starter script
|
||||
|
||||
## CLI Daemon (Interactive Mode)
|
||||
|
||||
The CLI supports a daemon mode for persistent bot connections and inline code execution.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Execute code on a bot (auto-starts daemon if needed)
|
||||
bun sdk/cli.ts mybot exec "return sdk.getState()?.player"
|
||||
1. Install dependencies: `bun install` (from project root)
|
||||
2. Open project in your AI coding tool — the MCP server will be available automatically
|
||||
3. Control your bot with suggestions.
|
||||
|
||||
# Execute via heredoc (avoids shell quoting)
|
||||
bun sdk/cli.ts mybot exec <<'EOF'
|
||||
await bot.chopTree()
|
||||
return sdk.getInventory()
|
||||
EOF
|
||||
### Tools
|
||||
|
||||
# Check state (uses daemon if running, otherwise one-shot)
|
||||
bun sdk/cli.ts mybot state
|
||||
bun sdk/cli.ts mybot state --json
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. |
|
||||
| `list_bots()` | List connected bots |
|
||||
| `disconnect_bot(name)` | Disconnect a bot |
|
||||
|
||||
# Stop the daemon
|
||||
bun sdk/cli.ts mybot stop
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
// Just execute - auto-connects on first use
|
||||
execute_code({
|
||||
bot_name: "mybot",
|
||||
code: `
|
||||
const state = sdk.getState();
|
||||
console.log('Position:', state.player.worldX, state.player.worldZ);
|
||||
|
||||
// Chop trees for 1 minute
|
||||
const endTime = Date.now() + 60_000;
|
||||
while (Date.now() < endTime) {
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
if (tree) await bot.chopTree(tree);
|
||||
}
|
||||
|
||||
return sdk.getInventory();
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
The daemon holds the bot connection alive between commands. It auto-starts on first `exec` and shuts down after 5 minutes of idle time.
|
||||
See `mcp/README.md` for detailed API reference.
|
||||
|
||||
Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals.
|
||||
|
||||
|
||||
@@ -27,31 +27,45 @@ This automatically creates:
|
||||
- `bots/{username}/lab_log.md` - Session notes template
|
||||
- `bots/{username}/script.ts` - Ready-to-run starter script
|
||||
|
||||
## CLI Daemon (Interactive Mode)
|
||||
|
||||
The CLI supports a daemon mode for persistent bot connections and inline code execution.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Execute code on a bot (auto-starts daemon if needed)
|
||||
bun sdk/cli.ts mybot exec "return sdk.getState()?.player"
|
||||
1. Install dependencies: `bun install` (from project root)
|
||||
2. Open project in your AI coding tool — the MCP server will be available automatically
|
||||
3. Control your bot with suggestions.
|
||||
|
||||
# Execute via heredoc (avoids shell quoting)
|
||||
bun sdk/cli.ts mybot exec <<'EOF'
|
||||
await bot.chopTree()
|
||||
return sdk.getInventory()
|
||||
EOF
|
||||
### Tools
|
||||
|
||||
# Check state (uses daemon if running, otherwise one-shot)
|
||||
bun sdk/cli.ts mybot state
|
||||
bun sdk/cli.ts mybot state --json
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. |
|
||||
| `list_bots()` | List connected bots |
|
||||
| `disconnect_bot(name)` | Disconnect a bot |
|
||||
|
||||
# Stop the daemon
|
||||
bun sdk/cli.ts mybot stop
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
// Just execute - auto-connects on first use
|
||||
execute_code({
|
||||
bot_name: "mybot",
|
||||
code: `
|
||||
const state = sdk.getState();
|
||||
console.log('Position:', state.player.worldX, state.player.worldZ);
|
||||
|
||||
// Chop trees for 1 minute
|
||||
const endTime = Date.now() + 60_000;
|
||||
while (Date.now() < endTime) {
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
if (tree) await bot.chopTree(tree);
|
||||
}
|
||||
|
||||
return sdk.getInventory();
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
The daemon holds the bot connection alive between commands. It auto-starts on first `exec` and shuts down after 5 minutes of idle time.
|
||||
See `mcp/README.md` for detailed API reference.
|
||||
|
||||
Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals.
|
||||
|
||||
|
||||
@@ -27,31 +27,45 @@ This automatically creates:
|
||||
- `bots/{username}/lab_log.md` - Session notes template
|
||||
- `bots/{username}/script.ts` - Ready-to-run starter script
|
||||
|
||||
## CLI Daemon (Interactive Mode)
|
||||
|
||||
The CLI supports a daemon mode for persistent bot connections and inline code execution.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Execute code on a bot (auto-starts daemon if needed)
|
||||
bun sdk/cli.ts mybot exec "return sdk.getState()?.player"
|
||||
1. Install dependencies: `bun install` (from project root)
|
||||
2. Open project in your AI coding tool — the MCP server will be available automatically
|
||||
3. Control your bot with suggestions.
|
||||
|
||||
# Execute via heredoc (avoids shell quoting)
|
||||
bun sdk/cli.ts mybot exec <<'EOF'
|
||||
await bot.chopTree()
|
||||
return sdk.getInventory()
|
||||
EOF
|
||||
### Tools
|
||||
|
||||
# Check state (uses daemon if running, otherwise one-shot)
|
||||
bun sdk/cli.ts mybot state
|
||||
bun sdk/cli.ts mybot state --json
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. |
|
||||
| `list_bots()` | List connected bots |
|
||||
| `disconnect_bot(name)` | Disconnect a bot |
|
||||
|
||||
# Stop the daemon
|
||||
bun sdk/cli.ts mybot stop
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
// Just execute - auto-connects on first use
|
||||
execute_code({
|
||||
bot_name: "mybot",
|
||||
code: `
|
||||
const state = sdk.getState();
|
||||
console.log('Position:', state.player.worldX, state.player.worldZ);
|
||||
|
||||
// Chop trees for 1 minute
|
||||
const endTime = Date.now() + 60_000;
|
||||
while (Date.now() < endTime) {
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
if (tree) await bot.chopTree(tree);
|
||||
}
|
||||
|
||||
return sdk.getInventory();
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
The daemon holds the bot connection alive between commands. It auto-starts on first `exec` and shuts down after 5 minutes of idle time.
|
||||
See `mcp/README.md` for detailed API reference.
|
||||
|
||||
Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals.
|
||||
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# RS-Agent MCP Server
|
||||
|
||||
MCP (Model Context Protocol) server for controlling RS-Agent bots. Supports multiple simultaneous bot connections.
|
||||
|
||||
## Quick Start (Claude Code)
|
||||
|
||||
Claude Code auto-discovers the MCP server via `.mcp.json`. Just:
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
cd mcp && bun install
|
||||
```
|
||||
|
||||
2. **Create a bot (if you haven't):**
|
||||
```bash
|
||||
bun bots/create-bot.ts mybot
|
||||
```
|
||||
|
||||
3. **Open the project in Claude Code** — it will prompt you to approve the MCP server.
|
||||
|
||||
4. **Control your bot:**
|
||||
```
|
||||
Execute code on "mybot" to chop some trees
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### `execute_code`
|
||||
Execute TypeScript code on a bot. Auto-connects on first use using credentials from `bots/{name}/bot.env`.
|
||||
|
||||
```typescript
|
||||
execute_code({
|
||||
bot_name: "mybot",
|
||||
code: `
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
if (tree) {
|
||||
const result = await bot.chopTree(tree);
|
||||
console.log('Chopped:', result);
|
||||
}
|
||||
return sdk.getInventory();
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### `list_bots`
|
||||
List all connected bots and their status.
|
||||
|
||||
```typescript
|
||||
list_bots()
|
||||
// Returns: { bots: [{ name: "mybot", username: "mybot", connected: true }], count: 1 }
|
||||
```
|
||||
|
||||
### `disconnect_bot`
|
||||
Disconnect a connected bot.
|
||||
|
||||
```typescript
|
||||
disconnect_bot({ name: "mybot" })
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
The server exposes API documentation as resources:
|
||||
|
||||
- `file://api/bot.ts` — High-level bot actions (chopTree, walkTo, attackNpc, etc.)
|
||||
- `file://api/sdk.ts` — Low-level SDK (getState, sendWalk, findNearbyNpc, etc.)
|
||||
|
||||
Read these to discover available methods.
|
||||
|
||||
## Multiple Bots
|
||||
|
||||
Control multiple bots simultaneously — each auto-connects on first use:
|
||||
|
||||
```typescript
|
||||
// Execute on different bots (auto-connects each)
|
||||
execute_code({
|
||||
bot_name: "woodcutter",
|
||||
code: "await bot.chopTree()"
|
||||
})
|
||||
|
||||
execute_code({
|
||||
bot_name: "miner",
|
||||
code: "await bot.mineRock()"
|
||||
})
|
||||
```
|
||||
|
||||
## Manual Setup (without auto-discovery)
|
||||
|
||||
If you're not using Claude Code's auto-discovery, add to your MCP client config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rs-agent": {
|
||||
"command": "bun",
|
||||
"args": ["run", "/path/to/rs-agent/Server/mcp/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or run directly for testing:
|
||||
|
||||
```bash
|
||||
bun run mcp/server.ts
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
mcp/
|
||||
├── server.ts # MCP server (stdio transport)
|
||||
├── package.json # MCP SDK dependency
|
||||
└── api/
|
||||
├── index.ts # BotManager - manages multiple connections
|
||||
├── bot.ts # High-level BotActions API docs
|
||||
└── sdk.ts # Low-level BotSDK API docs
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Bot not found"**
|
||||
- Create the bot first: `bun bots/create-bot.ts {name}`
|
||||
- Check `bots/{name}/bot.env` exists
|
||||
|
||||
**"Bot is not connected"**
|
||||
- Use `connect_bot` before `execute_code`
|
||||
- Use `list_bots` to see connected bots
|
||||
|
||||
**"Connection failed"**
|
||||
- Check the gateway is running
|
||||
- Verify credentials in `bots/{name}/bot.env`
|
||||
|
||||
**MCP server not appearing in Claude Code**
|
||||
- Run `bun install` in the mcp directory
|
||||
- Check `.mcp.json` exists at project root
|
||||
- Restart Claude Code
|
||||
|
||||
## API Reference
|
||||
|
||||
See `api/bot.ts` and `api/sdk.ts` for full API documentation.
|
||||
|
||||
### High-Level Bot Actions
|
||||
|
||||
- Movement: `walkTo(x, z)`
|
||||
- Skills: `chopTree()`, `mineRock()`, `fish()`, `cookFood()`
|
||||
- Combat: `attackNpc(target)`, `eatFood(food)`
|
||||
- Banking: `openBank()`, `depositItem()`, `withdrawItem()`
|
||||
- Shopping: `openShop()`, `buyFromShop()`, `sellToShop()`
|
||||
- Crafting: `smithAtAnvil()`, `fletchLogs()`, `craftLeather()`
|
||||
- UI: `dismissBlockingUI()`, `skipTutorial()`
|
||||
|
||||
### Low-Level SDK Methods
|
||||
|
||||
- State: `getState()`, `getStateAge()`
|
||||
- Inventory: `getInventory()`, `findInventoryItem(pattern)`
|
||||
- NPCs: `getNearbyNpcs()`, `findNearbyNpc(pattern)`
|
||||
- Locations: `getNearbyLocs()`, `findNearbyLoc(pattern)`
|
||||
- Actions: `sendWalk()`, `sendInteractLoc()`, `sendInteractNpc()`
|
||||
- Utilities: `findPath()`, `waitForCondition()`
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* High-level Bot Actions API
|
||||
*
|
||||
* This module provides domain-aware methods that handle game mechanics automatically.
|
||||
* Actions wait for effects to complete (not just acknowledgment from server).
|
||||
*
|
||||
* Import via: const { bot } = await import('./api/index');
|
||||
* Or use directly if passed as parameter: bot.chopTree(), bot.walkTo(), etc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Available methods on the bot object:
|
||||
*
|
||||
* == Movement ==
|
||||
* - walkTo(x, z, tolerance?) - Walk to coordinates, returns when arrived or stuck
|
||||
*
|
||||
* == Woodcutting ==
|
||||
* - chopTree(target?) - Chop a tree, wait for logs. target can be location, name pattern, or regex
|
||||
*
|
||||
* == Firemaking ==
|
||||
* - burnLogs(logs?) - Burn logs on ground. If logs specified, drops them first
|
||||
*
|
||||
* == Mining ==
|
||||
* - mineRock(target?) - Mine a rock, wait for ore
|
||||
*
|
||||
* == Fishing ==
|
||||
* - fish(target?) - Fish at a spot, wait for fish
|
||||
*
|
||||
* == Cooking ==
|
||||
* - cookFood(food?, range?) - Cook food on range/fire
|
||||
*
|
||||
* == Combat ==
|
||||
* - attackNpc(target, timeout?) - Attack NPC and wait for combat to complete
|
||||
* - eatFood(food) - Eat food item from inventory
|
||||
*
|
||||
* == Items & Inventory ==
|
||||
* - pickupItem(target) - Pick up ground item by name/pattern
|
||||
* - equipItem(item) - Equip item from inventory
|
||||
* - unequipItem(item) - Unequip item to inventory
|
||||
* - dropItem(item) - Drop item from inventory
|
||||
* - useItemOnLoc(item, loc) - Use item on location (e.g., logs on fire)
|
||||
* - useItemOnNpc(item, npc) - Use item on NPC
|
||||
* - useItemOnItem(item1, item2) - Use one item on another
|
||||
*
|
||||
* == Doors ==
|
||||
* - openDoor(target?) - Open door, target can be location or pattern
|
||||
*
|
||||
* == Shopping ==
|
||||
* - openShop(target?) - Open shop by talking to NPC
|
||||
* - buyFromShop(item, amount?) - Buy item from open shop
|
||||
* - sellToShop(item, amount?) - Sell inventory item to shop
|
||||
*
|
||||
* == Banking ==
|
||||
* - openBank(timeout?) - Open bank (walks to banker if needed)
|
||||
* - depositItem(item, amount?) - Deposit item to bank
|
||||
* - depositAll() - Deposit entire inventory
|
||||
* - withdrawItem(item, amount?) - Withdraw item from bank
|
||||
*
|
||||
* == Crafting ==
|
||||
* - smithAtAnvil(product, options?) - Smith bars at anvil
|
||||
* - fletchLogs(product?) - Fletch logs into bows/arrows
|
||||
* - craftLeather(product?) - Craft leather at workbench
|
||||
* - spinWool() - Spin wool into ball of wool
|
||||
* - makePotery(product?) - Create pottery items
|
||||
*
|
||||
* == Prayer ==
|
||||
* - buryBones(bones) - Bury bones from inventory
|
||||
*
|
||||
* == Magic ==
|
||||
* - castSpell(spellName, target?) - Cast spell on target or self
|
||||
*
|
||||
* == UI ==
|
||||
* - dismissBlockingUI() - Close any blocking UI (dialogs, level-ups, etc.)
|
||||
* - skipTutorial() - Navigate through tutorial dialogs
|
||||
*
|
||||
* All methods return result objects with { success: boolean, message?: string, ... }
|
||||
* Check result.success before continuing. On failure, result.message explains why.
|
||||
*
|
||||
* Example usage:
|
||||
* const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
* const result = await bot.chopTree(tree);
|
||||
* if (result.success) {
|
||||
* console.log('Got logs:', result.logs);
|
||||
* }
|
||||
*/
|
||||
@@ -0,0 +1,188 @@
|
||||
// Bot connection manager
|
||||
// Supports multiple simultaneous bot connections
|
||||
|
||||
import { BotSDK, deriveGatewayUrl } from '../../sdk/index';
|
||||
import { BotActions } from '../../sdk/actions';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
export interface BotConnection {
|
||||
sdk: BotSDK;
|
||||
bot: BotActions;
|
||||
username: string;
|
||||
connected: boolean;
|
||||
unsubscribeConnectionState?: () => void;
|
||||
}
|
||||
|
||||
class BotManager {
|
||||
private connections: Map<string, BotConnection> = new Map();
|
||||
private defaultGatewayUrl = 'ws://localhost:7780';
|
||||
|
||||
/**
|
||||
* Connect to a bot by name.
|
||||
* If password is not provided, loads credentials from bots/{name}/bot.env
|
||||
*/
|
||||
async connect(name: string, password?: string, gatewayUrl?: string): Promise<BotConnection> {
|
||||
// Check if already connected
|
||||
if (this.connections.has(name)) {
|
||||
const existing = this.connections.get(name)!;
|
||||
if (existing.connected) {
|
||||
return existing;
|
||||
}
|
||||
// Reconnect if disconnected
|
||||
await this.connectWithTimeout(existing.sdk, 30000);
|
||||
existing.connected = true;
|
||||
return existing;
|
||||
}
|
||||
|
||||
let username = name;
|
||||
let pwd = password;
|
||||
let gateway = gatewayUrl || this.defaultGatewayUrl;
|
||||
let showChat = false;
|
||||
|
||||
// Load credentials from bot.env if no password provided
|
||||
if (!password) {
|
||||
const envPath = join(process.cwd(), 'bots', name, 'bot.env');
|
||||
|
||||
if (!existsSync(envPath)) {
|
||||
throw new Error(`Bot "${name}" not found. Create it first with: bun bots/create-bot.ts ${name}`);
|
||||
}
|
||||
|
||||
const envContent = await readFile(envPath, 'utf-8');
|
||||
const env = this.parseEnv(envContent);
|
||||
|
||||
username = env.BOT_USERNAME || name;
|
||||
pwd = env.PASSWORD;
|
||||
|
||||
if (env.SERVER) {
|
||||
gateway = deriveGatewayUrl(env.SERVER);
|
||||
}
|
||||
|
||||
// Check if chat should be shown (default: false for safety)
|
||||
if (env.SHOW_CHAT?.toLowerCase() === 'true') {
|
||||
showChat = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pwd) {
|
||||
throw new Error(`No password provided for bot "${name}"`);
|
||||
}
|
||||
|
||||
console.error(`[MCP] Connecting bot "${name}":`);
|
||||
console.error(`[MCP] username: ${username}`);
|
||||
console.error(`[MCP] gateway: ${gateway}`);
|
||||
console.error(`[MCP] password: ${pwd ? pwd.substring(0, 3) + '...' : 'MISSING'}`);
|
||||
|
||||
const sdk = new BotSDK({
|
||||
botUsername: username,
|
||||
password: pwd,
|
||||
gatewayUrl: gateway,
|
||||
connectionMode: 'control',
|
||||
autoReconnect: true, // Enable auto-reconnect for connection stability
|
||||
autoLaunchBrowser: 'auto', // Auto-launch browser if session is stale
|
||||
showChat, // Show other players' chat (default: false for safety)
|
||||
});
|
||||
|
||||
const bot = new BotActions(sdk);
|
||||
|
||||
// Connect with 30s timeout to avoid blocking forever
|
||||
console.error(`[MCP] Starting connection...`);
|
||||
await this.connectWithTimeout(sdk, 30000);
|
||||
console.error(`[MCP] Bot "${name}" connected!`);
|
||||
|
||||
const connection: BotConnection = {
|
||||
sdk,
|
||||
bot,
|
||||
username,
|
||||
connected: true
|
||||
};
|
||||
|
||||
// Track connection state changes
|
||||
connection.unsubscribeConnectionState = sdk.onConnectionStateChange((state) => {
|
||||
const wasConnected = connection.connected;
|
||||
connection.connected = state === 'connected';
|
||||
if (wasConnected && !connection.connected) {
|
||||
console.error(`[MCP] Bot "${name}" connection lost (${state}), will auto-reconnect...`);
|
||||
} else if (!wasConnected && connection.connected) {
|
||||
console.error(`[MCP] Bot "${name}" reconnected!`);
|
||||
}
|
||||
});
|
||||
|
||||
this.connections.set(name, connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async connectWithTimeout(sdk: BotSDK, timeoutMs: number): Promise<void> {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs / 1000}s`)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([sdk.connect(), timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a bot by name
|
||||
*/
|
||||
async disconnect(name: string): Promise<void> {
|
||||
const connection = this.connections.get(name);
|
||||
if (!connection) {
|
||||
throw new Error(`Bot "${name}" is not connected`);
|
||||
}
|
||||
|
||||
console.error(`[MCP] Disconnecting bot "${name}"...`);
|
||||
if (connection.unsubscribeConnectionState) {
|
||||
connection.unsubscribeConnectionState();
|
||||
}
|
||||
connection.sdk.disconnect();
|
||||
connection.connected = false;
|
||||
this.connections.delete(name);
|
||||
console.error(`[MCP] Bot "${name}" disconnected`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bot connection by name
|
||||
*/
|
||||
get(name: string): BotConnection | undefined {
|
||||
return this.connections.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all connected bots
|
||||
*/
|
||||
list(): Array<{ name: string; username: string; connected: boolean }> {
|
||||
return Array.from(this.connections.entries()).map(([name, conn]) => ({
|
||||
name,
|
||||
username: conn.username,
|
||||
connected: conn.connected
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a bot is connected
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
return this.connections.has(name);
|
||||
}
|
||||
|
||||
private parseEnv(content: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
result[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const botManager = new BotManager();
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Low-level Bot SDK API
|
||||
*
|
||||
* This module provides direct access to the bot protocol and state queries.
|
||||
* Methods resolve when server ACKNOWLEDGES them (not when effects complete).
|
||||
* For most use cases, prefer the high-level `bot` API instead.
|
||||
*
|
||||
* Import via: const { sdk } = await import('./api/index');
|
||||
* Or use directly if passed as parameter: sdk.getState(), sdk.sendWalk(), etc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Available methods on the sdk object:
|
||||
*
|
||||
* == Connection ==
|
||||
* - isConnected() - Check if connected to gateway
|
||||
* - getConnectionState() - Get connection state ('connected', 'connecting', 'disconnected', 'reconnecting')
|
||||
*
|
||||
* == State Access (Synchronous) ==
|
||||
* - getState(): BotWorldState - Full state snapshot. Shape: { tick, inGame, player: PlayerState | null, skills: SkillState[], inventory: InventoryItem[], equipment: InventoryItem[], nearbyNpcs: NearbyNpc[], nearbyPlayers: NearbyPlayer[], nearbyLocs: NearbyLoc[], groundItems: GroundItem[], gameMessages: GameMessage[], dialog: DialogState, shop: ShopState, bank: BankState, combatEvents: CombatEvent[] }
|
||||
* - getStateAge(): number - Milliseconds since last state update
|
||||
*
|
||||
* == State Queries (all return directly, NOT wrapped in objects) ==
|
||||
* - getInventory(): InventoryItem[] - Returns array directly. Each item: { slot, id, name, count, optionsWithIndex }
|
||||
* - findInventoryItem(pattern): InventoryItem | undefined - Find item by name/id/regex
|
||||
* - getNearbyNpcs(): NearbyNpc[] - Returns array directly. Each NPC: { index, name, combatLevel, x, z, distance, hp, maxHp, healthPercent, inCombat, options }
|
||||
* - findNearbyNpc(pattern): NearbyNpc | undefined - Find NPC by name/id/regex
|
||||
* - getNearbyLocs(): NearbyLoc[] - Returns array directly. Each loc: { id, name, x, z, distance, options }
|
||||
* - findNearbyLoc(pattern): NearbyLoc | undefined - Find location by name/regex
|
||||
* - getGroundItems(): GroundItem[] - Returns array directly. Each: { id, name, count, x, z, distance }
|
||||
* - findGroundItem(pattern): GroundItem | undefined - Find ground item by name/regex
|
||||
* - getSkill(name): SkillState | undefined - Returns { name, level, baseLevel, experience }
|
||||
* - getAllSkills(): SkillState[] - All skills
|
||||
* - getEquippedItems(): InventoryItem[] - Worn equipment as array
|
||||
*
|
||||
* == Key Types ==
|
||||
* PlayerState: { name, combatLevel, x, z, worldX, worldZ, level (floor 0-3), runEnergy, animId, combat: { inCombat, targetIndex, lastDamageTick } }
|
||||
* NOTE: PlayerState has NO hp/health field. Use sdk.getSkill('hitpoints') for current HP.
|
||||
*
|
||||
* == Raw Actions (Promise-based, resolve on acknowledgment) ==
|
||||
* - sendWalk(x, z, running?) - Walk to coordinates
|
||||
* - sendInteractLoc(x, z, locId, option) - Interact with location (chop tree, mine rock, etc.)
|
||||
* - sendInteractNpc(npcIndex, option) - Interact with NPC (talk, attack, trade, etc.)
|
||||
* - sendTakeGroundItem(x, z, itemId) - Pick up ground item
|
||||
* - sendUseItem(slot, option) - Use/equip/drop item
|
||||
* - sendUseItemOnLoc(slot, x, z, locId) - Use item on location
|
||||
* - sendUseItemOnNpc(slot, npcIndex) - Use item on NPC
|
||||
* - sendUseItemOnItem(slot1, slot2) - Use item on another item
|
||||
* - sendUseItemOnGroundItem(slot, x, z, itemId) - Use item on ground item
|
||||
* - sendClickDialog(option) - Click dialog option
|
||||
* - sendOpenBank() - Request to open bank
|
||||
* - sendBankDeposit(slot, amount) - Deposit item to bank
|
||||
* - sendBankWithdraw(bankSlot, amount) - Withdraw item from bank
|
||||
* - sendShopBuy(shopSlot, amount) - Buy from shop
|
||||
* - sendShopSell(invSlot, amount) - Sell to shop
|
||||
* - sendCastSpell(spellName) - Cast spell
|
||||
* - sendCastSpellOnNpc(spellName, npcIndex) - Cast spell on NPC
|
||||
* - sendCastSpellOnItem(spellName, slot) - Cast spell on item
|
||||
* - sendCastSpellOnGroundItem(spellName, x, z, itemId) - Cast spell on ground item
|
||||
*
|
||||
* == Utility ==
|
||||
* - findPath(startX, startZ, endX, endZ) - Find walkable path between two points
|
||||
* - sendScreenshot() - Request screenshot from bot client (returns base64 data URL)
|
||||
* - checkBotStatus() - Check if bot client is connected to gateway
|
||||
* - waitForCondition(predicate, timeout?) - Wait for state to match predicate
|
||||
* - waitForStateChange(timeout?) - Wait for any state update
|
||||
*
|
||||
* == Listeners ==
|
||||
* - onStateUpdate(callback) - Register listener for state updates (returns unsubscribe function)
|
||||
* - onConnectionStateChange(callback) - Register listener for connection changes
|
||||
*
|
||||
* All send* methods return Promise<ActionResult> with { success: boolean, message?: string }
|
||||
* State query methods return data synchronously from cached state.
|
||||
*
|
||||
* Example usage:
|
||||
* const state = sdk.getState();
|
||||
* console.log('Position:', state.player.worldX, state.player.worldZ);
|
||||
*
|
||||
* const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
* if (tree) {
|
||||
* const result = await sdk.sendInteractLoc(tree.x, tree.z, tree.id, 0);
|
||||
* console.log('Chop result:', result);
|
||||
* }
|
||||
*/
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "rs-agent-mcp",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "rs-agent-mcp",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
}
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* MCP Code Execution Server for RS-Agent
|
||||
*
|
||||
* Manages multiple bot connections dynamically at runtime.
|
||||
* Agents can connect, disconnect, and execute code on any connected bot.
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
ListToolsRequestSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { botManager } from './api/index.js';
|
||||
import { formatWorldState } from '../sdk/formatter.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Create MCP server
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'rs-agent-bot',
|
||||
version: '2.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// List available API modules as resources
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
uri: 'file://api/bot.ts',
|
||||
name: 'Bot API',
|
||||
description: 'High-level bot actions: chopTree, walkTo, attackNpc, openBank, etc. Domain-aware methods that wait for effects.',
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
{
|
||||
uri: 'file://api/sdk.ts',
|
||||
name: 'SDK API',
|
||||
description: 'Low-level SDK: getState, sendWalk, getInventory, findNearbyNpc, etc. Direct protocol access.',
|
||||
mimeType: 'text/plain'
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Read API module contents
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
try {
|
||||
const uri = request.params.uri;
|
||||
let filePath: string;
|
||||
|
||||
if (uri.startsWith('file://')) {
|
||||
const relativePath = uri.replace('file://', '');
|
||||
filePath = join(__dirname, relativePath);
|
||||
} else {
|
||||
throw new Error(`Unsupported URI scheme: ${uri}`);
|
||||
}
|
||||
|
||||
const content = await Bun.file(filePath).text();
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'text/plain',
|
||||
text: content
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to read resource: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'execute_code',
|
||||
description: 'Execute TypeScript code on a bot. Auto-connects using credentials from bots/{name}/bot.env. The code runs in an async context with bot (BotActions) and sdk (BotSDK) available.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bot_name: {
|
||||
type: 'string',
|
||||
description: 'Bot name (matches folder in bots/). Auto-connects on first use.'
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'TypeScript code to execute. Available globals: bot (BotActions), sdk (BotSDK). Example: "await bot.chopTree(); return sdk.getState();"'
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Execution timeout in minutes (default: 2, max: 60)'
|
||||
}
|
||||
},
|
||||
required: ['bot_name', 'code']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'disconnect_bot',
|
||||
description: 'Disconnect a connected bot',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Bot name to disconnect'
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'list_bots',
|
||||
description: 'List all connected bots',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'disconnect_bot': {
|
||||
const botName = args?.name as string;
|
||||
|
||||
if (!botName) {
|
||||
return errorResponse('Bot name is required');
|
||||
}
|
||||
|
||||
await botManager.disconnect(botName);
|
||||
return successResponse({ message: `Disconnected bot "${botName}"` });
|
||||
}
|
||||
|
||||
case 'list_bots': {
|
||||
const bots = botManager.list();
|
||||
return successResponse({
|
||||
bots,
|
||||
count: bots.length
|
||||
});
|
||||
}
|
||||
|
||||
case 'execute_code': {
|
||||
const botName = args?.bot_name as string;
|
||||
const code = args?.code as string;
|
||||
|
||||
if (!botName) {
|
||||
return errorResponse('bot_name is required');
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return errorResponse('code is required');
|
||||
}
|
||||
|
||||
const isLongCode = code.length > 2000;
|
||||
|
||||
// Capture console output BEFORE connecting to prevent SDK logs from corrupting MCP JSON-RPC stdout
|
||||
const logs: string[] = [];
|
||||
const originalLog = console.log;
|
||||
const originalWarn = console.warn;
|
||||
const originalError = console.error;
|
||||
|
||||
console.log = (...args) => logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '));
|
||||
console.warn = (...args) => logs.push('[warn] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '));
|
||||
// Don't capture console.error - let it go to stderr for MCP debugging
|
||||
|
||||
// Auto-connect if not already connected
|
||||
let connection = botManager.get(botName);
|
||||
if (!connection) {
|
||||
console.error(`[MCP] Bot "${botName}" not connected, auto-connecting...`);
|
||||
connection = await botManager.connect(botName);
|
||||
// Wait up to 15s for initial world state after fresh connection
|
||||
try {
|
||||
await connection.sdk.waitForCondition(() => connection!.sdk.getState() !== null, 15000);
|
||||
} catch {
|
||||
console.error(`[MCP] Warning: initial state not received within 15s for bot "${botName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const fn = new AsyncFunction('bot', 'sdk', code);
|
||||
|
||||
// Execute code with configurable timeout + MCP cancellation signal
|
||||
const timeoutMinutes = Math.min(Math.max((args?.timeout as number) || 2, 0.1), 60);
|
||||
const EXECUTION_TIMEOUT = timeoutMinutes * 60 * 1000;
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(`Code execution timed out after ${timeoutMinutes} minute(s)`)), EXECUTION_TIMEOUT);
|
||||
});
|
||||
|
||||
// AbortController that fires on MCP cancellation
|
||||
const abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
|
||||
if (extra.signal) {
|
||||
if (extra.signal.aborted) {
|
||||
abortController.abort(extra.signal.reason);
|
||||
} else {
|
||||
extra.signal.addEventListener('abort', () => {
|
||||
console.error(`[MCP] execute_code cancelled by client for bot "${botName}"`);
|
||||
abortController.abort('Cancelled by client');
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
const cancelPromise = new Promise<never>((_, reject) => {
|
||||
signal.addEventListener('abort', () => {
|
||||
reject(new Error(typeof signal.reason === 'string' ? signal.reason : 'Code execution cancelled'));
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
// Wrap bot and sdk in proxies that throw on every method call once cancelled
|
||||
const cancellable = <T extends object>(target: T): T =>
|
||||
new Proxy(target, {
|
||||
get(obj, prop, receiver) {
|
||||
const value = Reflect.get(obj, prop, receiver);
|
||||
if (typeof value === 'function') {
|
||||
return (...args: any[]) => {
|
||||
if (signal.aborted) throw new Error('Execution cancelled');
|
||||
return value.apply(obj, args);
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
let result: any;
|
||||
try {
|
||||
result = await Promise.race([fn(cancellable(connection.bot), cancellable(connection.sdk)), timeoutPromise, cancelPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId!);
|
||||
if (!signal.aborted) abortController.abort('Execution finished');
|
||||
}
|
||||
|
||||
// Build formatted output
|
||||
const parts: string[] = [];
|
||||
|
||||
if (logs.length > 0) {
|
||||
parts.push('── Console ──');
|
||||
parts.push(logs.join('\n'));
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
if (logs.length > 0) parts.push('');
|
||||
parts.push('── Result ──');
|
||||
parts.push(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
// Append formatted world state
|
||||
const state = connection.sdk.getState();
|
||||
if (state) {
|
||||
parts.push('');
|
||||
parts.push('── World State ──');
|
||||
parts.push(formatWorldState(state, connection.sdk.getStateAge()));
|
||||
}
|
||||
|
||||
// Add reminder for long code
|
||||
if (isLongCode) {
|
||||
parts.push('');
|
||||
parts.push('── Tip ──');
|
||||
parts.push(`Long script detected. Consider writing to a .ts file and running with: bun run bots/${botName}/script.ts`);
|
||||
}
|
||||
|
||||
const output = parts.length > 0 ? parts.join('\n') : '(no output)';
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
console.warn = originalWarn;
|
||||
console.error = originalError;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = `Error: ${error.message}\n\nStack trace:\n${error.stack}`;
|
||||
return {
|
||||
content: [{ type: 'text', text: errorMessage }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function successResponse(data: any) {
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
||||
};
|
||||
}
|
||||
|
||||
function errorResponse(message: string) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${message}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Start server
|
||||
async function main() {
|
||||
console.error('[MCP Server] Starting RS-Agent MCP server v2.0...');
|
||||
console.error('[MCP Server] No bots connected. Use connect_bot tool to connect.');
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
console.error('[MCP Server] Server running on stdio');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[MCP Server] Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"rs-agent": {
|
||||
"type": "local",
|
||||
"command": ["bun", "run", "mcp/server.ts"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user