create-multicast 0.1.1 → 0.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/dist/cli.js CHANGED
@@ -256,45 +256,88 @@ async function main() {
256
256
  process.exit(0);
257
257
  }
258
258
  const selectedNames = selections;
259
- // Confirm/collect auth for each selected server
259
+ // Auto-detect auth for each selected server
260
+ // Strategy:
261
+ // 1. If auth found in config → use it silently
262
+ // 2. If no auth → probe the server to check if it needs auth
263
+ // 3. Only ask the user if we can't determine automatically
264
+ const authSpinner = p.spinner();
265
+ authSpinner.start("Detecting authentication requirements...");
260
266
  for (const name of selectedNames) {
261
267
  const server = httpServers.find((s) => s.name === name);
262
268
  let auth = server.auth;
269
+ let authStatus;
263
270
  if (auth) {
264
- const useExisting = await p.confirm({
265
- message: `${name} found auth credentials. Use them?`,
266
- initialValue: true,
267
- });
268
- if (p.isCancel(useExisting)) {
269
- p.cancel("Setup cancelled.");
270
- process.exit(0);
271
- }
272
- if (!useExisting)
273
- auth = undefined;
271
+ // Auth found in existing config — use it
272
+ authStatus = "credentials found in config";
274
273
  }
275
- if (!auth) {
276
- const needsAuth = await p.confirm({
277
- message: `${name} — does this server require authentication?`,
278
- initialValue: false,
279
- });
280
- if (!p.isCancel(needsAuth) && needsAuth) {
281
- const authResult = await p.text({
282
- message: `Enter auth header for ${name}:`,
283
- placeholder: "Bearer your-token-here",
274
+ else {
275
+ // No auth in config — probe the server
276
+ try {
277
+ const probe = await fetch(server.url, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json" },
280
+ body: JSON.stringify({
281
+ jsonrpc: "2.0",
282
+ method: "initialize",
283
+ params: {
284
+ protocolVersion: "2024-11-05",
285
+ capabilities: {},
286
+ clientInfo: { name: "multicast-probe", version: "0.1.0" },
287
+ },
288
+ id: "probe",
289
+ }),
290
+ signal: AbortSignal.timeout(5000),
284
291
  });
285
- if (p.isCancel(authResult)) {
286
- p.cancel("Setup cancelled.");
287
- process.exit(0);
292
+ if (probe.status === 401 || probe.status === 403) {
293
+ // Server requires auth but we don't have it — need to ask
294
+ authStatus = "needs-auth";
295
+ }
296
+ else {
297
+ // Server responded without auth — no auth needed
298
+ authStatus = "no auth required";
288
299
  }
289
- auth = authResult;
300
+ }
301
+ catch {
302
+ // Can't reach server — assume no auth (will fail later with clear error)
303
+ authStatus = "unreachable (skipping auth)";
290
304
  }
291
305
  }
306
+ if (authStatus === "needs-auth") {
307
+ // Only ask when we KNOW the server needs auth but we don't have it
308
+ authSpinner.stop(`${pc.yellow("!")} ${name} requires authentication.`);
309
+ const authResult = await p.text({
310
+ message: `Enter auth header for ${pc.bold(name)}:`,
311
+ placeholder: "Bearer your-token-here",
312
+ validate: (v) => {
313
+ if (!v.trim())
314
+ return "Auth header is required — server returned 401";
315
+ return undefined;
316
+ },
317
+ });
318
+ if (p.isCancel(authResult)) {
319
+ p.cancel("Setup cancelled.");
320
+ process.exit(0);
321
+ }
322
+ auth = authResult;
323
+ authSpinner.start("Detecting authentication requirements...");
324
+ }
292
325
  selectedServers.push({
293
326
  name,
294
327
  url: server.url,
295
328
  auth: auth || undefined,
296
329
  });
297
330
  }
331
+ authSpinner.stop("Authentication configured.");
332
+ // Show auth summary
333
+ for (const s of selectedServers) {
334
+ if (s.auth) {
335
+ p.log.message(` ${pc.green("✓")} ${pc.bold(s.name)} — auth configured`);
336
+ }
337
+ else {
338
+ p.log.message(` ${pc.green("✓")} ${pc.bold(s.name)} — no auth needed`);
339
+ }
340
+ }
298
341
  }
299
342
  // Option to add servers manually
300
343
  let addMore = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-multicast",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Create a Multicast MCP gateway — one command to scaffold, configure, and deploy your parallel MCP server.",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",
@@ -67,16 +67,53 @@ async function callMcpServer(
67
67
  const timer = setTimeout(() => controller.abort(), timeoutMs);
68
68
 
69
69
  try {
70
- const headers: Record<string, string> = {
70
+ const baseHeaders: Record<string, string> = {
71
71
  "Content-Type": "application/json",
72
+ "Accept": "application/json, text/event-stream",
72
73
  };
73
74
  if (server.auth) {
74
- headers["Authorization"] = server.auth;
75
+ baseHeaders["Authorization"] = server.auth;
76
+ }
77
+
78
+ // Step 1: Initialize MCP session
79
+ const initResponse = await fetch(server.url, {
80
+ method: "POST",
81
+ headers: baseHeaders,
82
+ body: JSON.stringify({
83
+ jsonrpc: "2.0",
84
+ method: "initialize",
85
+ params: {
86
+ protocolVersion: "2024-11-05",
87
+ capabilities: {},
88
+ clientInfo: { name: "multicast", version: "0.1.0" },
89
+ },
90
+ id: "init-" + crypto.randomUUID(),
91
+ }),
92
+ signal: controller.signal,
93
+ });
94
+
95
+ const sessionId = initResponse.headers.get("mcp-session-id");
96
+
97
+ // Step 2: Send initialized notification (required by spec)
98
+ const callHeaders = { ...baseHeaders };
99
+ if (sessionId) {
100
+ callHeaders["mcp-session-id"] = sessionId;
75
101
  }
76
102
 
103
+ await fetch(server.url, {
104
+ method: "POST",
105
+ headers: callHeaders,
106
+ body: JSON.stringify({
107
+ jsonrpc: "2.0",
108
+ method: "notifications/initialized",
109
+ }),
110
+ signal: controller.signal,
111
+ });
112
+
113
+ // Step 3: Call the tool with the session
77
114
  const response = await fetch(server.url, {
78
115
  method: "POST",
79
- headers,
116
+ headers: callHeaders,
80
117
  body: JSON.stringify({
81
118
  jsonrpc: "2.0",
82
119
  method: "tools/call",
@@ -148,6 +185,7 @@ async function discoverServerTools(
148
185
  try {
149
186
  const headers: Record<string, string> = {
150
187
  "Content-Type": "application/json",
188
+ "Accept": "application/json, text/event-stream",
151
189
  };
152
190
  if (server.auth) {
153
191
  headers["Authorization"] = server.auth;
@@ -222,6 +260,15 @@ async function discoverServerTools(
222
260
 
223
261
  const tools = data.result?.tools || [];
224
262
 
263
+ // Upsert server row FIRST (tools table has FK reference to servers)
264
+ await db
265
+ .prepare(
266
+ `INSERT OR REPLACE INTO servers (name, url, description, tool_count, status, last_error, last_discovered_at, updated_at)
267
+ VALUES (?, ?, '', ?, 'active', NULL, datetime('now'), datetime('now'))`
268
+ )
269
+ .bind(server.name, server.url, tools.length)
270
+ .run();
271
+
225
272
  // Clear old tools for this server
226
273
  await db
227
274
  .prepare("DELETE FROM tools WHERE server_name = ?")
@@ -244,15 +291,6 @@ async function discoverServerTools(
244
291
  .run();
245
292
  }
246
293
 
247
- // Update server metadata
248
- await db
249
- .prepare(
250
- `INSERT OR REPLACE INTO servers (name, url, description, tool_count, status, last_error, last_discovered_at, updated_at)
251
- VALUES (?, ?, '', ?, 'active', NULL, datetime('now'), datetime('now'))`
252
- )
253
- .bind(server.name, server.url, tools.length)
254
- .run();
255
-
256
294
  return { tool_count: tools.length };
257
295
  } catch (err: unknown) {
258
296
  const message = err instanceof Error ? err.message : "discovery failed";