neon 3.4.0 → 3.5.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.
@@ -1,18 +1,17 @@
1
1
  import { FALLBACK_TEMPLATES, fetchTemplates, scaffoldTemplate } from "./bootstrap.js";
2
2
  import { detectAgent, detectIde } from "./detect_agent.js";
3
- import { ALL_CONFIGURABLE_AGENTS, getAddMcpAgentId } from "./agents.js";
3
+ import { agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, getSkillsAgentName, mcpPickerOptions, tryResolveAddMcpAgentId } from "./agents.js";
4
4
  import { ensureNeonctlAuth, isAuthenticated } from "./auth.js";
5
- import { detectAvailableEditors } from "./editors.js";
6
- import { installExtension, isExtensionInstalled, usesExtension } from "./extension.js";
5
+ import { installExtension, isExtensionInstalled } from "./extension.js";
7
6
  import { inspectProject } from "./inspect.js";
7
+ import { installNeonMcpServer } from "./install_mcp.js";
8
8
  import { ensureNeonctl } from "./neonctl.js";
9
- import { ensureSkillsUpToDate, installAgentSkills } from "./skills.js";
9
+ import { ensureSkillsUpToDate, installAgentSkills, skillsInstalledForAgent } from "./skills.js";
10
10
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
  import which from "which";
13
13
  import { SelectPrompt } from "@clack/core";
14
14
  import { confirm, isCancel, log, multiselect, outro, select, spinner } from "@clack/prompts";
15
- import { execa } from "execa";
16
15
  import { bold, dim, gray, italic } from "yoctocolors";
17
16
  import picocolors from "picocolors";
18
17
  //#region src/init/interactive.ts
