@holin-work/holin-cli 1.1.3 → 1.2.0
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/bin/holin-cli.js +92 -24
- package/package.json +1 -1
package/bin/holin-cli.js
CHANGED
|
@@ -119,28 +119,81 @@ async function pingVerify(apiKey) {
|
|
|
119
119
|
throw new Error("Unexpected response format from MCP server");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Call set_holin_credentials on MCP server to establish server-side session.
|
|
124
|
+
* This is required for cli-login mode: Accio sends requests without API key,
|
|
125
|
+
* server reads key from session established by this call.
|
|
126
|
+
*/
|
|
127
|
+
async function setCredentials(apiKey) {
|
|
128
|
+
const res = await fetch(UPSTREAM_URL, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
Accept: "application/json, text/event-stream",
|
|
133
|
+
"X-API-Key": apiKey,
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
id: 2,
|
|
138
|
+
method: "tools/call",
|
|
139
|
+
params: { name: "set_holin_credentials", arguments: { api_key: apiKey } },
|
|
140
|
+
}),
|
|
141
|
+
signal: AbortSignal.timeout(10000),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!res.ok) throw new Error(`MCP server responded ${res.status}`);
|
|
145
|
+
|
|
146
|
+
const raw = await res.text();
|
|
147
|
+
for (const line of raw.split("\n")) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
if (trimmed.startsWith("data:")) {
|
|
150
|
+
const envelope = JSON.parse(trimmed.slice(5).trim());
|
|
151
|
+
if (envelope?.error) {
|
|
152
|
+
throw new Error(envelope.error.message || "set_holin_credentials failed");
|
|
153
|
+
}
|
|
154
|
+
const text = envelope?.result?.content?.[0]?.text;
|
|
155
|
+
if (text) return JSON.parse(text);
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw new Error("Unexpected response format from set_holin_credentials");
|
|
160
|
+
}
|
|
161
|
+
|
|
122
162
|
// ── Proxy Server ─────────────────────────────────────────────────────────────
|
|
123
163
|
|
|
124
164
|
function createProxyServer() {
|
|
125
165
|
const server = http.createServer((req, res) => {
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
// Collect request body for logging
|
|
167
|
+
const chunks = [];
|
|
168
|
+
req.on("data", (c) => chunks.push(c));
|
|
169
|
+
req.on("end", () => {
|
|
170
|
+
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
171
|
+
let bodyPreview = rawBody.slice(0, 300);
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(rawBody);
|
|
174
|
+
bodyPreview = JSON.stringify({ method: parsed.method, id: parsed.id, params: parsed.params ? Object.keys(parsed.params) : undefined });
|
|
175
|
+
} catch { /* not JSON */ }
|
|
176
|
+
|
|
177
|
+
log(`[proxy] ${req.method} ${req.url} body=${bodyPreview}`);
|
|
178
|
+
|
|
179
|
+
// Block OAuth discovery probes — return 401 so platform knows auth is required
|
|
180
|
+
if (req.url.startsWith("/.well-known/")) {
|
|
181
|
+
const body = JSON.stringify({ error: "unauthorized" });
|
|
182
|
+
res.writeHead(401, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
183
|
+
res.end(body);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
133
186
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
187
|
+
// Read credentials on every request (hot-reload support)
|
|
188
|
+
const creds = readCredentials();
|
|
189
|
+
if (!creds?.api_key) {
|
|
190
|
+
// No credentials: return 401 so platform knows auth is required (not "no auth needed")
|
|
191
|
+
const body = JSON.stringify({ error: "unauthorized", message: "Run 'holin-cli auth login' first." });
|
|
192
|
+
res.writeHead(401, {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
"Content-Length": Buffer.byteLength(body),
|
|
195
|
+
"WWW-Authenticate": "Bearer realm=\"Holin API\"",
|
|
196
|
+
});
|
|
144
197
|
res.end(body);
|
|
145
198
|
return;
|
|
146
199
|
}
|
|
@@ -167,19 +220,23 @@ function createProxyServer() {
|
|
|
167
220
|
// MCP streamable-http has a single endpoint; forward directly to upstream path.
|
|
168
221
|
// Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
|
|
169
222
|
const upstreamUrl = new URL(UPSTREAM_URL);
|
|
223
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
224
|
+
const forwardHeaders = { ...req.headers };
|
|
225
|
+
delete forwardHeaders["transfer-encoding"];
|
|
226
|
+
forwardHeaders["content-length"] = bodyBuffer.length;
|
|
227
|
+
forwardHeaders["host"] = upstreamUrl.hostname;
|
|
228
|
+
forwardHeaders["X-API-Key"] = creds.api_key;
|
|
229
|
+
|
|
170
230
|
const options = {
|
|
171
231
|
hostname: upstreamUrl.hostname,
|
|
172
232
|
port: upstreamUrl.port || 443,
|
|
173
233
|
path: upstreamUrl.pathname,
|
|
174
234
|
method: req.method,
|
|
175
|
-
headers:
|
|
176
|
-
...req.headers,
|
|
177
|
-
host: upstreamUrl.hostname,
|
|
178
|
-
"X-API-Key": creds.api_key,
|
|
179
|
-
},
|
|
235
|
+
headers: forwardHeaders,
|
|
180
236
|
};
|
|
181
237
|
|
|
182
238
|
const proxyReq = https.request(options, (proxyRes) => {
|
|
239
|
+
log(`[proxy] ← ${proxyRes.statusCode} content-type=${proxyRes.headers["content-type"]}`);
|
|
183
240
|
// Forward response headers (strip content-length for chunked SSE)
|
|
184
241
|
const headers = { ...proxyRes.headers };
|
|
185
242
|
delete headers["content-length"];
|
|
@@ -211,8 +268,10 @@ function createProxyServer() {
|
|
|
211
268
|
}
|
|
212
269
|
});
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
271
|
+
proxyReq.write(bodyBuffer);
|
|
272
|
+
proxyReq.end();
|
|
273
|
+
}); // end req.on("end")
|
|
274
|
+
}); // end http.createServer
|
|
216
275
|
|
|
217
276
|
return server;
|
|
218
277
|
}
|
|
@@ -255,6 +314,15 @@ async function cmdAuthLogin() {
|
|
|
255
314
|
writeCredentials(creds);
|
|
256
315
|
console.log(`[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`);
|
|
257
316
|
|
|
317
|
+
// Establish server-side session via set_holin_credentials
|
|
318
|
+
// Accio sends MCP requests without API key; server reads key from this session.
|
|
319
|
+
try {
|
|
320
|
+
await setCredentials(apiKey);
|
|
321
|
+
console.log("[holin-cli] Server-side session established.");
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error(`[holin-cli] Warning: Failed to establish server session — ${err.message}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
258
326
|
// Auto-start proxy in background (detached), wait until it's listening
|
|
259
327
|
try {
|
|
260
328
|
await startProxyDaemon();
|