@blockrun/runcode 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { ModelClient } from './llm.js';
7
7
  import { autoCompactIfNeeded, forceCompact, microCompact } from './compact.js';
8
- import { estimateHistoryTokens } from './tokens.js';
8
+ import { estimateHistoryTokens, updateActualTokens, resetTokenAnchor } from './tokens.js';
9
9
  import { PermissionManager } from './permissions.js';
10
10
  import { StreamingExecutor } from './streaming-executor.js';
11
11
  import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS } from './optimize.js';
@@ -287,6 +287,26 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
287
287
  onEvent({ kind: 'turn_done', reason: 'completed' });
288
288
  continue;
289
289
  }
290
+ // Handle /mcp — show connected MCP servers
291
+ if (input === '/mcp') {
292
+ const { listMcpServers } = await import('../mcp/client.js');
293
+ const servers = listMcpServers();
294
+ if (servers.length === 0) {
295
+ onEvent({ kind: 'text_delta', text: 'No MCP servers connected.\nAdd servers to `~/.blockrun/mcp.json` or `.mcp.json` in your project.\n' });
296
+ }
297
+ else {
298
+ let text = `**${servers.length} MCP server(s) connected:**\n\n`;
299
+ for (const s of servers) {
300
+ text += ` **${s.name}** — ${s.toolCount} tools\n`;
301
+ for (const t of s.tools) {
302
+ text += ` · ${t}\n`;
303
+ }
304
+ }
305
+ onEvent({ kind: 'text_delta', text });
306
+ }
307
+ onEvent({ kind: 'turn_done', reason: 'completed' });
308
+ continue;
309
+ }
290
310
  // Handle /bug — open issue tracker
291
311
  if (input === '/bug') {
292
312
  onEvent({ kind: 'text_delta', text: 'Report issues at: https://github.com/BlockRunAI/runcode/issues\n' });
@@ -480,7 +500,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
480
500
  }
481
501
  // Handle /context — show current session context info
482
502
  if (input === '/context') {
483
- const tokens = estimateHistoryTokens(history);
503
+ const { getAnchoredTokenCount, getContextWindow } = await import('./tokens.js');
504
+ const { estimated, apiAnchored } = getAnchoredTokenCount(history);
505
+ const contextWindow = getContextWindow(config.model);
506
+ const usagePct = ((estimated / contextWindow) * 100).toFixed(1);
484
507
  const msgs = history.length;
485
508
  const model = config.model;
486
509
  const dir = config.workingDir || process.cwd();
@@ -489,7 +512,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
489
512
  ` Model: ${model}\n` +
490
513
  ` Mode: ${mode}\n` +
491
514
  ` Messages: ${msgs}\n` +
492
- ` Tokens: ~${tokens.toLocaleString()}\n` +
515
+ ` Tokens: ~${estimated.toLocaleString()} / ${(contextWindow / 1000).toFixed(0)}k (${usagePct}%)${apiAnchored ? ' ✓' : ' ~'}\n` +
493
516
  ` Session: ${sessionId}\n` +
494
517
  ` Directory: ${dir}\n`
495
518
  });
@@ -605,6 +628,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
605
628
  if (didCompact) {
606
629
  history.length = 0;
607
630
  history.push(...compacted);
631
+ resetTokenAnchor(); // Reset anchor after compaction — estimates will be used
608
632
  if (config.debug) {
609
633
  console.error(`[runcode] History compacted: ~${estimateHistoryTokens(history)} tokens`);
610
634
  }
@@ -696,6 +720,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
696
720
  onEvent({ kind: 'turn_done', reason: 'error', error: errMsg + suggestion });
697
721
  break;
698
722
  }