@@ -114,8 +113,10 @@ async function interactiveInitInner(options) {
114
113
  " ╚═╝ ╚══╝╚═════╝ ╚═════╝ ╚═╝ ╚══╝"
115
114
  ].join("\n")}\x1b[0m\n`);
116
115
  process.stdout.write(`${dim(wordWrap("\nLet's get your project set up with Neon. We'll install the MCP server, agent skills, and IDE extension, then connect your app to a database.\n", process.stdout.columns || 80))}\n`);
117
- const detectedAgentId = detectAgent();
118
- const detectedEditor = detectedAgentId ? agentIdToEditor(detectedAgentId) : null;
116
+ const detectedAgent = (() => {
117
+ const raw = detectAgent();
118
+ return raw ? tryResolveAddMcpAgentId(raw) : void 0;
119
+ })();
119
120
  const inspectSpinner = spinner();
120
121
  inspectSpinner.start("Checking existing configuration...");
121
122
  const inspection = await inspectProject([
@@ -216,12 +217,12 @@ async function interactiveInitInner(options) {
216
217
  existing._init = { features: selectedFeatures };
217
218
  writeFileSync(neonPath, `${JSON.stringify(existing, null, 2)}\n`);
218
219
  }
219
- const mcpAlready = inspection.mcpConfigured === true;
220
- const skillsAlready = inspection.skillsInstalled === true || selectedTemplate !== null;
220
+ const mcpHits = inspection.mcpAgents ?? [];
221
+ const detectedHasMcp = detectedAgent ? mcpHits.some((hit) => hit.agent === detectedAgent) : false;
222
+ const mcpAlready = detectedAgent ? detectedHasMcp : inspection.mcpConfigured === true;
223
+ const skillsAlready = selectedTemplate !== null || (detectedAgent ? skillsInstalledForAgent(detectedAgent, process.cwd(), options.preview) : inspection.skillsInstalled === true);
221
224
  const hasNeonConnection = inspection.connectionString === true;
222
- let needsMcp = !mcpAlready;
223
- const needsSkills = !skillsAlready;
224
- const needsInstall = needsMcp || needsSkills;
225
+ const needsInstall = !mcpAlready || !skillsAlready;
225
226
  const neonContextPath = resolve(process.cwd(), ".neon");
226
227
  const hasNeonContext = existsSync(neonContextPath) && (() => {
227
228
  try {
@@ -241,7 +242,8 @@ async function interactiveInitInner(options) {
241
242
  return false;
242
243
  })();
243
244
  let extensionAlready = false;
244
- if (detectedEditor && usesExtension(detectedEditor)) extensionAlready = await isExtensionInstalled(detectedEditor);
245
+ const detectedIdeEditor = extensionEditorForAgent(detectedAgent);
246
+ if (detectedIdeEditor) extensionAlready = await isExtensionInstalled(detectedIdeEditor);
245
247
  if (mcpAlready && skillsAlready && hasNeonConnection && hasNeonContext) {
246
248
  log.step(dim(`Neon MCP server already configured (${inspection.mcpScope || "detected"}) ✓`));
247
249
  log.step(dim(`Neon agent skills already installed (${inspection.skillsScope || "detected"}) ✓`));
@@ -282,55 +284,39 @@ async function interactiveInitInner(options) {
282
284
  if (mcpAlready) log.step(dim(`Neon MCP server already configured (${inspection.mcpScope || "detected"}) ✓`));
283
285
  if (skillsAlready) log.step(dim(`Neon agent skills already installed (${inspection.skillsScope || "detected"}) ✓`));
284
286
  if (needsInstall) {
285
- const homeDir = process.env.HOME || process.env.USERPROFILE;
286
- if (!homeDir) {
287
+ if (!(process.env.HOME || process.env.USERPROFILE)) {
287
288
  log.error("Could not determine home directory.");
288
289
  outro("Setup failed.");
289
290
  return;
290
291
  }
291
- let selectedEditors;
292
- if (detectedEditor) selectedEditors = [detectedEditor];
292
+ let selectedAgents;
293
+ if (detectedAgent) selectedAgents = [detectedAgent];
293
294
  else {
294
- const availableEditors = await detectAvailableEditors(homeDir);
295
- const editorResponse = await multiselect({
296
- message: "Which editor(s) would you like to configure?",
297
- options: ALL_CONFIGURABLE_AGENTS.map((agent) => ({
298
- value: agent.editor,
299
- label: agent.editor,
300
- hint: agent.hint
301
- })),
302
- initialValues: availableEditors,
303
- required: true
304
- });
305
- if (isCancel(editorResponse)) {
306
- outro("Setup cancelled.");
307
- return;
308
- }
309
- selectedEditors = editorResponse;
310
- if (selectedEditors.length === 0) {
311
- log.warn("No editors selected.");
295
+ const picked = await pickAgents();
296
+ if (!picked) {
312
297
  outro("Setup cancelled.");
313
298
  return;
314
299
  }
300
+ selectedAgents = picked;
315
301
  }
316
- const vscodeEditors = selectedEditors.filter(usesExtension);
302
+ let vscodeEditors = extensionEditorsFor(selectedAgents);
317
303
  let extensionAlreadyInstalled = false;
318
304
  if (vscodeEditors.length > 0) {
319
305
  extensionAlreadyInstalled = (await Promise.all(vscodeEditors.map((e) => isExtensionInstalled(e)))).every(Boolean);
320
306
  if (extensionAlreadyInstalled) log.step(dim("Neon editor extension already installed ✓"));
321
307
  }
322
- const canInstallExtension = vscodeEditors.length > 0 && !extensionAlreadyInstalled;
308
+ let canInstallExtension = vscodeEditors.length > 0 && !extensionAlreadyInstalled;
323
309
  let doInstallExtension = false;
324
- const hintParts = [];
325
- if (needsMcp) hintParts.push("MCP server (global)");
326
- if (needsSkills) hintParts.push("agent skills (project)");
327
310
  let mcpScope = "global";
328
311
  let skillsScope = "project";
329
312
  let modeResult;
330
313
  while (true) {
331
- const editorName = selectedEditors.join(", ");
314
+ const hintParts = [];
315
+ if (selectedAgents.some((agent) => !mcpHits.some((hit) => hit.agent === agent))) hintParts.push("MCP server (global)");
316
+ if (selectedTemplate === null && selectedAgents.some((agent) => getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview))) hintParts.push("agent skills (project)");
317
+ const agentNames = selectedAgents.map((id) => getAgentDisplayName(id)).join(", ");
332
318
  const result = await select({
333
- message: `Configure ${editorName} for Neon:`,
319
+ message: `Configure ${agentNames} for Neon:`,
334
320
  options: [
335
321
  {
336
322
  value: "defaults",
@@ -344,7 +330,7 @@ async function interactiveInitInner(options) {
344
330
  },
345
331
  {
346
332
  value: "change_editor",
347
- label: "Configure a different editor"
333
+ label: "Configure a different agent"
348
334
  }
349
335
  ],
350
336
  initialValue: "defaults"
@@ -354,33 +340,21 @@ async function interactiveInitInner(options) {
354
340
  return;
355
341
  }
356
342
  if (result === "change_editor") {
357
- const availableEditors = await detectAvailableEditors(homeDir);
358
- const editorResponse = await multiselect({
359
- message: "Which editor(s) would you like to configure?",
360
- options: ALL_CONFIGURABLE_AGENTS.map((agent) => ({
361
- value: agent.editor,
362
- label: agent.editor,
363
- hint: agent.hint
364
- })),
365
- initialValues: availableEditors,
366
- required: true
367
- });
368
- if (isCancel(editorResponse)) {
369
- outro("Setup cancelled.");
370
- return;
371
- }
372
- selectedEditors = editorResponse;
373
- if (selectedEditors.length === 0) {
343
+ const picked = await pickAgents();
344
+ if (!picked) {
374
345
  outro("Setup cancelled.");
375
346
  return;
376
347
  }
348
+ selectedAgents = picked;
349
+ vscodeEditors = extensionEditorsFor(selectedAgents);
350
+ canInstallExtension = vscodeEditors.length > 0 && !extensionAlreadyInstalled;
377
351
  continue;
378
352
  }
379
353
  modeResult = result;
380
354
  break;
381
355
  }
382
356
  if (modeResult === "customize") {
383
- if (needsMcp) {
357
+ if (selectedAgents.some((agent) => !mcpHits.some((hit) => hit.agent === agent))) {
384
358
  const scopeResult = await select({
385
359
  message: "Where should the Neon MCP server be configured?",
386
360
  options: [
@@ -388,10 +362,10 @@ async function interactiveInitInner(options) {
388
362
  value: "global",
389
363
  label: "Global (available in all projects)"
390
364
  },
391
- {
365
+ ...selectedAgents.every(agentSupportsProjectMcp) ? [{
392
366
  value: "project",
393
367
  label: "Project-level (this project only)"
394
- },
368
+ }] : [],
395
369
  {
396
370
  value: "none",
397
371
  label: "Skip — do not install the MCP server"
@@ -403,9 +377,8 @@ async function interactiveInitInner(options) {
403
377
  return;
404
378
  }
405
379
  mcpScope = scopeResult;
406
- if (mcpScope === "none") needsMcp = false;
407
380
  }
408
- if (needsSkills) {
381
+ if (selectedTemplate === null && selectedAgents.some((agent) => getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview))) {
409
382
  const skillsScopeResult = await select({
410
383
  message: "Where should Neon agent skills be installed?",
411
384
  options: [{
@@ -444,7 +417,9 @@ async function interactiveInitInner(options) {
444
417
  }
445
418
  const nctlS = spinner();
446
419
  nctlS.start("Checking Neon CLI...");
447
- const nctlResult = await ensureNeonctl();
420
+ const nctlResult = await ensureNeonctl((phase) => {
421
+ nctlS.message(phase === "installing" ? "Installing Neon CLI..." : "Updating Neon CLI...");
422
+ });
448
423
  switch (nctlResult.status) {
449
424
  case "already_current":
450
425
  nctlS.stop(dim(`Neon CLI is up to date (v${nctlResult.version}) ✓`));
@@ -456,47 +431,39 @@ async function interactiveInitInner(options) {
456
431
  nctlS.stop(dim(`Updated Neon CLI to v${nctlResult.version} ✓`));
457
432
  break;
458
433
  case "failed":
459
- nctlS.stop("Failed to install Neon CLI");
434
+ nctlS.stop(nctlResult.action === "updating" ? "Failed to update Neon CLI" : "Failed to install Neon CLI");
460
435
  log.warn(nctlResult.error ?? "The Neon CLI could not be installed automatically.");
461
436
  if (which.sync("npx", { nothrow: true })) log.warn("Setup will continue using npx.");
462
437
  }
463
- for (const editor of selectedEditors) {
464
- if (needsMcp) {
465
- const mcpArgs = [
466
- "-y",
467
- "add-mcp",
468
- "https://mcp.neon.tech/mcp",
469
- "-n",
470
- "Neon",
471
- "-y",
472
- "-a",
473
- getAddMcpAgentId(editor)
474
- ];
475
- if (mcpScope === "global") mcpArgs.splice(5, 0, "-g");
438
+ for (const agent of selectedAgents) {
439
+ const label = getAgentDisplayName(agent);
440
+ if (mcpScope !== "none" && !mcpHits.some((hit) => hit.agent === agent)) {
476
441
  const mcpS = spinner();
477
- mcpS.start(`Installing Neon MCP server for ${editor}...`);
478
- try {
479
- await execa("npx", mcpArgs, {
480
- stdio: "pipe",
481
- timeout: 6e4
482
- });
483
- mcpS.stop(dim(`Neon MCP server configured for ${editor} (${mcpScope}) ✓`));
484
- } catch (err) {
485
- const msg = err instanceof Error ? err.message : "Unknown error";
486
- mcpS.stop(`Failed to configure MCP server for ${editor}`);
487
- log.error(msg);
442
+ mcpS.start(`Installing Neon MCP server for ${label}...`);
443
+ const installed = installNeonMcpServer({
444
+ agent,
445
+ scope: mcpScope === "project" ? "project" : "global",
446
+ cwd: process.cwd()
447
+ });
448
+ if (installed.ok) mcpS.stop(dim(`Neon MCP server configured for ${label} (${mcpScope}) ✓`));
449
+ else if (installed.unsupported) {
450
+ mcpS.stop(`Could not write MCP config for ${label}`);
451
+ log.warn(installed.error);
452
+ } else {
453
+ mcpS.stop(`Failed to configure MCP server for ${label}`);
454
+ log.error(installed.error);
488
455
  }
489
456
  }
490
- if (needsSkills) await installAgentSkills([editor], {
457
+ if (selectedTemplate === null && getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview)) await installAgentSkills([agent], {
491
458
  scope: skillsScope,
492
459
  preview: options.preview
493
460
  });
494
- if (doInstallExtension && usesExtension(editor)) {
495
- const extS = spinner();
496
- extS.start(`Installing Neon extension for ${editor}...`);
497
- if (await installExtension(editor)) extS.stop(dim(`Neon extension installed for ${editor} ✓`));
498
- else extS.stop(`Extension install failed — install manually from the extensions panel.`);
499
- }
461
+ }
462
+ if (doInstallExtension) for (const editor of vscodeEditors) {
463
+ const extS = spinner();
464
+ extS.start(`Installing Neon extension for ${editor}...`);
465
+ if (await installExtension(editor)) extS.stop(dim(`Neon extension installed for ${editor} ✓`));
466
+ else extS.stop(`Extension install failed — install manually from the extensions panel.`);
500
467
  }
501
468
  }
502
469
  const ide = detectIde();
@@ -524,16 +491,37 @@ async function interactiveInitInner(options) {
524
491
  log.message(wordWrap(promptText, cols).split("\n").map((line) => bold(neonGreenFn(line))).join("\n"));
525
492
  outro(dim("Have feedback? Email us at feedback@neon.tech"));
526
493
  }
527
- function agentIdToEditor(agentId) {
528
- switch (agentId) {
529
- case "cursor": return "Cursor";
530
- case "vscode": return "VS Code";
531
- case "claude-code": return "Claude CLI";
532
- case "windsurf": return null;
533
- case "codex": return "Codex";
534
- case "cline": return "Cline";
535
- default: return null;
494
+ function extensionEditorForAgent(agent) {
495
+ if (agent === "cursor") return "Cursor";
496
+ if (agent === "vscode") return "VS Code";
497
+ const ide = detectIde();
498
+ if (ide === "Cursor" || ide === "VS Code") return ide;
499
+ return null;
500
+ }
501
+ function extensionEditorsFor(selected) {
502
+ const out = [];
503
+ if (selected.includes("cursor")) out.push("Cursor");
504
+ if (selected.includes("vscode")) out.push("VS Code");
505
+ const ide = detectIde();
506
+ if ((ide === "Cursor" || ide === "VS Code") && !out.includes(ide)) out.push(ide);
507
+ return out;
508
+ }
509
+ async function pickAgents() {
510
+ const options = mcpPickerOptions();
511
+ const installed = await detectInstalledAgents();
512
+ const response = await multiselect({
513
+ message: "Which agent(s) would you like to configure?",
514
+ options,
515
+ initialValues: installed.filter((id) => options.some((option) => option.value === id)),
516
+ required: true
517
+ });
518
+ if (isCancel(response)) return null;
519
+ const selected = response;
520
+ if (selected.length === 0) {
521
+ log.warn("No agents selected.");
522
+ return null;
536
523
  }
524
+ return selected;
537
525
  }
538
526
  //#endregion
539
527
  export { interactiveInit };
@@ -104,7 +104,7 @@ function isLocalDevSymlink() {
104
104
  * Ensures neonctl is globally installed and up to date.
105
105
  * Uses the same package manager that invoked the init command.
106
106
  */
107
- async function ensureNeonctl() {
107
+ async function ensureNeonctl(onProgress) {
108
108
  if (isLocalDevSymlink()) return {
109
109
  status: "already_current",
110
110
  version: await getNeonCliVersion() ?? "dev"
@@ -116,11 +116,14 @@ async function ensureNeonctl() {
116
116
  };
117
117
  const pm = resolveInvokingPackageManager();
118
118
  const install = globalInstallCommand(pm, "neon");
119
+ const action = check.installed ? "updating" : "installing";
119
120
  if (!install) return {
120
121
  status: "failed",
121
- error: "Could not install the Neon CLI: this machine has no package manager that can perform a global install. npm, pnpm and bun are not on PATH (yarn Berry has no global install). Install npm, pnpm or bun, then run this again."
122
+ action,
123
+ error: `Could not ${action === "updating" ? "update" : "install"} the Neon CLI: this machine has no package manager that can perform a global install. npm, pnpm and bun are not on PATH (yarn Berry has no global install). Install npm, pnpm or bun, then run this again.`
122
124
  };
123
125
  const { command, args } = install;
126
+ onProgress?.(action);
124
127
  try {
125
128
  await execa(command, args, {
126
129
  stdio: "pipe",
@@ -134,6 +137,7 @@ async function ensureNeonctl() {
134
137
  } catch (err) {
135
138
  return {
136
139
  status: "failed",
140
+ action,
137
141
  error: err instanceof Error ? err.message : "Unknown error"
138
142
  };
139
143
  }
@@ -1,5 +1,7 @@
1
+ import { supportsSkills, tryResolveAddMcpAgentId } from "./agents.js";
1
2
  import { isAuthenticated } from "./auth.js";
2
3
  import { inspectProject } from "./inspect.js";
4
+ import { getSkillList, missingSkillsForAgent, skillsInstalledForAgent } from "./skills.js";
3
5
  import { handleAuthPhase } from "./phases/auth.js";
4
6
  import { handleGettingStartedPhase } from "./phases/getting_started.js";
5
7
  import { handleMigrationsPhase } from "./phases/migrations.js";
@@ -62,9 +64,19 @@ async function orchestrate(options) {
62
64
  }
63
65
  ]);
64
66
  const hasApp = options.preview ? inspection.hasApp === true : true;
67
+ const requestedAgent = options.agent ? tryResolveAddMcpAgentId(options.agent) : void 0;
65
68
  let skillsInstalled = inspection.skillsInstalled;
66
- if (options.preview && skillsInstalled && inspection.skillsScope) {
67
- const { getSkillList } = await import("./skills.js");
69
+ if (options.preview && requestedAgent && supportsSkills(requestedAgent)) {
70
+ const missing = missingSkillsForAgent(requestedAgent, {
71
+ cwd,
72
+ preview: true
73
+ });
74
+ const required = getSkillList(true);
75
+ if (missing.length > 0) {
76
+ skillsInstalled = false;
77
+ if (missing.length < required.length) inspection.skillsScope = `${inspection.skillsScope && !String(inspection.skillsScope).includes("partial") ? inspection.skillsScope : "project"}-partial`;
78
+ }
79
+ } else if (options.preview && skillsInstalled && inspection.skillsScope) {
68
80
  const previewSkills = getSkillList(true);
69
81
  const { existsSync: exists } = await import("node:fs");
70
82
  const { resolve: resolvePath } = await import("node:path");
@@ -83,7 +95,9 @@ async function orchestrate(options) {
83
95
  inspection.skillsScope = `${inspection.skillsScope}-partial`;
84
96
  }
85
97
  }
86
- const toolingInstalled = inspection.mcpConfigured && skillsInstalled;
98
+ const mcpForThisAgent = requestedAgent ? (inspection.mcpAgents ?? []).some((hit) => hit.agent === requestedAgent) : inspection.mcpConfigured === true;
99
+ const skillsReady = requestedAgent && !supportsSkills(requestedAgent) ? true : requestedAgent ? skillsInstalledForAgent(requestedAgent, cwd, options.preview) : skillsInstalled === true;
100
+ const toolingInstalled = mcpForThisAgent && skillsReady;
87
101
  const hasNeonConnection = inspection.connectionString === true;
88
102
  if (!hasApp || !toolingInstalled) {
89
103
  cleanupInitState(resolve(cwd, ".neon"));
@@ -91,9 +105,9 @@ async function orchestrate(options) {
91
105
  agent: options.agent,
92
106
  preview: options.preview,
93
107
  hasApp,
94
- mcpConfigured: inspection.mcpConfigured ?? null,
108
+ mcpConfigured: requestedAgent ? mcpForThisAgent : inspection.mcpConfigured ?? null,
95
109
  mcpScope: inspection.mcpScope || void 0,
96
- skillsInstalled: skillsInstalled ?? null,
110
+ skillsInstalled: requestedAgent ? skillsReady : skillsInstalled ?? null,
97
111
  skillsScope: inspection.skillsScope || void 0
98
112
  });
99
113
  }
@@ -137,7 +151,7 @@ async function orchestrate(options) {
137
151
  status: "complete",
138
152
  nextAction: {
139
153
  type: "complete",
140
- message: "Neon setup is complete. Your database is configured and your agent has the Neon MCP server and skills available."
154
+ message: requestedAgent && !supportsSkills(requestedAgent) ? "Neon setup is complete. Your database is configured and your agent has the Neon MCP server available." : "Neon setup is complete. Your database is configured and your agent has the Neon MCP server and skills available."
141
155
  }
142
156
  };
143
157
  }
@@ -1,6 +1,20 @@
1
- import { resolveAddMcpAgentId } from "../agents.js";
1
+ import { agentSupportsHttpMcp, agentSupportsProjectMcp, getAgentDisplayName, getSkillsAgentName, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../agents.js";
2
2
  import { isAuthenticated } from "../auth.js";
3
+ import { installNeonMcpServer } from "../install_mcp.js";
3
4
  //#region src/init/phases/mcp.ts
5
+ function skillsFollowUp(agent) {
6
+ if (agent && !getSkillsAgentName(agent)) return agent ? [
7
+ "--agent",
8
+ agent,
9
+ "--json"
10
+ ] : ["--json"];
11
+ return [
12
+ "skills",
13
+ "--json",
14
+ ...agent ? ["--agent", agent] : [],
15
+ "--install"
16
+ ];
17
+ }
4
18
  async function handleMcpPhase(options) {
5
19
  const agentArgs = options.agent ? [
6
20
  "--agent",
@@ -40,36 +54,53 @@ async function handleMcpPhase(options) {
40
54
  };
41
55
  const scope = options.scope ?? "global";
42
56
  const mcpAgentId = resolveAddMcpAgentId(options.agent ?? "claude-code");
43
- const isCursor = mcpAgentId === "cursor" || options.agent?.toLowerCase() === "cursor";
44
- const isClaudeCode = mcpAgentId === "claude-code" || options.agent?.toLowerCase() === "claude-code";
45
- const installCmd = [
46
- "npx -y add-mcp https://mcp.neon.tech/mcp",
47
- scope === "global" ? "-g" : "",
48
- "-n Neon",
49
- "-y",
50
- `-a ${mcpAgentId}`
51
- ].filter(Boolean).join(" ");
52
- let enableNote = "";
53
- if (isCursor && scope === "project") enableNote = " Note: Cursor disables project-level MCP servers by default — after installation, open Cursor Settings > MCP and toggle the \"Neon\" server on.";
54
- else if (isClaudeCode) enableNote = " Note: Claude Code requires approval for newly added MCP servers. When prompted, approve the \"Neon\" server to enable it.";
55
- return {
56
- phase: "tooling",
57
- status: "installing",
58
- nextAction: {
59
- type: "run_command",
60
- command: installCmd,
61
- description: `Installing Neon MCP server (${scope} scope) for ${mcpAgentId}.${enableNote}`,
62
- timeout: 6e4,
63
- onSuccess: {
64
- type: "run_neon_init",
65
- args: [
66
- "skills",
67
- "--json",
68
- ...options.agent ? ["--agent", options.agent] : [],
69
- "--install"
70
- ]
71
- },
72
- onFailure: { other: {
57
+ const installed = installNeonMcpServer({
58
+ agent: mcpAgentId,
59
+ scope,
60
+ cwd: process.cwd()
61
+ });
62
+ if (!installed.ok) {
63
+ if (installed.unsupported) {
64
+ if (scope === "project" && !agentSupportsProjectMcp(mcpAgentId) && agentSupportsHttpMcp(mcpAgentId)) return {
65
+ phase: "tooling",
66
+ status: "unsupported",
67
+ error: installed.error,
68
+ nextAction: {
69
+ type: "ask_user",
70
+ question: `${getAgentDisplayName(mcpAgentId)} does not support project-level MCP. Install the Neon MCP server globally instead?`,
71
+ options: [{
72
+ value: "global",
73
+ label: "Install globally"
74
+ }, {
75
+ value: "skip",
76
+ label: "Skip MCP install"
77
+ }],
78
+ responseMapping: {
79
+ global: { args: [
80
+ "mcp",
81
+ "--json",
82
+ ...options.agent ? ["--agent", options.agent] : [],
83
+ "--install"
84
+ ] },
85
+ skip: { args: skillsFollowUp(options.agent) }
86
+ }
87
+ }
88
+ };
89
+ return {
90
+ phase: "tooling",
91
+ status: "unsupported",
92
+ error: installed.error,
93
+ nextAction: {
94
+ type: "run_neon_init",
95
+ args: skillsFollowUp(options.agent)
96
+ }
97
+ };
98
+ }
99
+ return {
100
+ phase: "tooling",
101
+ status: "failed",
102
+ error: installed.error,
103
+ nextAction: {
73
104
  type: "ask_user",
74
105
  question: "Failed to install the Neon MCP server automatically. Would you like to try again or configure it manually?",
75
106
  options: [{
@@ -84,12 +115,28 @@ async function handleMcpPhase(options) {
84
115
  "mcp",
85
116
  "--json",
86
117
  ...options.agent ? ["--agent", options.agent] : [],
87
- "--install"
118
+ "--install",
119
+ ...scope === "project" ? ["--scope", "project"] : []
88
120
  ] },
89
121
  manual: { args: agentArgs }
90
122
  }
91
- } }
92
- }
123
+ }
124
+ };
125
+ }
126
+ const isCursor = mcpAgentId === "cursor" || options.agent?.toLowerCase() === "cursor";
127
+ const isClaudeCode = mcpAgentId === "claude-code" || options.agent?.toLowerCase() === "claude-code";
128
+ let enableNote = "";
129
+ if (isCursor && scope === "project") enableNote = " Cursor disables project-level MCP servers by default — open Cursor Settings > MCP and toggle the \"Neon\" server on.";
130
+ else if (isClaudeCode) enableNote = " Claude Code requires approval for newly added MCP servers. When prompted, approve the \"Neon\" server to enable it.";
131
+ return {
132
+ phase: "tooling",
133
+ status: "installed",
134
+ path: installed.path,
135
+ nextAction: {
136
+ type: "run_neon_init",
137
+ args: skillsFollowUp(options.agent)
138
+ },
139
+ message: `Installed Neon MCP server (${scope} scope) for ${mcpAgentId}.${enableNote}`
93
140
  };
94
141
  }
95
142
  if (options.mcpConfigured === true) return {
@@ -97,12 +144,7 @@ async function handleMcpPhase(options) {
97
144
  status: "mcp_configured",
98
145
  nextAction: {
99
146
  type: "run_neon_init",
100
- args: [
101
- "skills",
102
- "--json",
103
- ...options.agent ? ["--agent", options.agent] : [],
104
- "--install"
105
- ]
147
+ args: skillsFollowUp(options.agent)
106
148
  }
107
149
  };
108
150
  if (options.mcpConfigured === false) return {
@@ -116,10 +158,14 @@ async function handleMcpPhase(options) {
116
158
  value: "defaults",
117
159
  label: "Yes, install with default settings"
118
160
  },
119
- {
120
- value: "project_scope",
121
- label: "Yes, install for this project only"
122
- },
161
+ ...(() => {
162
+ const known = options.agent ? tryResolveAddMcpAgentId(options.agent) : void 0;
163
+ if (known && !agentSupportsProjectMcp(known)) return [];
164
+ return [{
165
+ value: "project_scope",
166
+ label: "Yes, install for this project only"
167
+ }];
168
+ })(),
123
169
  {
124
170
  value: "skip",
125
171
  label: "Skip for now"
@@ -141,12 +187,7 @@ async function handleMcpPhase(options) {
141
187
  "--scope",
142
188
  "project"
143
189
  ] },
144
- skip: { args: [
145
- "skills",
146
- "--json",
147
- ...options.agent ? ["--agent", options.agent] : [],
148
- "--install"
149
- ] }
190
+ skip: { args: skillsFollowUp(options.agent) }
150
191
  }
151
192
  }
152
193
  };