droid-mode 0.0.5 → 0.0.7
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/README.md
CHANGED
|
@@ -22,22 +22,50 @@ Droid Mode lets you:
|
|
|
22
22
|
2. Access them **on-demand** through progressive discovery
|
|
23
23
|
3. Run procedural workflows that call MCP tools **outside** the LLM loop
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Benchmarks
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Independent benchmarks comparing Droid Mode against Native MCP (direct HTTP).
|
|
28
28
|
|
|
29
|
-
###
|
|
29
|
+
### Summary
|
|
30
30
|
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
|
31
|
+
| Configuration | Per-Tool Latency | vs Native MCP |
|
|
32
|
+
|---------------|------------------|---------------|
|
|
33
|
+
| **Droid Mode (Daemon)** | **616ms** | **11% faster** |
|
|
34
|
+
| Native MCP (HTTP) | 695ms | baseline |
|
|
35
|
+
| Droid Mode (No Daemon) | 2,365ms | 240% slower |
|
|
35
36
|
|
|
36
|
-
###
|
|
37
|
+
### Single Tool Performance
|
|
38
|
+
|
|
39
|
+
| Call | No Daemon | Daemon | Native MCP |
|
|
40
|
+
|------|-----------|--------|------------|
|
|
41
|
+
| 1 | 2948ms | 845ms | 866ms |
|
|
42
|
+
| 2 | 2442ms | 610ms | 789ms |
|
|
43
|
+
| 3 | 2300ms | 613ms | 798ms |
|
|
44
|
+
| 4 | 2415ms | 706ms | 742ms |
|
|
45
|
+
| 5 | 2334ms | 617ms | 690ms |
|
|
46
|
+
| **Average** | **2488ms** | **678ms** | **777ms** |
|
|
47
|
+
|
|
48
|
+
### Scale Performance (10 Tools)
|
|
49
|
+
|
|
50
|
+
| Metric | No Daemon | Daemon | Native MCP |
|
|
51
|
+
|--------|-----------|--------|------------|
|
|
52
|
+
| Total Time | 23.7s | **6.2s** | 6.9s |
|
|
53
|
+
| Per-Tool Avg | 2365ms | **616ms** | 695ms |
|
|
54
|
+
|
|
55
|
+
### Key Findings
|
|
56
|
+
|
|
57
|
+
1. **Daemon beats Native MCP** — 11-14% faster per-tool latency
|
|
58
|
+
2. **Scales linearly** — No cumulative overhead at 10+ tools
|
|
59
|
+
3. **Consistent** — ±262ms variance vs ±236ms for Native MCP
|
|
60
|
+
4. **No-daemon is prohibitive** — 74% slower, only for one-off calls
|
|
61
|
+
|
|
62
|
+
*Benchmarks: macOS Darwin 25.2.0, Context Repo MCP server, January 2026*
|
|
63
|
+
|
|
64
|
+
## Daemon Mode
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
The daemon maintains persistent MCP connections via a Unix socket, eliminating stdio spawn overhead.
|
|
39
67
|
|
|
40
|
-
|
|
68
|
+
### How It Works
|
|
41
69
|
|
|
42
70
|
```
|
|
43
71
|
┌──────────────────────────────────────────────────┐
|
|
@@ -56,26 +84,33 @@ The daemon maintains a connection pool. After the first call to a server, subseq
|
|
|
56
84
|
### Usage
|
|
57
85
|
|
|
58
86
|
```bash
|
|
59
|
-
#
|
|
60
|
-
dm
|
|
87
|
+
# Daemon auto-starts on first dm call
|
|
88
|
+
dm call list_collections --server context-repo
|
|
61
89
|
|
|
62
|
-
#
|
|
63
|
-
dm
|
|
90
|
+
# Or start manually
|
|
91
|
+
dm daemon start
|
|
64
92
|
|
|
65
93
|
# Check connection status
|
|
66
94
|
dm daemon status
|
|
67
95
|
|
|
68
|
-
#
|
|
96
|
+
# Pre-warm specific servers
|
|
97
|
+
dm daemon warm context-repo
|
|
98
|
+
|
|
99
|
+
# Stop daemon
|
|
69
100
|
dm daemon stop
|
|
101
|
+
|
|
102
|
+
# Bypass daemon for direct call
|
|
103
|
+
dm call tool --server X --no-daemon
|
|
70
104
|
```
|
|
71
105
|
|
|
72
|
-
###
|
|
106
|
+
### Configuration
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
```bash
|
|
109
|
+
# Disable auto-warm for a server
|
|
110
|
+
dm config context-repo autoWarm false
|
|
111
|
+
```
|
|
77
112
|
|
|
78
|
-
The daemon is optional.
|
|
113
|
+
The daemon is optional. Use `--no-daemon` to bypass it.
|
|
79
114
|
|
|
80
115
|
## Installation
|
|
81
116
|
|
|
@@ -171,6 +206,8 @@ dm run --server context-repo \
|
|
|
171
206
|
| `dm daemon start` | Start background daemon |
|
|
172
207
|
| `dm daemon stop` | Stop daemon |
|
|
173
208
|
| `dm daemon status` | Show connection pool status |
|
|
209
|
+
| `dm daemon warm [server]` | Pre-warm server connections |
|
|
210
|
+
| `dm config <server> autoWarm false` | Disable auto-warm for server |
|
|
174
211
|
|
|
175
212
|
## Design Philosophy
|
|
176
213
|
|
package/package.json
CHANGED
|
@@ -8,7 +8,8 @@ import { getToolsCached, compactToolSummaries } from "../lib/tool_index.mjs";
|
|
|
8
8
|
import { searchTools } from "../lib/search.mjs";
|
|
9
9
|
import { hydrateTools } from "../lib/hydrate.mjs";
|
|
10
10
|
import { createToolApi, executeWorkflow, writeRunArtifact } from "../lib/run.mjs";
|
|
11
|
-
import { isDaemonRunning, callViaDaemon, getDaemonStatus, shutdownDaemon, startDaemon } from "../lib/daemon_client.mjs";
|
|
11
|
+
import { isDaemonRunning, callViaDaemon, getDaemonStatus, shutdownDaemon, startDaemon, ensureDaemonRunning, warmServers } from "../lib/daemon_client.mjs";
|
|
12
|
+
import { setServerAutoWarm, getServerState } from "../lib/state.mjs";
|
|
12
13
|
|
|
13
14
|
function usage() {
|
|
14
15
|
return `
|
|
@@ -24,9 +25,13 @@ Usage:
|
|
|
24
25
|
dm call <tool> --server <name> [--args-json '{...}'] [--args-file path.json] [--json] [--no-daemon]
|
|
25
26
|
|
|
26
27
|
Daemon (persistent connections for faster calls):
|
|
27
|
-
dm daemon start → Start background daemon
|
|
28
|
+
dm daemon start → Start background daemon (auto-warms servers)
|
|
28
29
|
dm daemon stop → Stop daemon
|
|
29
30
|
dm daemon status [--json] → Show connection pool status
|
|
31
|
+
dm daemon warm [<server>] → Pre-warm server(s)
|
|
32
|
+
|
|
33
|
+
Configuration:
|
|
34
|
+
dm config <server> autoWarm true|false → Enable/disable auto-warm for server
|
|
30
35
|
|
|
31
36
|
Progressive Disclosure Flow:
|
|
32
37
|
1. dm servers → Discover available MCP servers
|
|
@@ -394,19 +399,21 @@ async function cmdCall(args) {
|
|
|
394
399
|
}
|
|
395
400
|
|
|
396
401
|
// Try daemon first (unless --no-daemon flag is set)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
// Auto-start daemon if not running
|
|
403
|
+
if (!args.flags["no-daemon"]) {
|
|
404
|
+
const daemonReady = await ensureDaemonRunning();
|
|
405
|
+
if (daemonReady) {
|
|
406
|
+
const res = await callViaDaemon({ server: serverName, tool, args: argObj });
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
fail(`Daemon call failed: ${res.error}`);
|
|
409
|
+
}
|
|
410
|
+
if (args.flags.json) {
|
|
411
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
process.stdout.write(JSON.stringify(res.result, null, 2) + "\n");
|
|
406
415
|
return;
|
|
407
416
|
}
|
|
408
|
-
process.stdout.write(JSON.stringify(res.result, null, 2) + "\n");
|
|
409
|
-
return;
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
// Direct call (no daemon)
|
|
@@ -488,7 +495,65 @@ async function cmdDaemon(args) {
|
|
|
488
495
|
return;
|
|
489
496
|
}
|
|
490
497
|
|
|
491
|
-
|
|
498
|
+
if (subcmd === "warm") {
|
|
499
|
+
const running = await isDaemonRunning();
|
|
500
|
+
if (!running) {
|
|
501
|
+
// Auto-start daemon for warm command
|
|
502
|
+
try {
|
|
503
|
+
await startDaemon();
|
|
504
|
+
process.stdout.write("Daemon started.\n");
|
|
505
|
+
} catch (err) {
|
|
506
|
+
fail(`Failed to start daemon: ${err.message}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const serverName = args._[2]; // Optional: specific server
|
|
511
|
+
const result = await warmServers(serverName);
|
|
512
|
+
|
|
513
|
+
if (args.flags.json) {
|
|
514
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (serverName) {
|
|
519
|
+
process.stdout.write(result.warmed ? `Warmed: ${serverName}\n` : `Already warm: ${serverName}\n`);
|
|
520
|
+
} else {
|
|
521
|
+
const { warmed = [], failed = [] } = result;
|
|
522
|
+
if (warmed.length) {
|
|
523
|
+
process.stdout.write(`Warmed ${warmed.length} server(s): ${warmed.join(", ")}\n`);
|
|
524
|
+
}
|
|
525
|
+
if (failed.length) {
|
|
526
|
+
process.stdout.write(`Failed: ${failed.join(", ")}\n`);
|
|
527
|
+
}
|
|
528
|
+
if (!warmed.length && !failed.length) {
|
|
529
|
+
process.stdout.write("All servers already warm or none configured for auto-warm.\n");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fail(`Unknown daemon subcommand: ${subcmd}\nUsage: dm daemon start|stop|status|warm`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function cmdConfig(args) {
|
|
539
|
+
const serverName = args._[1];
|
|
540
|
+
const key = args._[2];
|
|
541
|
+
const value = args._[3];
|
|
542
|
+
|
|
543
|
+
if (!serverName || !key) {
|
|
544
|
+
fail("Usage: dm config <server> <key> <value>\n\nSupported keys:\n autoWarm true|false - Enable/disable auto-warm for server");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (key === "autoWarm") {
|
|
548
|
+
if (value !== "true" && value !== "false") {
|
|
549
|
+
fail("autoWarm must be 'true' or 'false'");
|
|
550
|
+
}
|
|
551
|
+
setServerAutoWarm(serverName, value === "true");
|
|
552
|
+
process.stdout.write(`Set ${serverName}.autoWarm = ${value}\n`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
fail(`Unknown config key: ${key}\nSupported: autoWarm`);
|
|
492
557
|
}
|
|
493
558
|
|
|
494
559
|
async function main() {
|
|
@@ -509,6 +574,7 @@ async function main() {
|
|
|
509
574
|
if (cmd === "run") return await cmdRun(args);
|
|
510
575
|
if (cmd === "call") return await cmdCall(args);
|
|
511
576
|
if (cmd === "daemon") return await cmdDaemon(args);
|
|
577
|
+
if (cmd === "config") return await cmdConfig(args);
|
|
512
578
|
fail(`Unknown command: ${cmd}\n\n${usage()}`);
|
|
513
579
|
} catch (err) {
|
|
514
580
|
fail(err.message || err);
|
|
@@ -2,10 +2,12 @@ import net from "node:net";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { McpClient } from "./mcp_client.mjs";
|
|
5
|
-
import { resolveServer, listAllServers } from "./config.mjs";
|
|
5
|
+
import { resolveServer, listAllServers, loadMcpConfigs } from "./config.mjs";
|
|
6
|
+
import { loadState, saveState, getServersToWarm, recordServerUsage } from "./state.mjs";
|
|
6
7
|
|
|
7
8
|
const SOCKET_PATH = process.env.DM_DAEMON_SOCKET || "/tmp/dm-daemon.sock";
|
|
8
9
|
const IDLE_TIMEOUT_MS = parseInt(process.env.DM_DAEMON_IDLE_MS || "600000", 10); // 10 minutes default
|
|
10
|
+
const MAX_CONNECTIONS = 50;
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Connection pool managing lazy-initialized MCP clients.
|
|
@@ -30,6 +32,11 @@ class ConnectionPool {
|
|
|
30
32
|
return existing.client;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// Enforce max connections limit
|
|
36
|
+
if (this.connections.size >= MAX_CONNECTIONS) {
|
|
37
|
+
throw new Error(`Connection pool full (max ${MAX_CONNECTIONS}). Close unused connections or increase limit.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
// Lazy init: resolve from mcp.json, connect, cache
|
|
34
41
|
const { entry } = resolveServer({ server: serverName });
|
|
35
42
|
const client = new McpClient({ serverName, entry });
|
|
@@ -42,9 +49,66 @@ class ConnectionPool {
|
|
|
42
49
|
callCount: 1,
|
|
43
50
|
});
|
|
44
51
|
|
|
52
|
+
// Record usage for auto-warm state
|
|
53
|
+
recordServerUsage(serverName);
|
|
54
|
+
|
|
45
55
|
return client;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Pre-warm a specific server connection.
|
|
60
|
+
* @param {string} serverName
|
|
61
|
+
* @returns {Promise<boolean>} true if warmed, false if already warm or failed
|
|
62
|
+
*/
|
|
63
|
+
async warmServer(serverName) {
|
|
64
|
+
if (this.connections.has(serverName)) {
|
|
65
|
+
return false; // Already warm
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await this.getClient(serverName);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (process.env.DM_DEBUG === "1") {
|
|
73
|
+
process.stderr.write(`[daemon] Failed to warm ${serverName}: ${err.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pre-warm all servers that should be auto-warmed.
|
|
81
|
+
* @returns {Promise<{ warmed: string[], failed: string[] }>}
|
|
82
|
+
*/
|
|
83
|
+
async warmAll() {
|
|
84
|
+
const servers = listAllServers();
|
|
85
|
+
const serverNames = servers.map((s) => s.name);
|
|
86
|
+
const toWarm = getServersToWarm(serverNames);
|
|
87
|
+
|
|
88
|
+
const warmed = [];
|
|
89
|
+
const failed = [];
|
|
90
|
+
|
|
91
|
+
// Warm in parallel with concurrency limit
|
|
92
|
+
const results = await Promise.allSettled(
|
|
93
|
+
toWarm.map(async (name) => {
|
|
94
|
+
const success = await this.warmServer(name);
|
|
95
|
+
return { name, success };
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
for (const result of results) {
|
|
100
|
+
if (result.status === "fulfilled" && result.value.success) {
|
|
101
|
+
warmed.push(result.value.name);
|
|
102
|
+
} else if (result.status === "fulfilled") {
|
|
103
|
+
// Already warm, don't count as failed
|
|
104
|
+
} else {
|
|
105
|
+
failed.push(result.reason?.name || "unknown");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { warmed, failed };
|
|
110
|
+
}
|
|
111
|
+
|
|
48
112
|
/**
|
|
49
113
|
* Get status of all connections.
|
|
50
114
|
*/
|
|
@@ -185,6 +249,18 @@ export class DaemonServer {
|
|
|
185
249
|
}
|
|
186
250
|
}
|
|
187
251
|
|
|
252
|
+
if (action === "warm") {
|
|
253
|
+
if (server) {
|
|
254
|
+
// Warm specific server
|
|
255
|
+
const success = await this.pool.warmServer(server);
|
|
256
|
+
return { ok: true, server, warmed: success };
|
|
257
|
+
} else {
|
|
258
|
+
// Warm all auto-warm servers
|
|
259
|
+
const result = await this.pool.warmAll();
|
|
260
|
+
return { ok: true, ...result };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
188
264
|
if (action === "shutdown") {
|
|
189
265
|
// Graceful shutdown
|
|
190
266
|
setImmediate(() => this.stop());
|
|
@@ -194,6 +270,32 @@ export class DaemonServer {
|
|
|
194
270
|
return { ok: false, error: `Unknown action: ${action}` };
|
|
195
271
|
}
|
|
196
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Start watching mcp.json for changes.
|
|
275
|
+
*/
|
|
276
|
+
startMcpWatcher() {
|
|
277
|
+
const { projectPath, userPath } = loadMcpConfigs();
|
|
278
|
+
const watchPaths = [userPath, projectPath].filter(Boolean);
|
|
279
|
+
|
|
280
|
+
for (const watchPath of watchPaths) {
|
|
281
|
+
if (!fs.existsSync(watchPath)) continue;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
fs.watch(watchPath, { persistent: false }, async (eventType) => {
|
|
285
|
+
if (eventType === "change") {
|
|
286
|
+
if (process.env.DM_DEBUG === "1") {
|
|
287
|
+
process.stderr.write(`[daemon] mcp.json changed, warming new servers...\n`);
|
|
288
|
+
}
|
|
289
|
+
// Re-warm any new servers
|
|
290
|
+
await this.pool.warmAll();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// Ignore watch errors (file might not exist yet)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
197
299
|
/**
|
|
198
300
|
* Start the daemon server.
|
|
199
301
|
*/
|
|
@@ -236,9 +338,19 @@ export class DaemonServer {
|
|
|
236
338
|
|
|
237
339
|
this.server.on("error", reject);
|
|
238
340
|
|
|
239
|
-
this.server.listen(SOCKET_PATH, () => {
|
|
341
|
+
this.server.listen(SOCKET_PATH, async () => {
|
|
240
342
|
// Set socket permissions (user only)
|
|
241
343
|
fs.chmodSync(SOCKET_PATH, 0o600);
|
|
344
|
+
|
|
345
|
+
// Start mcp.json watcher
|
|
346
|
+
this.startMcpWatcher();
|
|
347
|
+
|
|
348
|
+
// Auto-warm servers on startup
|
|
349
|
+
const { warmed, failed } = await this.pool.warmAll();
|
|
350
|
+
if (process.env.DM_DEBUG === "1" && warmed.length > 0) {
|
|
351
|
+
process.stderr.write(`[daemon] Pre-warmed ${warmed.length} server(s): ${warmed.join(", ")}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
242
354
|
resolve();
|
|
243
355
|
});
|
|
244
356
|
});
|
|
@@ -163,3 +163,35 @@ export async function startDaemon() {
|
|
|
163
163
|
} catch {}
|
|
164
164
|
throw new Error("Daemon failed to start within timeout");
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ensure daemon is running, starting it if necessary.
|
|
169
|
+
* @returns {Promise<boolean>} true if daemon is now running
|
|
170
|
+
*/
|
|
171
|
+
export async function ensureDaemonRunning() {
|
|
172
|
+
if (await isDaemonRunning()) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await startDaemon();
|
|
178
|
+
return true;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (process.env.DM_DEBUG === "1") {
|
|
181
|
+
process.stderr.write(`[dm] Failed to auto-start daemon: ${err.message}\n`);
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Request daemon to warm a specific server or all servers.
|
|
189
|
+
* @param {string} [serverName] - If provided, warm only this server. Otherwise warm all.
|
|
190
|
+
* @returns {Promise<object>}
|
|
191
|
+
*/
|
|
192
|
+
export async function warmServers(serverName) {
|
|
193
|
+
return sendRequest({
|
|
194
|
+
action: "warm",
|
|
195
|
+
server: serverName,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getDroidModeDataDir, ensureDir, readJsonFileIfExists, writeJson } from "./util.mjs";
|
|
4
|
+
|
|
5
|
+
const STATE_FILE = "daemon-state.json";
|
|
6
|
+
const STATE_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the path to the daemon state file.
|
|
10
|
+
*/
|
|
11
|
+
export function getStateFilePath() {
|
|
12
|
+
return path.join(getDroidModeDataDir(), STATE_FILE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load daemon state from disk.
|
|
17
|
+
* @returns {{ version: number, servers: Record<string, ServerState> }}
|
|
18
|
+
*/
|
|
19
|
+
export function loadState() {
|
|
20
|
+
const filePath = getStateFilePath();
|
|
21
|
+
const data = readJsonFileIfExists(filePath);
|
|
22
|
+
|
|
23
|
+
if (!data || data.version !== STATE_VERSION) {
|
|
24
|
+
return { version: STATE_VERSION, servers: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save daemon state to disk.
|
|
32
|
+
* @param {{ version: number, servers: Record<string, ServerState> }} state
|
|
33
|
+
*/
|
|
34
|
+
export function saveState(state) {
|
|
35
|
+
const filePath = getStateFilePath();
|
|
36
|
+
ensureDir(path.dirname(filePath));
|
|
37
|
+
writeJson(filePath, { ...state, version: STATE_VERSION });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get server state, creating default if not exists.
|
|
42
|
+
* @param {string} serverName
|
|
43
|
+
* @returns {{ autoWarm: boolean, lastUsed: string|null, usageCount: number }}
|
|
44
|
+
*/
|
|
45
|
+
export function getServerState(serverName) {
|
|
46
|
+
const state = loadState();
|
|
47
|
+
return state.servers[serverName] || {
|
|
48
|
+
autoWarm: true, // Default: opt-out (auto-warm enabled)
|
|
49
|
+
lastUsed: null,
|
|
50
|
+
usageCount: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update server state after use.
|
|
56
|
+
* @param {string} serverName
|
|
57
|
+
*/
|
|
58
|
+
export function recordServerUsage(serverName) {
|
|
59
|
+
const state = loadState();
|
|
60
|
+
const server = state.servers[serverName] || {
|
|
61
|
+
autoWarm: true,
|
|
62
|
+
lastUsed: null,
|
|
63
|
+
usageCount: 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
server.lastUsed = new Date().toISOString();
|
|
67
|
+
server.usageCount = (server.usageCount || 0) + 1;
|
|
68
|
+
state.servers[serverName] = server;
|
|
69
|
+
|
|
70
|
+
saveState(state);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set autoWarm for a server.
|
|
75
|
+
* @param {string} serverName
|
|
76
|
+
* @param {boolean} autoWarm
|
|
77
|
+
*/
|
|
78
|
+
export function setServerAutoWarm(serverName, autoWarm) {
|
|
79
|
+
const state = loadState();
|
|
80
|
+
const server = state.servers[serverName] || {
|
|
81
|
+
autoWarm: true,
|
|
82
|
+
lastUsed: null,
|
|
83
|
+
usageCount: 0,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
server.autoWarm = autoWarm;
|
|
87
|
+
state.servers[serverName] = server;
|
|
88
|
+
|
|
89
|
+
saveState(state);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get list of servers that should be auto-warmed.
|
|
94
|
+
* @param {string[]} availableServers - List of server names from mcp.json
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
export function getServersToWarm(availableServers) {
|
|
98
|
+
const state = loadState();
|
|
99
|
+
|
|
100
|
+
return availableServers.filter((name) => {
|
|
101
|
+
const serverState = state.servers[name];
|
|
102
|
+
// Default to true if no state exists (opt-out model)
|
|
103
|
+
return serverState?.autoWarm !== false;
|
|
104
|
+
});
|
|
105
|
+
}
|