723
+ // Anchor token tracking to actual API counts
724
+ updateActualTokens(usage.inputTokens, usage.outputTokens, history.length);
699
725
  onEvent({
700
726
  kind: 'usage',
701
727
  inputTokens: usage.inputTokens,
@@ -1,8 +1,27 @@
1
1
  /**
2
2
  * Token estimation for runcode.
3
3
  * Uses byte-based heuristic (no external tokenizer dependency).
4
+ * Anchors to actual API counts when available, estimates on top for new messages.
4
5
  */
5
6
  import type { Dialogue } from './types.js';
7
+ /**
8
+ * Update with actual token counts from API response.
9
+ * This anchors our estimates to reality.
10
+ */
11
+ export declare function updateActualTokens(inputTokens: number, outputTokens: number, messageCount: number): void;
12
+ /**
13
+ * Get token count using API anchor + estimation for new messages.
14
+ * More accurate than pure estimation because it's grounded in actual API counts.
15
+ */
16
+ export declare function getAnchoredTokenCount(history: Dialogue[]): {
17
+ estimated: number;
18
+ apiAnchored: boolean;
19
+ contextUsagePct: number;
20
+ };
21
+ /**
22
+ * Reset anchor (e.g., after compaction).
23
+ */
24
+ export declare function resetTokenAnchor(): void;
6
25
  /**
7
26
  * Estimate token count for a string using byte-length heuristic.
8
27
  * JSON-heavy content uses 2 bytes/token; general text uses 4.
@@ -1,8 +1,57 @@
1
1
  /**
2
2
  * Token estimation for runcode.
3
3
  * Uses byte-based heuristic (no external tokenizer dependency).
4
+ * Anchors to actual API counts when available, estimates on top for new messages.
4
5
  */
5
6
  const DEFAULT_BYTES_PER_TOKEN = 4;
7
+ // ─── API-anchored token tracking ───────────────────────���──────────────────
8
+ /** Last known actual token count from API response */
9
+ let lastApiInputTokens = 0;
10
+ let lastApiOutputTokens = 0;
11
+ let lastApiMessageCount = 0;
12
+ /**
13
+ * Update with actual token counts from API response.
14
+ * This anchors our estimates to reality.
15
+ */
16
+ export function updateActualTokens(inputTokens, outputTokens, messageCount) {
17
+ lastApiInputTokens = inputTokens;
18
+ lastApiOutputTokens = outputTokens;
19
+ lastApiMessageCount = messageCount;
20
+ }
21
+ /**
22
+ * Get token count using API anchor + estimation for new messages.
23
+ * More accurate than pure estimation because it's grounded in actual API counts.
24
+ */
25
+ export function getAnchoredTokenCount(history) {
26
+ if (lastApiInputTokens > 0 && history.length >= lastApiMessageCount) {
27
+ // Anchor to API count, estimate only new messages
28
+ const newMessages = history.slice(lastApiMessageCount);
29
+ let newTokens = 0;
30
+ for (const msg of newMessages) {
31
+ newTokens += estimateDialogueTokens(msg);
32
+ }
33
+ const total = lastApiInputTokens + newTokens;
34
+ return {
35
+ estimated: total,
36
+ apiAnchored: true,
37
+ contextUsagePct: 0, // Will be calculated by caller with model context window
38
+ };
39
+ }
40
+ // No anchor — pure estimation
41
+ return {
42
+ estimated: estimateHistoryTokens(history),
43
+ apiAnchored: false,
44
+ contextUsagePct: 0,
45
+ };
46
+ }
47
+ /**
48
+ * Reset anchor (e.g., after compaction).
49
+ */
50
+ export function resetTokenAnchor() {
51
+ lastApiInputTokens = 0;
52
+ lastApiOutputTokens = 0;
53
+ lastApiMessageCount = 0;
54
+ }
6
55
  /**
7
56
  * Estimate token count for a string using byte-length heuristic.
8
57
  * JSON-heavy content uses 2 bytes/token; general text uses 4.
@@ -9,6 +9,8 @@ import { interactiveSession } from '../agent/loop.js';
9
9
  import { allCapabilities, createSubAgentCapability } from '../tools/index.js';
10
10
  import { launchInkUI } from '../ui/app.js';
11
11
  import { pickModel, resolveModel } from '../ui/model-picker.js';
12
+ import { loadMcpConfig } from '../mcp/config.js';
13
+ import { connectMcpServers, disconnectMcpServers } from '../mcp/client.js';
12
14
  export async function startCommand(options) {
13
15
  const version = options.version ?? '1.0.0';
14
16
  const chain = loadChain();
@@ -92,9 +94,26 @@ export async function startCommand(options) {
92
94
  })();
93
95
  // Assemble system instructions
94
96
  const systemInstructions = assembleInstructions(workDir);
95
- // Build capabilities
97
+ // Connect MCP servers (non-blocking — add tools if servers are available)
98
+ const mcpConfig = loadMcpConfig(workDir);
99
+ let mcpTools = [];
100
+ const mcpServerCount = Object.keys(mcpConfig.mcpServers).filter(k => !mcpConfig.mcpServers[k].disabled).length;
101
+ if (mcpServerCount > 0) {
102
+ try {
103
+ mcpTools = await connectMcpServers(mcpConfig, options.debug);
104
+ if (mcpTools.length > 0) {
105
+ console.log(chalk.dim(` MCP: ${mcpTools.length} tools from ${mcpServerCount} server(s)`));
106
+ }
107
+ }
108
+ catch (err) {
109
+ if (options.debug) {
110
+ console.error(chalk.yellow(` MCP error: ${err.message}`));
111
+ }
112
+ }
113
+ }
114
+ // Build capabilities (built-in + MCP + sub-agent)
96
115
  const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities);
97
- const capabilities = [...allCapabilities, subAgent];
116
+ const capabilities = [...allCapabilities, ...mcpTools, subAgent];
98
117
  // Agent config
99
118
  const agentConfig = {
100
119
  model,
@@ -149,6 +168,7 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
149
168
  }
150
169
  ui.cleanup();
151
170
  flushStats();
171
+ await disconnectMcpServers();
152
172
  console.log(chalk.dim('\nGoodbye.\n'));
153
173
  process.exit(0);
154
174
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * MCP Client for runcode.
3
+ * Connects to MCP servers, discovers tools, and wraps them as CapabilityHandlers.
4
+ * Supports stdio and HTTP (SSE) transports.
5
+ */
6
+ import type { CapabilityHandler } from '../agent/types.js';
7
+ export interface McpServerConfig {
8
+ /** Transport type */
9
+ transport: 'stdio' | 'http';
10
+ /** For stdio: command to run */
11
+ command?: string;
12
+ /** For stdio: arguments */
13
+ args?: string[];
14
+ /** For stdio: environment variables */
15
+ env?: Record<string, string>;
16
+ /** For http: server URL */
17
+ url?: string;
18
+ /** For http: headers */
19
+ headers?: Record<string, string>;
20
+ /** Human-readable label */
21
+ label?: string;
22
+ /** Disable this server */
23
+ disabled?: boolean;
24
+ }
25
+ export interface McpConfig {
26
+ mcpServers: Record<string, McpServerConfig>;
27
+ }
28
+ /**
29
+ * Connect to all configured MCP servers and return discovered tools.
30
+ */
31
+ export declare function connectMcpServers(config: McpConfig, debug?: boolean): Promise<CapabilityHandler[]>;
32
+ /**
33
+ * Disconnect all MCP servers.
34
+ */
35
+ export declare function disconnectMcpServers(): Promise<void>;
36
+ /**
37
+ * List connected MCP servers and their tools.
38
+ */
39
+ export declare function listMcpServers(): Array<{
40
+ name: string;
41
+ toolCount: number;
42
+ tools: string[];
43
+ }>;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * MCP Client for runcode.
3
+ * Connects to MCP servers, discovers tools, and wraps them as CapabilityHandlers.
4
+ * Supports stdio and HTTP (SSE) transports.
5
+ */
6
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
+ // ─── Connection Management ────────────────────────────────────────────────
9
+ const connections = new Map();
10
+ /**
11
+ * Connect to an MCP server via stdio transport.
12
+ * Discovers tools and returns them as CapabilityHandlers.
13
+ */
14
+ async function connectStdio(name, config) {
15
+ if (!config.command) {
16
+ throw new Error(`MCP server "${name}" missing command`);
17
+ }
18
+ const transport = new StdioClientTransport({
19
+ command: config.command,
20
+ args: config.args || [],
21
+ env: { ...process.env, ...(config.env || {}) },
22
+ });
23
+ const client = new Client({ name: `runcode-mcp-${name}`, version: '1.0.0' }, { capabilities: {} });
24
+ await client.connect(transport);
25
+ // Discover tools
26
+ const { tools: mcpTools } = await client.listTools();
27
+ const capabilities = [];
28
+ for (const tool of mcpTools) {
29
+ const toolName = `mcp__${name}__${tool.name}`;
30
+ const toolDescription = (tool.description || '').slice(0, 2048);
31
+ capabilities.push({
32
+ spec: {
33
+ name: toolName,
34
+ description: toolDescription || `MCP tool from ${name}`,
35
+ input_schema: tool.inputSchema || {
36
+ type: 'object',
37
+ properties: {},
38
+ },
39
+ },
40
+ execute: async (input, _ctx) => {
41
+ try {
42
+ const result = await client.callTool({
43
+ name: tool.name,
44
+ arguments: input,
45
+ });
46
+ // Extract text content from MCP response
47
+ const output = result.content
48
+ ?.filter(c => c.type === 'text')
49
+ ?.map(c => c.text)
50
+ ?.join('\n') || JSON.stringify(result.content);
51
+ return {
52
+ output,
53
+ isError: result.isError === true,
54
+ };
55
+ }
56
+ catch (err) {
57
+ return {
58
+ output: `MCP tool error (${name}/${tool.name}): ${err.message}`,
59
+ isError: true,
60
+ };
61
+ }
62
+ },
63
+ concurrent: true, // MCP tools are safe to run concurrently
64
+ });
65
+ }
66
+ const connected = { name, client, transport, tools: capabilities };
67
+ connections.set(name, connected);
68
+ return connected;
69
+ }
70
+ /**
71
+ * Connect to all configured MCP servers and return discovered tools.
72
+ */
73
+ export async function connectMcpServers(config, debug) {
74
+ const allTools = [];
75
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
76
+ if (serverConfig.disabled)
77
+ continue;
78
+ try {
79
+ if (debug) {
80
+ console.error(`[runcode] Connecting to MCP server: ${name}...`);
81
+ }
82
+ let connected;
83
+ if (serverConfig.transport === 'stdio') {
84
+ connected = await connectStdio(name, serverConfig);
85
+ }
86
+ else {
87
+ // HTTP transport — TODO: implement SSE/HTTP transport
88
+ if (debug) {
89
+ console.error(`[runcode] MCP HTTP transport not yet supported for ${name}`);
90
+ }
91
+ continue;
92
+ }
93
+ allTools.push(...connected.tools);
94
+ if (debug) {
95
+ console.error(`[runcode] MCP ${name}: ${connected.tools.length} tools discovered`);
96
+ }
97
+ }
98
+ catch (err) {
99
+ // Graceful degradation — log and continue without this server
100
+ console.error(`[runcode] MCP ${name} failed: ${err.message}`);
101
+ }
102
+ }
103
+ return allTools;
104
+ }
105
+ /**
106
+ * Disconnect all MCP servers.
107
+ */
108
+ export async function disconnectMcpServers() {
109
+ for (const [name, conn] of connections) {
110
+ try {
111
+ await conn.client.close();
112
+ }
113
+ catch {
114
+ // Ignore cleanup errors
115
+ }
116
+ connections.delete(name);
117
+ }
118
+ }
119
+ /**
120
+ * List connected MCP servers and their tools.
121
+ */
122
+ export function listMcpServers() {
123
+ const result = [];
124
+ for (const [name, conn] of connections) {
125
+ result.push({
126
+ name,
127
+ toolCount: conn.tools.length,
128
+ tools: conn.tools.map(t => t.spec.name),
129
+ });
130
+ }
131
+ return result;
132
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * MCP configuration management for runcode.
3
+ * Loads MCP server configs from:
4
+ * 1. Global: ~/.blockrun/mcp.json
5
+ * 2. Project: .mcp.json in working directory
6
+ */
7
+ import type { McpConfig, McpServerConfig } from './client.js';
8
+ /**
9
+ * Load MCP server configurations from global + project files.
10
+ * Project config overrides global for same server name.
11
+ */
12
+ export declare function loadMcpConfig(workDir: string): McpConfig;
13
+ /**
14
+ * Save a server config to the global MCP config.
15
+ */
16
+ export declare function saveMcpServer(name: string, config: McpServerConfig): void;
17
+ /**
18
+ * Remove a server from the global MCP config.
19
+ */
20
+ export declare function removeMcpServer(name: string): boolean;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MCP configuration management for runcode.
3
+ * Loads MCP server configs from:
4
+ * 1. Global: ~/.blockrun/mcp.json
5
+ * 2. Project: .mcp.json in working directory
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { BLOCKRUN_DIR } from '../config.js';
10
+ const GLOBAL_MCP_FILE = path.join(BLOCKRUN_DIR, 'mcp.json');
11
+ /**
12
+ * Load MCP server configurations from global + project files.
13
+ * Project config overrides global for same server name.
14
+ */
15
+ export function loadMcpConfig(workDir) {
16
+ const servers = {};
17
+ // 1. Global config
18
+ try {
19
+ if (fs.existsSync(GLOBAL_MCP_FILE)) {
20
+ const raw = JSON.parse(fs.readFileSync(GLOBAL_MCP_FILE, 'utf-8'));
21
+ if (raw.mcpServers && typeof raw.mcpServers === 'object') {
22
+ Object.assign(servers, raw.mcpServers);
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ // Ignore corrupt global config
28
+ }
29
+ // 2. Project config (.mcp.json in working directory)
30
+ const projectMcpFile = path.join(workDir, '.mcp.json');
31
+ try {
32
+ if (fs.existsSync(projectMcpFile)) {
33
+ const raw = JSON.parse(fs.readFileSync(projectMcpFile, 'utf-8'));
34
+ if (raw.mcpServers && typeof raw.mcpServers === 'object') {
35
+ // Project overrides global for same name
36
+ Object.assign(servers, raw.mcpServers);
37
+ }
38
+ }
39
+ }
40
+ catch {
41
+ // Ignore corrupt project config
42
+ }
43
+ return { mcpServers: servers };
44
+ }
45
+ /**
46
+ * Save a server config to the global MCP config.
47
+ */
48
+ export function saveMcpServer(name, config) {
49
+ const existing = loadGlobalMcpConfig();
50
+ existing.mcpServers[name] = config;
51
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
52
+ fs.writeFileSync(GLOBAL_MCP_FILE, JSON.stringify(existing, null, 2) + '\n');
53
+ }
54
+ /**
55
+ * Remove a server from the global MCP config.
56
+ */
57
+ export function removeMcpServer(name) {
58
+ const existing = loadGlobalMcpConfig();
59
+ if (!(name in existing.mcpServers))
60
+ return false;
61
+ delete existing.mcpServers[name];
62
+ fs.writeFileSync(GLOBAL_MCP_FILE, JSON.stringify(existing, null, 2) + '\n');
63
+ return true;
64
+ }
65
+ function loadGlobalMcpConfig() {
66
+ try {
67
+ if (fs.existsSync(GLOBAL_MCP_FILE)) {
68
+ const raw = JSON.parse(fs.readFileSync(GLOBAL_MCP_FILE, 'utf-8'));
69
+ return { mcpServers: raw.mcpServers || {} };
70
+ }
71
+ }
72
+ catch { /* fresh */ }
73
+ return { mcpServers: {} };
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/runcode",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "RunCode — AI coding agent powered by 41+ models. Pay per use with USDC.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@blockrun/llm": "^1.4.2",
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
47
  "@solana/spl-token": "^0.4.14",
47
48
  "@solana/web3.js": "^1.98.4",
48
49
  "@types/react": "^19.2.14",