@holin-work/holin-cli 1.1.3 → 1.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/bin/holin-cli.js +133 -31
- 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,26 +220,64 @@ 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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
// Parse request body to determine MCP method
|
|
239
|
+
let mcpMethod = "";
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(rawBody);
|
|
242
|
+
mcpMethod = parsed.method || "";
|
|
243
|
+
} catch { /* not JSON */ }
|
|
244
|
+
|
|
245
|
+
// Non-streaming methods should return JSON, not SSE
|
|
246
|
+
// Accio expects JSON for initialize and tools/list
|
|
247
|
+
const shouldConvertToJson = mcpMethod === "initialize" || mcpMethod === "tools/list";
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
proxyRes.
|
|
249
|
+
const proxyReq = https.request(options, (proxyRes) => {
|
|
250
|
+
log(`[proxy] ← ${proxyRes.statusCode} content-type=${proxyRes.headers["content-type"]} method=${mcpMethod}`);
|
|
251
|
+
|
|
252
|
+
if (shouldConvertToJson && proxyRes.headers["content-type"]?.includes("text/event-stream")) {
|
|
253
|
+
// Collect SSE response and convert to JSON
|
|
254
|
+
const sseChunks = [];
|
|
255
|
+
proxyRes.on("data", (c) => sseChunks.push(c));
|
|
256
|
+
proxyRes.on("end", () => {
|
|
257
|
+
const sseText = Buffer.concat(sseChunks).toString("utf8");
|
|
258
|
+
let jsonBody = sseText;
|
|
259
|
+
// Parse SSE: extract the last "data:" line
|
|
260
|
+
for (const line of sseText.split("\n")) {
|
|
261
|
+
const trimmed = line.trim();
|
|
262
|
+
if (trimmed.startsWith("data:")) {
|
|
263
|
+
jsonBody = trimmed.slice(5).trim();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const headers = { ...proxyRes.headers };
|
|
267
|
+
headers["content-type"] = "application/json";
|
|
268
|
+
headers["content-length"] = Buffer.byteLength(jsonBody);
|
|
269
|
+
delete headers["transfer-encoding"];
|
|
270
|
+
res.writeHead(proxyRes.statusCode || 502, headers);
|
|
271
|
+
res.end(jsonBody);
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
// Stream SSE directly for tools/call and other methods
|
|
275
|
+
const headers = { ...proxyRes.headers };
|
|
276
|
+
delete headers["content-length"];
|
|
277
|
+
delete headers["transfer-encoding"];
|
|
278
|
+
res.writeHead(proxyRes.statusCode || 502, headers);
|
|
279
|
+
proxyRes.pipe(res);
|
|
280
|
+
}
|
|
190
281
|
});
|
|
191
282
|
|
|
192
283
|
proxyReq.on("error", (err) => {
|
|
@@ -211,8 +302,10 @@ function createProxyServer() {
|
|
|
211
302
|
}
|
|
212
303
|
});
|
|
213
304
|
|
|
214
|
-
|
|
215
|
-
|
|
305
|
+
proxyReq.write(bodyBuffer);
|
|
306
|
+
proxyReq.end();
|
|
307
|
+
}); // end req.on("end")
|
|
308
|
+
}); // end http.createServer
|
|
216
309
|
|
|
217
310
|
return server;
|
|
218
311
|
}
|
|
@@ -255,6 +348,15 @@ async function cmdAuthLogin() {
|
|
|
255
348
|
writeCredentials(creds);
|
|
256
349
|
console.log(`[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`);
|
|
257
350
|
|
|
351
|
+
// Establish server-side session via set_holin_credentials
|
|
352
|
+
// Accio sends MCP requests without API key; server reads key from this session.
|
|
353
|
+
try {
|
|
354
|
+
await setCredentials(apiKey);
|
|
355
|
+
console.log("[holin-cli] Server-side session established.");
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(`[holin-cli] Warning: Failed to establish server session — ${err.message}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
258
360
|
// Auto-start proxy in background (detached), wait until it's listening
|
|
259
361
|
try {
|
|
260
362
|
await startProxyDaemon();
|