wyrm-mcp 7.2.0 → 7.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
@@ -1,150 +1,4 @@
1
- /**
2
- * Maintenance ops — v7 F3 (T023).
3
- *
4
- * The `wyrm_maintenance` MCP case body (index.ts), extracted VERBATIM so the
5
- * new `wyrm maintenance` CLI subcommand and the MCP tool run the SAME code
6
- * path (spec FR-4 / T023: CLI commands wrap existing src modules — never
7
- * reimplementations). Behavior, knobs, and step order are unchanged:
8
- *
9
- * 1. archive old sessions (opt-in via archiveDays)
10
- * 2. VACUUM (opt-in)
11
- * 3. prune session_seen_artifacts (WYRM_SEEN_TTL_DAYS, default 7)
12
- * 4. prune Live Memory events (WYRM_EVENT_RETAIN_DAYS / _MAX_PER_PROJECT)
13
- * 5. prune failure_blocks (same retention knob as the event log)
14
- * 6. reap stale presence + expired quest claims (v7 F3 T029 — eviction
15
- * previously ran ONLY inline at claim time, so a fleet that stopped
16
- * claiming never shed its dead leases between waves)
17
- * 7. prune run_briefs (T028 fleet-brief cache; same retention knob as
18
- * the event log + a WYRM_RUN_BRIEFS_MAX count cap, default 512 —
19
- * security pass #1: the soft-ref cache is caller-mintable)
20
- * 8. run-quarantine sweep (WYRM_RUN_INACTIVE_HOURS, default 24)
21
- * 9. WAL checkpoint
22
- *
23
- * Every prune/sweep step is failure-isolated — maintenance never fails on a
24
- * single step (the 6.x contract).
25
- *
26
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
27
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
28
- */
29
- /**
30
- * Run the full maintenance sequence. Synchronous (every step is SQLite-local;
31
- * Article III: no network, no LLM). Returns the step report; rendering is the
32
- * caller's job (MCP case keeps its 6.x text verbatim, the CLI prints its own).
33
- */
34
- export function runMaintenance(deps, opts = {}) {
35
- const { db, sessionSeen, failures, presence } = deps;
36
- const { vacuum, archiveDays } = opts;
37
- const lines = [];
38
- if (archiveDays) {
39
- const projects = db.getAllProjects(1000);
40
- let archived = 0;
41
- for (const p of projects) {
42
- archived += db.archiveOldSessions(p.id, archiveDays);
43
- }
44
- lines.push(`Archived ${archived} old sessions`);
45
- }
46
- if (vacuum) {
47
- db.vacuum();
48
- lines.push(`Vacuumed database`);
49
- }
50
- // Spec 014: prune session_seen_artifacts older than WYRM_SEEN_TTL_DAYS (default 7).
51
- try {
52
- const ttlDays = parseInt(process.env.WYRM_SEEN_TTL_DAYS ?? '7', 10);
53
- const pruned = sessionSeen.prune(Number.isFinite(ttlDays) && ttlDays > 0 ? ttlDays : 7);
54
- if (pruned > 0)
55
- lines.push(`Pruned ${pruned} session_seen_artifacts rows older than ${ttlDays}d`);
56
- }
57
- catch { /* never fail maintenance on this */ }
58
- // Live Memory retention: bound the derived event log (default keep 90d /
59
- // 5000 per project; override via WYRM_EVENT_RETAIN_DAYS / WYRM_EVENT_MAX_PER_PROJECT).
60
- try {
61
- const retainDays = parseInt(process.env.WYRM_EVENT_RETAIN_DAYS ?? '90', 10);
62
- const maxPer = parseInt(process.env.WYRM_EVENT_MAX_PER_PROJECT ?? '5000', 10);
63
- const { deleted } = db.pruneEvents({
64
- olderThanDays: Number.isFinite(retainDays) ? retainDays : 90,
65
- maxPerProject: Number.isFinite(maxPer) ? maxPer : 5000,
66
- });
67
- if (deleted > 0)
68
- lines.push(`Pruned ${deleted} Live Memory events (retention)`);
69
- }
70
- catch { /* never fail maintenance on this */ }
71
- // v7 F2 review fix: failure_blocks retention. The prevented-repeat
72
- // ledger (migration 22) is append-only and caller-amplifiable (one
73
- // row per blocked failure_check — a read tool with no rate limit), so
74
- // bound it with the SAME retention knob as the event log.
75
- try {
76
- const retainDays = parseInt(process.env.WYRM_EVENT_RETAIN_DAYS ?? '90', 10);
77
- const effectiveDays = Number.isFinite(retainDays) && retainDays > 0 ? retainDays : 90;
78
- const prunedBlocks = failures.pruneBlocks(effectiveDays);
79
- if (prunedBlocks > 0)
80
- lines.push(`Pruned ${prunedBlocks} failure_blocks rows older than ${effectiveDays}d (retention)`);
81
- }
82
- catch { /* never fail maintenance on this */ }
83
- // v7 F3 (T029): stale-claim eviction. presence.reap() previously ran ONLY
84
- // inline at claimQuest/activeClaims time — an idle fleet (or one polling
85
- // status without claiming) never shed dead-session leases. Maintenance is
86
- // the async reclamation path orchestrators run between waves.
87
- try {
88
- const reaped = presence.reap();
89
- if (reaped.presence > 0 || reaped.claims > 0) {
90
- lines.push(`Reaped ${reaped.presence} stale presence row(s) and ${reaped.claims} expired quest claim(s)`);
91
- }
92
- }
93
- catch { /* never fail maintenance on this */ }
94
- // v7 F3 (T029): run_briefs retention — the T028 fleet-brief cache is one
95
- // row per (run_id, role) and a brief is only meaningful DURING its run, so
96
- // bound it with the SAME retention knob as the event log.
97
- try {
98
- const retainDays = parseInt(process.env.WYRM_EVENT_RETAIN_DAYS ?? '90', 10);
99
- const effectiveDays = Number.isFinite(retainDays) && retainDays > 0 ? retainDays : 90;
100
- const prunedBriefs = db.getDatabase()
101
- .prepare(`DELETE FROM run_briefs WHERE created_at < datetime('now', ?)`)
102
- .run(`-${effectiveDays} days`).changes;
103
- if (prunedBriefs > 0)
104
- lines.push(`Pruned ${prunedBriefs} run_briefs rows older than ${effectiveDays}d (retention)`);
105
- }
106
- catch { /* never fail maintenance on this */ }
107
- // Security pass #1 (confirmed finding): run_briefs had a DAYS bound only —
108
- // run_id is a deliberate soft reference (no FK, no existence check), so any
109
- // caller could mint unbounded multi-KB rows that lived the full 90 days.
110
- // Pair the days knob with a COUNT cap (the WYRM_EVENT_MAX_PER_PROJECT
111
- // pattern): keep the newest WYRM_RUN_BRIEFS_MAX rows (default 512 — a
112
- // 12-role fleet still retains ~40 runs of briefs). `wyrm_run action=end`
113
- // also now evicts its run's briefs eagerly; this cap covers runs that
114
- // never end cleanly.
115
- try {
116
- const maxBriefs = parseInt(process.env.WYRM_RUN_BRIEFS_MAX ?? '512', 10);
117
- const cap = Number.isFinite(maxBriefs) && maxBriefs > 0 ? maxBriefs : 512;
118
- const capped = db.getDatabase().prepare(`
1
+ function _(c,o={}){const{db:t,sessionSeen:u,failures:a,presence:p}=c,{vacuum:d,archiveDays:i}=o,n=[];if(i){const e=t.getAllProjects(1e3);let s=0;for(const r of e)s+=t.archiveOldSessions(r.id,i);n.push(`Archived ${s} old sessions`)}d&&(t.vacuum(),n.push("Vacuumed database"));try{const e=parseInt(process.env.WYRM_SEEN_TTL_DAYS??"7",10),s=u.prune(Number.isFinite(e)&&e>0?e:7);s>0&&n.push(`Pruned ${s} session_seen_artifacts rows older than ${e}d`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=parseInt(process.env.WYRM_EVENT_MAX_PER_PROJECT??"5000",10),{deleted:r}=t.pruneEvents({olderThanDays:Number.isFinite(e)?e:90,maxPerProject:Number.isFinite(s)?s:5e3});r>0&&n.push(`Pruned ${r} Live Memory events (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=Number.isFinite(e)&&e>0?e:90,r=a.pruneBlocks(s);r>0&&n.push(`Pruned ${r} failure_blocks rows older than ${s}d (retention)`)}catch{}try{const e=p.reap();(e.presence>0||e.claims>0)&&n.push(`Reaped ${e.presence} stale presence row(s) and ${e.claims} expired quest claim(s)`)}catch{}try{const e=parseInt(process.env.WYRM_EVENT_RETAIN_DAYS??"90",10),s=Number.isFinite(e)&&e>0?e:90,r=t.getDatabase().prepare("DELETE FROM run_briefs WHERE created_at < datetime('now', ?)").run(`-${s} days`).changes;r>0&&n.push(`Pruned ${r} run_briefs rows older than ${s}d (retention)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_BRIEFS_MAX??"512",10),s=Number.isFinite(e)&&e>0?e:512,r=t.getDatabase().prepare(`
119
2
  DELETE FROM run_briefs WHERE rowid IN (
120
3
  SELECT rowid FROM run_briefs ORDER BY created_at DESC, rowid DESC LIMIT -1 OFFSET ?
121
- )`).run(cap).changes;
122
- if (capped > 0)
123
- lines.push(`Pruned ${capped} run_briefs rows over the ${cap}-row cap (WYRM_RUN_BRIEFS_MAX)`);
124
- }
125
- catch { /* never fail maintenance on this */ }
126
- // v7 F2 (T015): run-quarantine sweep — promote unresolved run-scoped
127
- // failures from runs that ended or went inactive past the TTL
128
- // (default 24h; override WYRM_RUN_INACTIVE_HOURS) and expire
129
- // abandoned-run noise. Debrief-independent: works with zero runs rows
130
- // (wyrm_run arrives in F3).
131
- try {
132
- const ttlHours = parseInt(process.env.WYRM_RUN_INACTIVE_HOURS ?? '24', 10);
133
- const sweep = failures.sweepRunQuarantine({
134
- runInactiveHours: Number.isFinite(ttlHours) && ttlHours > 0 ? ttlHours : 24,
135
- });
136
- if (sweep.promoted > 0 || sweep.expired > 0 || sweep.runs_abandoned > 0) {
137
- lines.push(`Run-quarantine sweep: promoted ${sweep.promoted} failure(s), expired ${sweep.expired} abandoned-run failure(s), TTL-abandoned ${sweep.runs_abandoned} stalled run(s)`);
138
- }
139
- // A degraded sweep (live memory off, TTL paths skipped) must say so
140
- // even when nothing acted — silence here reads as "all promoted".
141
- if (sweep.degraded) {
142
- lines.push(`Run-quarantine sweep: ${sweep.degraded}`);
143
- }
144
- }
145
- catch { /* never fail maintenance on this */ }
146
- db.checkpoint();
147
- lines.push(`Checkpointed WAL`);
148
- return { lines, dbSize: db.getStats().dbSize };
149
- }
150
- //# sourceMappingURL=maintenance.js.map
4
+ )`).run(s).changes;r>0&&n.push(`Pruned ${r} run_briefs rows over the ${s}-row cap (WYRM_RUN_BRIEFS_MAX)`)}catch{}try{const e=parseInt(process.env.WYRM_RUN_INACTIVE_HOURS??"24",10),s=a.sweepRunQuarantine({runInactiveHours:Number.isFinite(e)&&e>0?e:24});(s.promoted>0||s.expired>0||s.runs_abandoned>0)&&n.push(`Run-quarantine sweep: promoted ${s.promoted} failure(s), expired ${s.expired} abandoned-run failure(s), TTL-abandoned ${s.runs_abandoned} stalled run(s)`),s.degraded&&n.push(`Run-quarantine sweep: ${s.degraded}`)}catch{}return t.checkpoint(),n.push("Checkpointed WAL"),{lines:n,dbSize:t.getStats().dbSize}}export{_ as runMaintenance};
@@ -1,41 +1,4 @@
1
- /**
2
- * Outbound MCP client — Wyrm calls other MCP servers.
3
- *
4
- * Wyrm has historically been a server. v5.0 adds the dual: Wyrm can also
5
- * spawn other MCP servers (over stdio) and invoke their tools. This is
6
- * what turns `wyrm_act` from "synthesize an answer" into "actually do
7
- * things" — call `github_create_pr`, `slack_send_message`, etc.
8
- *
9
- * Connections are lazy: a server is spawned on first call to its tools,
10
- * kept alive for `idleTimeoutMs`, then reaped. Spawns are bounded to
11
- * `maxClients` total to prevent fork-bombing.
12
- *
13
- * Config table: `mcp_client_configs`. Add a server via `register()`,
14
- * disable without removing via `disable()`.
15
- *
16
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
17
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
18
- */
19
- import { spawn } from 'child_process';
20
- const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
21
- const DEFAULT_MAX_CLIENTS = 10;
22
- const CALL_TIMEOUT_MS = 30_000;
23
- export class OutboundMcpClient {
24
- db;
25
- opts;
26
- clients = new Map();
27
- maintenance;
28
- constructor(db, opts = {}) {
29
- this.db = db;
30
- this.opts = opts;
31
- // Periodic reap of idle clients
32
- this.maintenance = setInterval(() => this.reapIdle(), 30_000);
33
- if (this.maintenance.unref)
34
- this.maintenance.unref();
35
- }
36
- /** Register or update an MCP server config row. */
37
- register(input) {
38
- this.db.prepare(`
1
+ import{spawn as h}from"child_process";const m=300*1e3,g=10,f=3e4;class _{db;opts;clients=new Map;maintenance;constructor(e,t={}){this.db=e,this.opts=t,this.maintenance=setInterval(()=>this.reapIdle(),3e4),this.maintenance.unref&&this.maintenance.unref()}register(e){return this.db.prepare(`
39
2
  INSERT INTO mcp_client_configs (server_name, command, args, env)
40
3
  VALUES (?, ?, ?, ?)
41
4
  ON CONFLICT(server_name) DO UPDATE SET
@@ -43,234 +6,15 @@ export class OutboundMcpClient {
43
6
  args = excluded.args,
44
7
  env = excluded.env,
45
8
  enabled = 1
46
- `).run(input.server_name, input.command, input.args ? JSON.stringify(input.args) : null, input.env ? JSON.stringify(input.env) : null);
47
- return this.getConfig(input.server_name);
48
- }
49
- getConfig(server_name) {
50
- return this.db.prepare('SELECT * FROM mcp_client_configs WHERE server_name = ?').get(server_name) ?? null;
51
- }
52
- list() {
53
- return this.db.prepare('SELECT * FROM mcp_client_configs ORDER BY server_name').all();
54
- }
55
- disable(server_name) {
56
- const info = this.db.prepare('UPDATE mcp_client_configs SET enabled = 0 WHERE server_name = ?').run(server_name);
57
- this.closeClient(server_name);
58
- return info.changes > 0;
59
- }
60
- delete(server_name) {
61
- const info = this.db.prepare('DELETE FROM mcp_client_configs WHERE server_name = ?').run(server_name);
62
- this.closeClient(server_name);
63
- return info.changes > 0;
64
- }
65
- /** Call a tool on a registered server. Lazily spawns + initializes. */
66
- async call(server_name, tool_name, args) {
67
- const start = Date.now();
68
- const config = this.getConfig(server_name);
69
- if (!config) {
70
- const r = {
71
- ok: false, server_name, tool_name,
72
- error: `Server '${server_name}' not registered. Use wyrm_mcp_register first.`,
73
- latency_ms: Date.now() - start,
74
- };
75
- this.logCall(r);
76
- return r;
77
- }
78
- if (!config.enabled) {
79
- const r = {
80
- ok: false, server_name, tool_name,
81
- error: `Server '${server_name}' is disabled.`,
82
- latency_ms: Date.now() - start,
83
- };
84
- this.logCall(r);
85
- return r;
86
- }
87
- try {
88
- const client = await this.ensureClient(server_name, config);
89
- const result = await this.rpc(client, 'tools/call', { name: tool_name, arguments: args });
90
- // Update usage stats
91
- this.db.prepare(`
9
+ `).run(e.server_name,e.command,e.args?JSON.stringify(e.args):null,e.env?JSON.stringify(e.env):null),this.getConfig(e.server_name)}getConfig(e){return this.db.prepare("SELECT * FROM mcp_client_configs WHERE server_name = ?").get(e)??null}list(){return this.db.prepare("SELECT * FROM mcp_client_configs ORDER BY server_name").all()}disable(e){const t=this.db.prepare("UPDATE mcp_client_configs SET enabled = 0 WHERE server_name = ?").run(e);return this.closeClient(e),t.changes>0}delete(e){const t=this.db.prepare("DELETE FROM mcp_client_configs WHERE server_name = ?").run(e);return this.closeClient(e),t.changes>0}async call(e,t,s){const i=Date.now(),r=this.getConfig(e);if(!r){const n={ok:!1,server_name:e,tool_name:t,error:`Server '${e}' not registered. Use wyrm_mcp_register first.`,latency_ms:Date.now()-i};return this.logCall(n),n}if(!r.enabled){const n={ok:!1,server_name:e,tool_name:t,error:`Server '${e}' is disabled.`,latency_ms:Date.now()-i};return this.logCall(n),n}try{const n=await this.ensureClient(e,r),o=await this.rpc(n,"tools/call",{name:t,arguments:s});this.db.prepare(`
92
10
  UPDATE mcp_client_configs
93
11
  SET last_used_at = datetime('now'),
94
12
  use_count = use_count + 1
95
13
  WHERE server_name = ?
96
- `).run(server_name);
97
- const r = {
98
- ok: true, server_name, tool_name, result,
99
- latency_ms: Date.now() - start,
100
- };
101
- this.logCall(r, args);
102
- return r;
103
- }
104
- catch (err) {
105
- const r = {
106
- ok: false, server_name, tool_name,
107
- error: err.message,
108
- latency_ms: Date.now() - start,
109
- };
110
- this.logCall(r, args);
111
- return r;
112
- }
113
- }
114
- /** List the tools a registered server exposes (one-shot tools/list call). */
115
- async listTools(server_name) {
116
- const config = this.getConfig(server_name);
117
- if (!config)
118
- return null;
119
- try {
120
- const client = await this.ensureClient(server_name, config);
121
- const result = await this.rpc(client, 'tools/list', {});
122
- return result.tools ?? [];
123
- }
124
- catch {
125
- return null;
126
- }
127
- }
128
- /** Cleanly tear down all clients (call on process exit). */
129
- async shutdown() {
130
- if (this.maintenance)
131
- clearInterval(this.maintenance);
132
- for (const name of Array.from(this.clients.keys())) {
133
- this.closeClient(name);
134
- }
135
- }
136
- // ============================ internals ============================
137
- logCall(r, args) {
138
- try {
139
- this.db.prepare(`
14
+ `).run(e);const l={ok:!0,server_name:e,tool_name:t,result:o,latency_ms:Date.now()-i};return this.logCall(l,s),l}catch(n){const o={ok:!1,server_name:e,tool_name:t,error:n.message,latency_ms:Date.now()-i};return this.logCall(o,s),o}}async listTools(e){const t=this.getConfig(e);if(!t)return null;try{const s=await this.ensureClient(e,t);return(await this.rpc(s,"tools/list",{})).tools??[]}catch{return null}}async shutdown(){this.maintenance&&clearInterval(this.maintenance);for(const e of Array.from(this.clients.keys()))this.closeClient(e)}logCall(e,t){try{this.db.prepare(`
140
15
  INSERT INTO external_call_log
141
16
  (server_name, tool_name, args_summary, success, error_message, latency_ms)
142
17
  VALUES (?, ?, ?, ?, ?, ?)
143
- `).run(r.server_name, r.tool_name, args ? JSON.stringify(args).slice(0, 500) : null, r.ok ? 1 : 0, r.error ?? null, r.latency_ms);
144
- }
145
- catch { /* logging never fails the call path */ }
146
- }
147
- reapIdle() {
148
- const idleCap = this.opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
149
- const now = Date.now();
150
- for (const [name, client] of this.clients) {
151
- if (now - client.lastUsedAt > idleCap)
152
- this.closeClient(name);
153
- }
154
- }
155
- closeClient(name) {
156
- const client = this.clients.get(name);
157
- if (!client)
158
- return;
159
- for (const pending of client.pendingByReqId.values()) {
160
- clearTimeout(pending.timer);
161
- pending.reject(new Error('Client closing'));
162
- }
163
- try {
164
- client.proc.kill('SIGTERM');
165
- }
166
- catch { /* best-effort */ }
167
- this.clients.delete(name);
168
- }
169
- async ensureClient(name, config) {
170
- const existing = this.clients.get(name);
171
- if (existing && !existing.proc.killed) {
172
- existing.lastUsedAt = Date.now();
173
- return existing;
174
- }
175
- // Enforce maxClients cap
176
- const cap = this.opts.maxClients ?? DEFAULT_MAX_CLIENTS;
177
- if (this.clients.size >= cap) {
178
- // Evict the least-recently-used
179
- const lru = Array.from(this.clients.entries())
180
- .sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
181
- if (lru)
182
- this.closeClient(lru[0]);
183
- }
184
- const args = config.args ? JSON.parse(config.args) : [];
185
- const env = config.env ? JSON.parse(config.env) : {};
186
- // SECURITY: do NOT forward Wyrm's entire process environment to a spawned
187
- // external MCP server — that leaks every secret in the parent env
188
- // (ANTHROPIC_API_KEY, WYRM_* tokens, OPENAI_API_KEY, …) to a third-party
189
- // binary. Pass only a minimal, non-secret base env; the per-server `env`
190
- // from its registered config carries anything that server explicitly needs.
191
- const ENV_ALLOWLIST = [
192
- 'PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'LC_CTYPE',
193
- 'TERM', 'TZ', 'SHELL', 'USER', 'LOGNAME', 'NODE_PATH',
194
- 'SystemRoot', 'WINDIR', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA',
195
- 'PATHEXT', 'COMSPEC', 'PROGRAMFILES', 'PROGRAMDATA',
196
- ];
197
- const baseEnv = {};
198
- for (const k of ENV_ALLOWLIST) {
199
- const v = process.env[k];
200
- if (v !== undefined)
201
- baseEnv[k] = v;
202
- }
203
- const proc = spawn(config.command, args, {
204
- env: { ...baseEnv, ...env },
205
- stdio: ['pipe', 'pipe', 'pipe'],
206
- });
207
- const client = {
208
- proc,
209
- pendingByReqId: new Map(),
210
- buffer: '',
211
- nextReqId: 1,
212
- lastUsedAt: Date.now(),
213
- initialized: false,
214
- };
215
- proc.stdout.setEncoding('utf-8');
216
- proc.stdout.on('data', (chunk) => this.handleStdout(client, chunk));
217
- proc.on('error', () => this.closeClient(name));
218
- proc.on('exit', () => this.closeClient(name));
219
- this.clients.set(name, client);
220
- // MCP handshake: send initialize, wait for response
221
- await this.rpc(client, 'initialize', {
222
- protocolVersion: '2024-11-05',
223
- capabilities: {},
224
- clientInfo: { name: 'wyrm-mcp', version: '5.0.0' },
225
- });
226
- // Some servers expect notifications/initialized after handshake
227
- this.send(client, { jsonrpc: '2.0', method: 'notifications/initialized' });
228
- client.initialized = true;
229
- return client;
230
- }
231
- handleStdout(client, chunk) {
232
- client.buffer += chunk;
233
- // MCP uses line-delimited JSON-RPC
234
- let idx;
235
- while ((idx = client.buffer.indexOf('\n')) >= 0) {
236
- const line = client.buffer.slice(0, idx).trim();
237
- client.buffer = client.buffer.slice(idx + 1);
238
- if (!line)
239
- continue;
240
- try {
241
- const msg = JSON.parse(line);
242
- if (msg.id != null) {
243
- const pending = client.pendingByReqId.get(msg.id);
244
- if (pending) {
245
- clearTimeout(pending.timer);
246
- client.pendingByReqId.delete(msg.id);
247
- if (msg.error)
248
- pending.reject(new Error(msg.error.message));
249
- else
250
- pending.resolve(msg.result);
251
- }
252
- }
253
- // Notifications (no id) — ignore for now
254
- }
255
- catch { /* malformed line, skip */ }
256
- }
257
- }
258
- send(client, payload) {
259
- if (!client.proc.stdin || client.proc.stdin.destroyed)
260
- return;
261
- client.proc.stdin.write(JSON.stringify(payload) + '\n');
262
- }
263
- rpc(client, method, params) {
264
- const id = client.nextReqId++;
265
- const payload = { jsonrpc: '2.0', id, method, params };
266
- return new Promise((resolve, reject) => {
267
- const timer = setTimeout(() => {
268
- client.pendingByReqId.delete(id);
269
- reject(new Error(`MCP call timed out after ${CALL_TIMEOUT_MS}ms`));
270
- }, CALL_TIMEOUT_MS);
271
- client.pendingByReqId.set(id, { resolve, reject, timer });
272
- this.send(client, payload);
273
- });
274
- }
275
- }
276
- //# sourceMappingURL=mcp-client.js.map
18
+ `).run(e.server_name,e.tool_name,t?JSON.stringify(t).slice(0,500):null,e.ok?1:0,e.error??null,e.latency_ms)}catch{}}reapIdle(){const e=this.opts.idleTimeoutMs??m,t=Date.now();for(const[s,i]of this.clients)t-i.lastUsedAt>e&&this.closeClient(s)}closeClient(e){const t=this.clients.get(e);if(t){for(const s of t.pendingByReqId.values())clearTimeout(s.timer),s.reject(new Error("Client closing"));try{t.proc.kill("SIGTERM")}catch{}this.clients.delete(e)}}async ensureClient(e,t){const s=this.clients.get(e);if(s&&!s.proc.killed)return s.lastUsedAt=Date.now(),s;const i=this.opts.maxClients??g;if(this.clients.size>=i){const c=Array.from(this.clients.entries()).sort(([,u],[,p])=>u.lastUsedAt-p.lastUsedAt)[0];c&&this.closeClient(c[0])}const r=t.args?JSON.parse(t.args):[],n=t.env?JSON.parse(t.env):{},o=["PATH","HOME","TMPDIR","TEMP","TMP","LANG","LC_ALL","LC_CTYPE","TERM","TZ","SHELL","USER","LOGNAME","NODE_PATH","SystemRoot","WINDIR","USERPROFILE","APPDATA","LOCALAPPDATA","PATHEXT","COMSPEC","PROGRAMFILES","PROGRAMDATA"],l={};for(const c of o){const u=process.env[c];u!==void 0&&(l[c]=u)}const d=h(t.command,r,{env:{...l,...n},stdio:["pipe","pipe","pipe"]}),a={proc:d,pendingByReqId:new Map,buffer:"",nextReqId:1,lastUsedAt:Date.now(),initialized:!1};return d.stdout.setEncoding("utf-8"),d.stdout.on("data",c=>this.handleStdout(a,c)),d.on("error",()=>this.closeClient(e)),d.on("exit",()=>this.closeClient(e)),this.clients.set(e,a),await this.rpc(a,"initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"wyrm-mcp",version:"5.0.0"}}),this.send(a,{jsonrpc:"2.0",method:"notifications/initialized"}),a.initialized=!0,a}handleStdout(e,t){e.buffer+=t;let s;for(;(s=e.buffer.indexOf(`
19
+ `))>=0;){const i=e.buffer.slice(0,s).trim();if(e.buffer=e.buffer.slice(s+1),!!i)try{const r=JSON.parse(i);if(r.id!=null){const n=e.pendingByReqId.get(r.id);n&&(clearTimeout(n.timer),e.pendingByReqId.delete(r.id),r.error?n.reject(new Error(r.error.message)):n.resolve(r.result))}}catch{}}}send(e,t){!e.proc.stdin||e.proc.stdin.destroyed||e.proc.stdin.write(JSON.stringify(t)+`
20
+ `)}rpc(e,t,s){const i=e.nextReqId++,r={jsonrpc:"2.0",id:i,method:t,params:s};return new Promise((n,o)=>{const l=setTimeout(()=>{e.pendingByReqId.delete(i),o(new Error(`MCP call timed out after ${f}ms`))},f);e.pendingByReqId.set(i,{resolve:n,reject:o,timer:l}),this.send(e,r)})}}export{_ as OutboundMcpClient};