@nforma.ai/nforma 0.2.1
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 +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// unified-mcp-server.mjs — config-driven MCP stdio server
|
|
3
|
+
// Implements raw JSON-RPC stdio (no SDK dependency)
|
|
4
|
+
// Wraps multiple CLI providers as MCP tools, driven by providers.json
|
|
5
|
+
//
|
|
6
|
+
// Modes:
|
|
7
|
+
// default — exposes all providers as slot-named tools (unified-1 usage)
|
|
8
|
+
// PROVIDER_SLOT=X — exposes only provider X with its original tool names
|
|
9
|
+
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { createInterface } from 'readline';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import https from 'https';
|
|
16
|
+
import http from 'http';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
// ─── Load providers config ─────────────────────────────────────────────────────
|
|
22
|
+
const configPath = process.env.UNIFIED_PROVIDERS_CONFIG
|
|
23
|
+
?? join(__dirname, 'providers.json');
|
|
24
|
+
let providers;
|
|
25
|
+
try {
|
|
26
|
+
providers = JSON.parse(fs.readFileSync(configPath, 'utf8')).providers;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
process.stderr.write(`[unified-mcp-server] Failed to load config: ${e.message}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Guard: handle empty providers array gracefully
|
|
33
|
+
if (!Array.isArray(providers) || providers.length === 0) {
|
|
34
|
+
process.stderr.write('[unified-mcp-server] WARNING: No providers configured in providers.json — server will start with zero tools\n');
|
|
35
|
+
providers = providers || [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── PROVIDER_SLOT mode detection ─────────────────────────────────────────────
|
|
39
|
+
const SLOT = process.env.PROVIDER_SLOT ?? null;
|
|
40
|
+
const slotProvider = SLOT ? providers.find(p => p.name === SLOT) : null;
|
|
41
|
+
|
|
42
|
+
if (SLOT && !slotProvider) {
|
|
43
|
+
process.stderr.write(`[unified-mcp-server] Unknown PROVIDER_SLOT: ${SLOT}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── MCP response helpers ──────────────────────────────────────────────────────
|
|
48
|
+
function send(obj) {
|
|
49
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sendResult(id, result) {
|
|
53
|
+
send({ jsonrpc: '2.0', id, result });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sendError(id, code, message) {
|
|
57
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Tool definitions ──────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/** All-providers mode: one tool per provider using provider name as tool name */
|
|
63
|
+
function buildAllProviderTools() {
|
|
64
|
+
return providers.map(p => ({
|
|
65
|
+
name: p.name,
|
|
66
|
+
description: p.description,
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
prompt: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'The prompt or task to send to the provider CLI',
|
|
73
|
+
},
|
|
74
|
+
timeout_ms: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
description: `Timeout in milliseconds (default: ${p.timeout_ms ?? 300000})`,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['prompt'],
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const PROMPT_SCHEMA = {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
prompt: { type: 'string', description: 'The prompt or task to send' },
|
|
88
|
+
timeout_ms: { type: 'number', description: 'Timeout in milliseconds' },
|
|
89
|
+
},
|
|
90
|
+
required: ['prompt'],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const NO_ARGS_SCHEMA = { type: 'object', properties: {}, required: [] };
|
|
94
|
+
|
|
95
|
+
/** PROVIDER_SLOT mode: expose original tool names for the given provider */
|
|
96
|
+
function buildSlotTools(provider) {
|
|
97
|
+
const tools = [];
|
|
98
|
+
|
|
99
|
+
// Universal: ping
|
|
100
|
+
tools.push({
|
|
101
|
+
name: 'ping',
|
|
102
|
+
description: 'Test connectivity by echoing a message back.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: { prompt: { type: 'string', default: '', description: 'Message to echo' } },
|
|
106
|
+
required: [],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Universal: identity
|
|
111
|
+
tools.push({
|
|
112
|
+
name: 'identity',
|
|
113
|
+
description: 'Get server identity: name, version, active LLM model, and MCP server name. Used by QGSD to fingerprint the active quorum team.',
|
|
114
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (provider.type === 'subprocess') {
|
|
118
|
+
// Main tool
|
|
119
|
+
tools.push({
|
|
120
|
+
name: provider.mainTool,
|
|
121
|
+
description: provider.description,
|
|
122
|
+
inputSchema: PROMPT_SCHEMA,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Help tool
|
|
126
|
+
tools.push({
|
|
127
|
+
name: 'help',
|
|
128
|
+
description: `Display ${provider.mainTool} CLI help and available options.`,
|
|
129
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Extra tools
|
|
133
|
+
for (const extra of provider.extraTools ?? []) {
|
|
134
|
+
tools.push({
|
|
135
|
+
name: extra.name,
|
|
136
|
+
description: extra.description,
|
|
137
|
+
inputSchema: extra.checkUpdate ? NO_ARGS_SCHEMA : PROMPT_SCHEMA,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// health_check tool for subprocess providers
|
|
142
|
+
if (provider.health_check_args) {
|
|
143
|
+
tools.push({
|
|
144
|
+
name: 'health_check',
|
|
145
|
+
description: 'Test CLI availability by running a lightweight command (e.g. --version). Returns { healthy, latencyMs, type: "subprocess" }.',
|
|
146
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} else if (provider.type === 'ccr') {
|
|
150
|
+
// CCR (Claude Code Router) — proxy binary, exposes same interface as subprocess
|
|
151
|
+
tools.push({
|
|
152
|
+
name: 'ask',
|
|
153
|
+
description: provider.description,
|
|
154
|
+
inputSchema: PROMPT_SCHEMA,
|
|
155
|
+
});
|
|
156
|
+
tools.push({
|
|
157
|
+
name: 'help',
|
|
158
|
+
description: 'Display CCR CLI help and available options.',
|
|
159
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
160
|
+
});
|
|
161
|
+
if (provider.health_check_args) {
|
|
162
|
+
tools.push({
|
|
163
|
+
name: 'health_check',
|
|
164
|
+
description: 'Test CCR binary availability. Returns { healthy, latencyMs, type: "ccr" }.',
|
|
165
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (provider.type === 'http') {
|
|
169
|
+
// claude tool
|
|
170
|
+
tools.push({
|
|
171
|
+
name: 'claude',
|
|
172
|
+
description: 'Execute Claude Code CLI in non-interactive mode for AI assistance',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
prompt: { type: 'string', description: 'The coding task, question, or analysis request' },
|
|
177
|
+
timeout_ms: { type: 'number', description: 'Timeout in milliseconds' },
|
|
178
|
+
},
|
|
179
|
+
required: ['prompt'],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// health_check tool
|
|
184
|
+
tools.push({
|
|
185
|
+
name: 'health_check',
|
|
186
|
+
description: 'Verify the upstream LLM endpoint is reachable by making a minimal API call. Returns { healthy, latencyMs, model } or { healthy: false, error }.',
|
|
187
|
+
inputSchema: NO_ARGS_SCHEMA,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return tools;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildTools() {
|
|
195
|
+
if (slotProvider) return buildSlotTools(slotProvider);
|
|
196
|
+
return buildAllProviderTools();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Subprocess execution ──────────────────────────────────────────────────────
|
|
200
|
+
const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
|
|
201
|
+
|
|
202
|
+
async function runProvider(provider, toolArgs) {
|
|
203
|
+
const prompt = toolArgs.prompt;
|
|
204
|
+
const timeoutMs = toolArgs.timeout_ms ?? provider.timeout_ms ?? 300000;
|
|
205
|
+
|
|
206
|
+
// Substitute {prompt} placeholder in args_template
|
|
207
|
+
const args = provider.args_template.map(a =>
|
|
208
|
+
a === '{prompt}' ? prompt : a
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const env = { ...process.env, ...(provider.env ?? {}) };
|
|
212
|
+
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
let child;
|
|
215
|
+
try {
|
|
216
|
+
child = spawn(provider.cli, args, {
|
|
217
|
+
env,
|
|
218
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
219
|
+
});
|
|
220
|
+
} catch (err) {
|
|
221
|
+
resolve(`[spawn error: ${err.message}]`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
child.stdin.end(); // providers are non-interactive; close stdin immediately
|
|
226
|
+
|
|
227
|
+
let stdout = '';
|
|
228
|
+
let stderr = '';
|
|
229
|
+
let truncated = false;
|
|
230
|
+
let timedOut = false;
|
|
231
|
+
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
timedOut = true;
|
|
234
|
+
child.kill('SIGTERM');
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
try { if (!child.killed) child.kill('SIGKILL'); } catch (_) { /* ignore */ }
|
|
237
|
+
}, 5000);
|
|
238
|
+
}, timeoutMs);
|
|
239
|
+
|
|
240
|
+
child.stdout.on('data', d => {
|
|
241
|
+
if (!truncated) {
|
|
242
|
+
const chunk = d.toString();
|
|
243
|
+
if (stdout.length + chunk.length > MAX_BUFFER) {
|
|
244
|
+
stdout += chunk.slice(0, MAX_BUFFER - stdout.length);
|
|
245
|
+
truncated = true;
|
|
246
|
+
} else {
|
|
247
|
+
stdout += chunk;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
child.stderr.on('data', d => {
|
|
253
|
+
stderr += d.toString().slice(0, 4096); // keep stderr brief
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
child.on('close', (code) => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
const output = stdout || stderr || '(no output)';
|
|
259
|
+
const suffix = timedOut
|
|
260
|
+
? `\n\n[TIMED OUT after ${timeoutMs}ms]`
|
|
261
|
+
: truncated ? '\n\n[OUTPUT TRUNCATED at 10MB]' : '';
|
|
262
|
+
const exitNote = (code !== 0 && !timedOut) ? `\n\n[exit code ${code}]` : '';
|
|
263
|
+
resolve(output + suffix + exitNote);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
child.on('error', (err) => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
resolve(`[spawn error: ${err.message}]`);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Run a subprocess with explicit args (used for help, extraTools) */
|
|
274
|
+
async function runSubprocessWithArgs(provider, args, timeoutMs = 30000) {
|
|
275
|
+
const env = { ...process.env, ...(provider.env ?? {}) };
|
|
276
|
+
|
|
277
|
+
return new Promise((resolve) => {
|
|
278
|
+
let child;
|
|
279
|
+
try {
|
|
280
|
+
child = spawn(provider.cli, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
resolve(`[spawn error: ${err.message}]`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
child.stdin.end();
|
|
287
|
+
let stdout = '';
|
|
288
|
+
let stderr = '';
|
|
289
|
+
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
child.kill('SIGTERM');
|
|
292
|
+
}, timeoutMs);
|
|
293
|
+
|
|
294
|
+
child.stdout.on('data', d => { stdout += d.toString(); });
|
|
295
|
+
child.stderr.on('data', d => { stderr += d.toString().slice(0, 4096); });
|
|
296
|
+
|
|
297
|
+
child.on('close', () => {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
resolve(stdout || stderr || '(no output)');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
child.on('error', (err) => {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
resolve(`[spawn error: ${err.message}]`);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Run npm view opencode version to check for updates */
|
|
310
|
+
async function runCheckUpdate() {
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
let child;
|
|
313
|
+
try {
|
|
314
|
+
child = spawn('npm', ['view', 'opencode', 'version'], {
|
|
315
|
+
env: process.env,
|
|
316
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
resolve(`[error checking update: ${err.message}]`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
child.stdin.end();
|
|
324
|
+
let stdout = '';
|
|
325
|
+
let stderr = '';
|
|
326
|
+
|
|
327
|
+
const timer = setTimeout(() => { child.kill('SIGTERM'); }, 30000);
|
|
328
|
+
|
|
329
|
+
child.stdout.on('data', d => { stdout += d.toString(); });
|
|
330
|
+
child.stderr.on('data', d => { stderr += d.toString().slice(0, 1000); });
|
|
331
|
+
|
|
332
|
+
child.on('close', () => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
resolve(stdout.trim() || stderr || '(no output)');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
child.on('error', (err) => {
|
|
338
|
+
clearTimeout(timer);
|
|
339
|
+
resolve(`[error: ${err.message}]`);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── HTTP provider execution ───────────────────────────────────────────────────
|
|
345
|
+
async function runHttpProvider(provider, toolArgs) {
|
|
346
|
+
const prompt = toolArgs.prompt;
|
|
347
|
+
const timeoutMs = toolArgs.timeout_ms ?? provider.timeout_ms ?? 120000;
|
|
348
|
+
const apiKey = process.env[provider.apiKeyEnv] ?? '';
|
|
349
|
+
|
|
350
|
+
const body = JSON.stringify({
|
|
351
|
+
model: provider.model,
|
|
352
|
+
messages: [{ role: 'user', content: prompt }],
|
|
353
|
+
stream: false,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const url = new URL(provider.baseUrl + '/chat/completions');
|
|
357
|
+
const isHttps = url.protocol === 'https:';
|
|
358
|
+
const transport = isHttps ? https : http;
|
|
359
|
+
|
|
360
|
+
const options = {
|
|
361
|
+
hostname: url.hostname,
|
|
362
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
363
|
+
path: url.pathname,
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: {
|
|
366
|
+
'Content-Type': 'application/json',
|
|
367
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
368
|
+
'Content-Length': Buffer.byteLength(body),
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
return new Promise((resolve) => {
|
|
373
|
+
let timedOut = false;
|
|
374
|
+
const req = transport.request(options, (res) => {
|
|
375
|
+
let data = '';
|
|
376
|
+
res.on('data', chunk => { data += chunk; });
|
|
377
|
+
res.on('end', () => {
|
|
378
|
+
if (timedOut) return;
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(data);
|
|
381
|
+
const content = parsed?.choices?.[0]?.message?.content;
|
|
382
|
+
if (content) {
|
|
383
|
+
resolve(content);
|
|
384
|
+
} else {
|
|
385
|
+
resolve(`[HTTP error: unexpected response shape] ${data.slice(0, 500)}`);
|
|
386
|
+
}
|
|
387
|
+
} catch (e) {
|
|
388
|
+
resolve(`[HTTP error: JSON parse failed] ${data.slice(0, 500)}`);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const timer = setTimeout(() => {
|
|
394
|
+
timedOut = true;
|
|
395
|
+
req.destroy();
|
|
396
|
+
resolve(`[TIMED OUT after ${timeoutMs}ms]`);
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
|
|
399
|
+
req.on('error', (err) => {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
resolve(`[HTTP request error: ${err.message}]`);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
req.on('close', () => clearTimeout(timer));
|
|
405
|
+
|
|
406
|
+
req.write(body);
|
|
407
|
+
req.end();
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Slot-mode HTTP execution: reads ANTHROPIC_BASE_URL/KEY/MODEL from env */
|
|
412
|
+
async function runSlotHttpProvider(provider, toolArgs) {
|
|
413
|
+
const effectiveProvider = {
|
|
414
|
+
...provider,
|
|
415
|
+
baseUrl: process.env.ANTHROPIC_BASE_URL ?? provider.baseUrl,
|
|
416
|
+
model: process.env.CLAUDE_DEFAULT_MODEL ?? provider.model,
|
|
417
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
418
|
+
timeout_ms: parseInt(process.env.CLAUDE_MCP_TIMEOUT_MS ?? '0') || provider.timeout_ms,
|
|
419
|
+
};
|
|
420
|
+
return runHttpProvider(effectiveProvider, toolArgs);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** health_check: POST minimal request, return { healthy, latencyMs, model } */
|
|
424
|
+
async function runHealthCheck(provider) {
|
|
425
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? provider.baseUrl;
|
|
426
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? process.env[provider.apiKeyEnv] ?? '';
|
|
427
|
+
const model = process.env.CLAUDE_DEFAULT_MODEL ?? provider.model;
|
|
428
|
+
const timeoutMs = parseInt(process.env.CLAUDE_MCP_HEALTH_TIMEOUT_MS ?? '30000');
|
|
429
|
+
|
|
430
|
+
const body = JSON.stringify({
|
|
431
|
+
model,
|
|
432
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
433
|
+
max_tokens: 1,
|
|
434
|
+
stream: false,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const url = new URL(baseUrl + '/chat/completions');
|
|
438
|
+
const isHttps = url.protocol === 'https:';
|
|
439
|
+
const transport = isHttps ? https : http;
|
|
440
|
+
|
|
441
|
+
const options = {
|
|
442
|
+
hostname: url.hostname,
|
|
443
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
444
|
+
path: url.pathname,
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
'Content-Type': 'application/json',
|
|
448
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
449
|
+
'Content-Length': Buffer.byteLength(body),
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const startTime = Date.now();
|
|
454
|
+
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
let timedOut = false;
|
|
457
|
+
const req = transport.request(options, (res) => {
|
|
458
|
+
let data = '';
|
|
459
|
+
res.on('data', chunk => { data += chunk; });
|
|
460
|
+
res.on('end', () => {
|
|
461
|
+
if (timedOut) return;
|
|
462
|
+
const latencyMs = Date.now() - startTime;
|
|
463
|
+
try {
|
|
464
|
+
const parsed = JSON.parse(data);
|
|
465
|
+
if (parsed?.choices || parsed?.id) {
|
|
466
|
+
resolve({ healthy: true, latencyMs, model });
|
|
467
|
+
} else if (parsed?.error) {
|
|
468
|
+
resolve({ healthy: false, error: parsed.error.message ?? JSON.stringify(parsed.error), latencyMs });
|
|
469
|
+
} else {
|
|
470
|
+
resolve({ healthy: false, error: `Unexpected response: ${data.slice(0, 200)}`, latencyMs });
|
|
471
|
+
}
|
|
472
|
+
} catch (e) {
|
|
473
|
+
resolve({ healthy: false, error: `JSON parse failed: ${data.slice(0, 200)}`, latencyMs });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const timer = setTimeout(() => {
|
|
479
|
+
timedOut = true;
|
|
480
|
+
req.destroy();
|
|
481
|
+
resolve({ healthy: false, error: `Timed out after ${timeoutMs}ms`, latencyMs: timeoutMs });
|
|
482
|
+
}, timeoutMs);
|
|
483
|
+
|
|
484
|
+
req.on('error', (err) => {
|
|
485
|
+
clearTimeout(timer);
|
|
486
|
+
resolve({ healthy: false, error: err.message, latencyMs: Date.now() - startTime });
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
req.on('close', () => clearTimeout(timer));
|
|
490
|
+
|
|
491
|
+
req.write(body);
|
|
492
|
+
req.end();
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Build identity JSON for slot mode */
|
|
497
|
+
function buildIdentityResult(provider) {
|
|
498
|
+
let version = '0.0.0';
|
|
499
|
+
try {
|
|
500
|
+
const pkg = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
501
|
+
version = pkg.version ?? version;
|
|
502
|
+
} catch (_) { /* ignore */ }
|
|
503
|
+
|
|
504
|
+
let model;
|
|
505
|
+
if (provider.type === 'http') {
|
|
506
|
+
model = process.env.CLAUDE_DEFAULT_MODEL ?? provider.model;
|
|
507
|
+
} else {
|
|
508
|
+
// Start with static fallback from providers.json, or binary name
|
|
509
|
+
model = provider.model ?? provider.mainTool ?? provider.cli;
|
|
510
|
+
// Attempt dynamic detection via model_detect config
|
|
511
|
+
if (provider.model_detect?.file && provider.model_detect?.pattern) {
|
|
512
|
+
try {
|
|
513
|
+
const detectPath = provider.model_detect.file.replace(/^~/, os.homedir());
|
|
514
|
+
const content = fs.readFileSync(detectPath, 'utf8');
|
|
515
|
+
const match = content.match(new RegExp(provider.model_detect.pattern, 'm'));
|
|
516
|
+
if (match?.[1]) model = match[1];
|
|
517
|
+
} catch (_) { /* fall through to static value */ }
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return JSON.stringify({
|
|
522
|
+
name: 'unified-mcp-server',
|
|
523
|
+
version,
|
|
524
|
+
slot: provider.name,
|
|
525
|
+
type: provider.type,
|
|
526
|
+
model,
|
|
527
|
+
display_provider: provider.display_provider ?? null,
|
|
528
|
+
provider: provider.description,
|
|
529
|
+
install_method: 'qgsd-monorepo',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function runSubprocessHealthCheck(provider) {
|
|
534
|
+
const args = provider.health_check_args ?? ['--version'];
|
|
535
|
+
const startTime = Date.now();
|
|
536
|
+
const output = await runSubprocessWithArgs(provider, args, 10000);
|
|
537
|
+
const latencyMs = Date.now() - startTime;
|
|
538
|
+
const healthy = !output.startsWith('[spawn error') && !output.startsWith('[TIMED');
|
|
539
|
+
return JSON.stringify({ healthy, latencyMs, type: 'subprocess' });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ─── Slot mode tool dispatcher ────────────────────────────────────────────────
|
|
543
|
+
async function handleSlotToolCall(toolName, toolArgs) {
|
|
544
|
+
if (toolName === 'ping') {
|
|
545
|
+
const message = toolArgs.prompt ?? 'pong';
|
|
546
|
+
return `${message} (slot: ${slotProvider.name})`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (toolName === 'identity') {
|
|
550
|
+
return buildIdentityResult(slotProvider);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (slotProvider.type === 'subprocess') {
|
|
554
|
+
if (toolName === 'help') {
|
|
555
|
+
return runSubprocessWithArgs(slotProvider, slotProvider.helpArgs ?? ['--help']);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (toolName === slotProvider.mainTool) {
|
|
559
|
+
return runProvider(slotProvider, toolArgs);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const extra = (slotProvider.extraTools ?? []).find(e => e.name === toolName);
|
|
563
|
+
if (extra) {
|
|
564
|
+
if (extra.checkUpdate) return runCheckUpdate();
|
|
565
|
+
// Run with extra's own args_template
|
|
566
|
+
return runProvider({ ...slotProvider, args_template: extra.args_template }, toolArgs);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (toolName === 'health_check' && slotProvider.health_check_args) {
|
|
570
|
+
return runSubprocessHealthCheck(slotProvider);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (slotProvider.type === 'ccr') {
|
|
575
|
+
if (toolName === 'help') {
|
|
576
|
+
return runSubprocessWithArgs(slotProvider, slotProvider.helpArgs ?? ['--help']);
|
|
577
|
+
}
|
|
578
|
+
if (toolName === 'ask') {
|
|
579
|
+
return runProvider(slotProvider, toolArgs);
|
|
580
|
+
}
|
|
581
|
+
if (toolName === 'health_check' && slotProvider.health_check_args) {
|
|
582
|
+
const result = await runSubprocessHealthCheck(slotProvider);
|
|
583
|
+
// Override type field to 'ccr' for clarity
|
|
584
|
+
try {
|
|
585
|
+
const parsed = JSON.parse(result);
|
|
586
|
+
return JSON.stringify({ ...parsed, type: 'ccr' });
|
|
587
|
+
} catch (_) { return result; }
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (slotProvider.type === 'http') {
|
|
592
|
+
if (toolName === 'claude') {
|
|
593
|
+
return runSlotHttpProvider(slotProvider, toolArgs);
|
|
594
|
+
}
|
|
595
|
+
if (toolName === 'health_check') {
|
|
596
|
+
return JSON.stringify(await runHealthCheck(slotProvider));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return null; // unknown tool
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ─── Request handlers ──────────────────────────────────────────────────────────
|
|
604
|
+
const toolMap = new Map(providers.map(p => [p.name, p]));
|
|
605
|
+
|
|
606
|
+
async function handleRequest(req) {
|
|
607
|
+
const { id, method, params } = req;
|
|
608
|
+
|
|
609
|
+
if (method === 'initialize') {
|
|
610
|
+
sendResult(id, {
|
|
611
|
+
protocolVersion: '2024-11-05',
|
|
612
|
+
serverInfo: { name: 'unified-mcp-server', version: '1.0.0' },
|
|
613
|
+
capabilities: { tools: {} },
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (method === 'notifications/initialized') {
|
|
619
|
+
return; // no response for notifications
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (method === 'tools/list') {
|
|
623
|
+
sendResult(id, { tools: buildTools() });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (method === 'tools/call') {
|
|
628
|
+
const toolName = params?.name;
|
|
629
|
+
const toolArgs = params?.arguments ?? {};
|
|
630
|
+
|
|
631
|
+
// ── PROVIDER_SLOT mode dispatch ──────────────────────────────────────────
|
|
632
|
+
if (slotProvider) {
|
|
633
|
+
let output;
|
|
634
|
+
try {
|
|
635
|
+
output = await handleSlotToolCall(toolName, toolArgs);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
sendResult(id, {
|
|
638
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
639
|
+
isError: true,
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (output === null) {
|
|
645
|
+
sendResult(id, {
|
|
646
|
+
content: [{ type: 'text', text: `Unknown tool in slot ${slotProvider.name}: ${toolName}` }],
|
|
647
|
+
isError: true,
|
|
648
|
+
});
|
|
649
|
+
} else {
|
|
650
|
+
sendResult(id, {
|
|
651
|
+
content: [{ type: 'text', text: typeof output === 'string' ? output : JSON.stringify(output) }],
|
|
652
|
+
isError: false,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── All-providers mode dispatch ──────────────────────────────────────────
|
|
659
|
+
const provider = toolMap.get(toolName);
|
|
660
|
+
|
|
661
|
+
if (!provider) {
|
|
662
|
+
sendResult(id, {
|
|
663
|
+
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
|
664
|
+
isError: true,
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const output = provider.type === 'http'
|
|
671
|
+
? await runHttpProvider(provider, toolArgs)
|
|
672
|
+
: await runProvider(provider, toolArgs);
|
|
673
|
+
sendResult(id, {
|
|
674
|
+
content: [{ type: 'text', text: output }],
|
|
675
|
+
isError: false,
|
|
676
|
+
});
|
|
677
|
+
} catch (err) {
|
|
678
|
+
sendResult(id, {
|
|
679
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
680
|
+
isError: true,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Unknown method — return method not found error
|
|
687
|
+
if (id !== undefined && id !== null) {
|
|
688
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ─── Main entry point (async for keytar bootstrap) ────────────────────────────
|
|
693
|
+
async function main() {
|
|
694
|
+
// ─── Keytar API key bootstrap ─────────────────────────────────────────────────
|
|
695
|
+
// If running in PROVIDER_SLOT mode, load the slot's API key from keytar at
|
|
696
|
+
// startup (one keychain access per process — no repeated prompts).
|
|
697
|
+
// Falls back to ANTHROPIC_API_KEY already in process.env (backward-compat).
|
|
698
|
+
if (SLOT && !process.env.ANTHROPIC_API_KEY) {
|
|
699
|
+
const keytarAccount = 'ANTHROPIC_API_KEY_' + SLOT.toUpperCase().replace(/-/g, '_');
|
|
700
|
+
try {
|
|
701
|
+
const { default: keytar } = await import('keytar');
|
|
702
|
+
const secret = await keytar.getPassword('qgsd', keytarAccount);
|
|
703
|
+
if (secret) {
|
|
704
|
+
process.env.ANTHROPIC_API_KEY = secret;
|
|
705
|
+
process.stderr.write(`[unified-mcp-server] Loaded API key for slot ${SLOT} from keychain\n`);
|
|
706
|
+
}
|
|
707
|
+
} catch (e) {
|
|
708
|
+
// keytar unavailable or no entry — continue without it
|
|
709
|
+
process.stderr.write(`[unified-mcp-server] keytar unavailable for slot ${SLOT}: ${e.message}\n`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ─── Stdin line reader ────────────────────────────────────────────────────────
|
|
714
|
+
const rl = createInterface({ input: process.stdin });
|
|
715
|
+
|
|
716
|
+
rl.on('line', async (line) => {
|
|
717
|
+
const trimmed = line.trim();
|
|
718
|
+
if (!trimmed) return;
|
|
719
|
+
let req;
|
|
720
|
+
try {
|
|
721
|
+
req = JSON.parse(trimmed);
|
|
722
|
+
} catch (e) {
|
|
723
|
+
sendError(null, -32700, 'Parse error');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
await handleRequest(req);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
rl.on('close', () => process.exit(0));
|
|
730
|
+
|
|
731
|
+
const slotLabel = SLOT ? ` [slot: ${SLOT}]` : ' [all-providers]';
|
|
732
|
+
process.stderr.write(`[unified-mcp-server] started${slotLabel}\n`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
main();
|