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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/maintenance.js
CHANGED
|
@@ -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(
|
|
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};
|
package/dist/mcp-client.js
CHANGED
|
@@ -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(
|
|
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(
|
|
144
|
-
|
|
145
|
-
|
|
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};
|