@holin-work/holin-cli 1.1.2 → 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 +93 -24
- package/package.json +1 -1
package/bin/holin-cli.js
CHANGED
|
@@ -119,21 +119,87 @@ 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
|
-
|
|
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
|
+
}
|
|
186
|
+
|
|
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
|
+
});
|
|
130
197
|
res.end(body);
|
|
131
198
|
return;
|
|
132
199
|
}
|
|
133
200
|
|
|
134
201
|
// Health check endpoint
|
|
135
202
|
if (req.method === "GET" && req.url === "/health") {
|
|
136
|
-
const creds = readCredentials();
|
|
137
203
|
const body = JSON.stringify({
|
|
138
204
|
status: "ok",
|
|
139
205
|
authenticated: !!(creds && creds.api_key),
|
|
@@ -150,35 +216,27 @@ function createProxyServer() {
|
|
|
150
216
|
return;
|
|
151
217
|
}
|
|
152
218
|
|
|
153
|
-
// Read credentials on every request (hot-reload support)
|
|
154
|
-
const creds = readCredentials();
|
|
155
|
-
if (!creds?.api_key) {
|
|
156
|
-
const body = JSON.stringify({ error: "not_authenticated", message: "Run 'holin-cli auth login' first." });
|
|
157
|
-
res.writeHead(401, {
|
|
158
|
-
"Content-Type": "application/json",
|
|
159
|
-
"Content-Length": Buffer.byteLength(body),
|
|
160
|
-
});
|
|
161
|
-
res.end(body);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
219
|
// Build upstream request
|
|
166
220
|
// MCP streamable-http has a single endpoint; forward directly to upstream path.
|
|
167
221
|
// Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
|
|
168
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
|
+
|
|
169
230
|
const options = {
|
|
170
231
|
hostname: upstreamUrl.hostname,
|
|
171
232
|
port: upstreamUrl.port || 443,
|
|
172
233
|
path: upstreamUrl.pathname,
|
|
173
234
|
method: req.method,
|
|
174
|
-
headers:
|
|
175
|
-
...req.headers,
|
|
176
|
-
host: upstreamUrl.hostname,
|
|
177
|
-
"X-API-Key": creds.api_key,
|
|
178
|
-
},
|
|
235
|
+
headers: forwardHeaders,
|
|
179
236
|
};
|
|
180
237
|
|
|
181
238
|
const proxyReq = https.request(options, (proxyRes) => {
|
|
239
|
+
log(`[proxy] ← ${proxyRes.statusCode} content-type=${proxyRes.headers["content-type"]}`);
|
|
182
240
|
// Forward response headers (strip content-length for chunked SSE)
|
|
183
241
|
const headers = { ...proxyRes.headers };
|
|
184
242
|
delete headers["content-length"];
|
|
@@ -210,8 +268,10 @@ function createProxyServer() {
|
|
|
210
268
|
}
|
|
211
269
|
});
|
|
212
270
|
|
|
213
|
-
|
|
214
|
-
|
|
271
|
+
proxyReq.write(bodyBuffer);
|
|
272
|
+
proxyReq.end();
|
|
273
|
+
}); // end req.on("end")
|
|
274
|
+
}); // end http.createServer
|
|
215
275
|
|
|
216
276
|
return server;
|
|
217
277
|
}
|
|
@@ -254,6 +314,15 @@ async function cmdAuthLogin() {
|
|
|
254
314
|
writeCredentials(creds);
|
|
255
315
|
console.log(`[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`);
|
|
256
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
|
+
|
|
257
326
|
// Auto-start proxy in background (detached), wait until it's listening
|
|
258
327
|
try {
|
|
259
328
|
await startProxyDaemon();
|