wyrm-mcp 7.2.1 → 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 (150) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.js +1 -60
  4. package/dist/agent-daemon.js +4 -281
  5. package/dist/agent-loop.js +7 -332
  6. package/dist/analytics.js +13 -236
  7. package/dist/attribution.js +1 -49
  8. package/dist/audit.js +2 -457
  9. package/dist/auto-capture.js +3 -138
  10. package/dist/auto-orchestrator.js +1 -325
  11. package/dist/autoconfig.js +39 -840
  12. package/dist/buddy-runner.js +1 -109
  13. package/dist/buddy.js +14 -564
  14. package/dist/build-flags.js +1 -17
  15. package/dist/capabilities.js +3 -183
  16. package/dist/capture.js +1 -56
  17. package/dist/causality.js +6 -107
  18. package/dist/cli.js +20 -281
  19. package/dist/cloud/cli.js +5 -541
  20. package/dist/cloud/client.js +1 -221
  21. package/dist/cloud/crypto.js +1 -85
  22. package/dist/cloud/machine-id.js +2 -113
  23. package/dist/cloud/recovery.js +1 -60
  24. package/dist/cloud/sync-engine.js +7 -543
  25. package/dist/cloud-backup.js +5 -579
  26. package/dist/cloud-profile.js +1 -138
  27. package/dist/cloud-sync-entrypoint.js +1 -47
  28. package/dist/cloud-sync.js +2 -309
  29. package/dist/constellation.js +12 -168
  30. package/dist/context-build-budgeted.js +4 -144
  31. package/dist/context-ranking.js +1 -69
  32. package/dist/crypto.js +1 -179
  33. package/dist/daemon-write-endpoint.js +1 -290
  34. package/dist/daemon-writer.js +2 -406
  35. package/dist/database.js +43 -1110
  36. package/dist/deprecations.js +2 -162
  37. package/dist/design.js +13 -141
  38. package/dist/event-replication.js +1 -112
  39. package/dist/events-sse.js +7 -43
  40. package/dist/events.js +6 -238
  41. package/dist/failure-patterns.js +42 -659
  42. package/dist/federation.js +12 -236
  43. package/dist/goals.js +13 -101
  44. package/dist/golden.js +3 -355
  45. package/dist/handlers/agent.js +4 -165
  46. package/dist/handlers/alias-adapters.js +1 -129
  47. package/dist/handlers/aliases.js +1 -171
  48. package/dist/handlers/audit.js +1 -87
  49. package/dist/handlers/boundary.js +1 -221
  50. package/dist/handlers/capture.js +73 -1109
  51. package/dist/handlers/causality.js +7 -114
  52. package/dist/handlers/cloud.js +85 -382
  53. package/dist/handlers/companion.js +28 -459
  54. package/dist/handlers/datalake.js +7 -187
  55. package/dist/handlers/dispatch-context.js +0 -22
  56. package/dist/handlers/entity.js +25 -256
  57. package/dist/handlers/events.js +16 -335
  58. package/dist/handlers/failure.js +13 -340
  59. package/dist/handlers/goals.js +4 -296
  60. package/dist/handlers/intelligence.js +126 -674
  61. package/dist/handlers/invoicing.js +1 -70
  62. package/dist/handlers/mcpclient.js +6 -137
  63. package/dist/handlers/orchestration.js +40 -125
  64. package/dist/handlers/output-schemas.js +1 -24
  65. package/dist/handlers/presence.js +3 -99
  66. package/dist/handlers/project.js +28 -182
  67. package/dist/handlers/prompts.js +6 -157
  68. package/dist/handlers/quest.js +4 -224
  69. package/dist/handlers/recall.js +11 -218
  70. package/dist/handlers/registry.js +1 -167
  71. package/dist/handlers/resources.js +1 -288
  72. package/dist/handlers/review.js +11 -74
  73. package/dist/handlers/run.js +17 -487
  74. package/dist/handlers/search.js +15 -326
  75. package/dist/handlers/session.js +28 -615
  76. package/dist/handlers/share.js +8 -184
  77. package/dist/handlers/shims.js +1 -464
  78. package/dist/handlers/skill.js +67 -449
  79. package/dist/handlers/survivors.js +1 -120
  80. package/dist/handlers/symbols.js +8 -109
  81. package/dist/handlers/syncops.js +4 -302
  82. package/dist/handlers/types.js +1 -27
  83. package/dist/harvest.js +5 -191
  84. package/dist/hours.js +7 -156
  85. package/dist/http-auth.js +3 -321
  86. package/dist/http-fast.js +21 -1137
  87. package/dist/icons.js +1 -47
  88. package/dist/index.js +2 -924
  89. package/dist/indexer.js +4 -145
  90. package/dist/intelligence.js +31 -261
  91. package/dist/internal-dispatch.js +3 -212
  92. package/dist/keyset.js +1 -110
  93. package/dist/knowledge-graph.js +12 -176
  94. package/dist/license.js +2 -441
  95. package/dist/logger.js +2 -199
  96. package/dist/maintenance.js +2 -148
  97. package/dist/mcp-client.js +6 -262
  98. package/dist/memory-artifacts.js +30 -449
  99. package/dist/migrate-prompt.js +2 -124
  100. package/dist/migrations.js +40 -655
  101. package/dist/performance.js +1 -228
  102. package/dist/presence.js +11 -140
  103. package/dist/priority-embed.js +5 -164
  104. package/dist/providers/embedding-provider.js +1 -196
  105. package/dist/readonly-gate.js +1 -29
  106. package/dist/rehydration.js +9 -157
  107. package/dist/reindex.js +1 -88
  108. package/dist/render-target.js +21 -514
  109. package/dist/render.js +4 -280
  110. package/dist/repl-guard.js +1 -173
  111. package/dist/replication-daemon-entrypoint.js +1 -31
  112. package/dist/replication-daemon.js +2 -262
  113. package/dist/resilience.js +1 -591
  114. package/dist/reverse-bridge.js +5 -360
  115. package/dist/security.js +1 -244
  116. package/dist/session-seen.js +3 -51
  117. package/dist/setup.js +1 -260
  118. package/dist/skill-author.js +5 -168
  119. package/dist/spec-kit.js +1 -191
  120. package/dist/sqlite-busy.js +1 -154
  121. package/dist/statusline.js +11 -315
  122. package/dist/sub-agent.js +13 -262
  123. package/dist/summarizer.js +13 -139
  124. package/dist/symbols.js +7 -283
  125. package/dist/sync.js +5 -359
  126. package/dist/tasks-dispatch.js +1 -84
  127. package/dist/tasks.js +1 -282
  128. package/dist/token-budget.js +1 -143
  129. package/dist/tool-analytics.js +7 -129
  130. package/dist/tool-annotations.js +1 -365
  131. package/dist/tool-manifest-v2.json +1 -1
  132. package/dist/tool-manifest.json +1 -1
  133. package/dist/tool-profiles.js +1 -75
  134. package/dist/trace-harvest.js +6 -244
  135. package/dist/types.js +1 -30
  136. package/dist/ui-dashboard.js +41 -50
  137. package/dist/ulid.js +1 -81
  138. package/dist/validate.js +1 -129
  139. package/dist/vault.js +1 -534
  140. package/dist/vectors.js +3 -184
  141. package/dist/version-check.js +4 -136
  142. package/dist/visibility.js +19 -155
  143. package/dist/wyrm-cli.js +98 -2464
  144. package/dist/wyrm-guard.js +14 -424
  145. package/dist/wyrm-loop.js +3 -150
  146. package/dist/wyrm-manifest.json +1 -1
  147. package/dist/wyrm-statusline-daemon.js +1 -11
  148. package/dist/wyrm-statusline.js +4 -56
  149. package/dist/wyrm-ui.js +9 -77
  150. 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};