neon 4.0.0 → 4.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/README.md CHANGED
@@ -62,7 +62,7 @@ Alternatively, you can authenticate a connection with a Neon API key using the `
62
62
  neon projects list --api-key <neon_api_key>
63
63
  ```
64
64
 
65
- For information about obtaining an Neon API key, see [Authentication](https://api-docs.neon.tech/reference/authentication), in the _Neon API Reference_.
65
+ For information about obtaining an Neon API key, see [Authentication](https://neon.com/docs/reference/api/get-started), in the _Neon API Reference_.
66
66
 
67
67
  ## Project and branch creation
68
68
 
@@ -722,6 +722,43 @@ That message reports where parsing stopped and nothing more. `--data` carries wh
722
722
  | `--preview` | Enable preview features (scaffolding a project from a template) |
723
723
  | `--profile <name>` | Run as that stored account. See [Which credential an invocation uses](#which-credential-an-invocation-uses). |
724
724
 
725
+ ## Install the Neon MCP server (`mcp`)
726
+
727
+ `neon mcp` writes the hosted Neon MCP server (`https://mcp.neon.tech/mcp`) into coding-agent config files.
728
+
729
+ ```bash
730
+ # Interactive: global or project, then agents, then API key or OAuth, then confirm.
731
+ $ neon mcp
732
+
733
+ # Skip prompts. Global config, every globally detected agent, minted API key.
734
+ $ neon mcp -y
735
+
736
+ # OAuth: no API key minted. The agent prompts for Neon sign-in on first use.
737
+ $ neon mcp --oauth
738
+
739
+ # Project-level config. A minted key is still account-wide.
740
+ $ neon mcp --project
741
+
742
+ $ neon mcp --agent cursor --agent claude-code
743
+
744
+ # Hide write tools. Does not change the minted key.
745
+ $ neon mcp --read-only
746
+
747
+ # Pin MCP tools to one project. Does not change the minted key.
748
+ $ neon mcp --project-id <project-id>
749
+
750
+ # Limit which tool categories are visible.
751
+ $ neon mcp --category querying --category schema
752
+ ```
753
+
754
+ On a TTY the command asks for config location (global is the default), then agents, then API key vs OAuth, then a summary to confirm before it writes. Detected agents start selected: globally installed agents or project-folder markers such as `.cursor` when the install is project.
755
+
756
+ `-y` skips those questions. `--project`, `--oauth` and `--agent` skip the question they answer and still apply with `-y`. `--read-only` and `--category` are flags only and are never prompted. A linked project-folder install asks whether to pin MCP tools to that `.neon` project (`?projectId=`). An unlinked project folder does not ask. Global installs never add that param unless you pass `--project-id`. Without a TTY, pass `-y` to mint into every detected agent, `--agent` to name the agents or `--oauth` to write the URL only.
757
+
758
+ `--agent` names: `antigravity`, `cline`, `cline-cli`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `goose`, `github-copilot-cli`, `grok-build`, `mcporter`, `opencode`, `vscode`, `windsurf`, `zed`. Project installs drop `antigravity`, `cline`, `cline-cli`, `goose` and `windsurf`. `claude-desktop` is a known name that is then skipped.
759
+
760
+ The default mints an account-wide API key (or reuses the Bearer already configured for Neon at `https://mcp.neon.tech/mcp`) and writes it into each selected agent's config. That key reaches everything the account can, in every organization. Revoke it with `neon api-keys revoke <id>`. `--oauth` writes the URL with no `Authorization` header; the agent signs in on first use. `--project` writes into the project config (`.cursor/mcp.json` and similar). `--read-only` adds `?readonly=true`. `--project-id` adds `?projectId=`. `--category` adds `?category=` (repeatable or comma-separated: `projects`, `branches`, `schema`, `querying`, `neon_auth`, `data_api`, `observability`, `docs`). Those query params restrict which MCP tools the server exposes; they do not change what the minted key can do.
761
+
725
762
  ## Snapshots (`snapshots`)
726
763
 
727
764
  `neon snapshots` (alias `neon snapshot`) manages **snapshots** — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
@@ -766,6 +803,15 @@ neon snapshots schedule set --branch main --schedule '[{"frequency":"weekly","da
766
803
 
767
804
  All sub-commands honor the [global options](#global-options), including `--output json|yaml|table`.
768
805
 
806
+ ## Database diagnostics (`inspect`)
807
+
808
+ `neon inspect db stalled-queries` takes a read-only snapshot of active queries that have run for more than 30 seconds and groups parallel workers with their leader. Table output shows duration, wait event, blocking pids, role, query group, and query. `--output json` adds timestamps, query IDs, pids, database, and the rest of the row. A blocking pid can belong to an idle-in-transaction backend this command does not list; `neon inspect db locks` shows lock holders.
809
+
810
+ ```bash
811
+ neon inspect db stalled-queries
812
+ neon inspect db stalled-queries --output json
813
+ ```
814
+
769
815
  ## Logs (`logs`)
770
816
 
771
817
  `neon logs` reads the log records the services on a branch emit — Neon Functions, object storage, and Postgres computes. **Logs require Neon Platform Beta and are currently available only for projects in `aws-us-east-2`.**
@@ -1078,6 +1124,7 @@ Id Name Project Created At Last Used At Last
1078
1124
  | [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
1079
1125
  | [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
1080
1126
  | logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
1127
+ | inspect | `db stalled-queries` | Inspect Postgres diagnostics |
1081
1128
  | snapshots | `list`, `get`, `create`, `update`, `delete`, `restore`, `finalize`, `schedule get`, `schedule set` | Manage snapshots |
1082
1129
  | [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
1083
1130
  | [psql](https://neon.com/docs/reference/cli-psql) | | Connect to a database via psql |
@@ -1091,6 +1138,7 @@ Id Name Project Created At Last Used At Last
1091
1138
  | deploy | | Alias for `config apply` |
1092
1139
  | bootstrap | | Scaffold a project from a template |
1093
1140
  | init | | Set up a project for a coding agent |
1141
+ | mcp | | Install the Neon MCP server |
1094
1142
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
1095
1143
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
1096
1144
 
@@ -1141,7 +1189,7 @@ Global options are supported with any Neon CLI command.
1141
1189
 
1142
1190
  - <a id="api-key"></a>`--api-key`
1143
1191
 
1144
- Specifies your Neon API key. You can authenticate using a Neon API key when running a Neon CLI command instead of using `neon auth`. For information about obtaining an Neon API key, see [Authentication](https://api-docs.neon.tech/reference/authentication), in the _Neon API Reference_.
1192
+ Specifies your Neon API key. You can authenticate using a Neon API key when running a Neon CLI command instead of using `neon auth`. For information about obtaining an Neon API key, see [Authentication](https://neon.com/docs/reference/api/get-started), in the _Neon API Reference_.
1145
1193
 
1146
1194
  ```bash
1147
1195
  neon <command> --api-key <neon_api_key>
@@ -6,7 +6,7 @@ import { log } from "../log.js";
6
6
  import { getApiClient } from "../api.js";
7
7
  import { o as isOwnedCredentialPath } from "../_chunks/paths-g3DRqJiD.js";
8
8
  import { setAuthContext } from "../auth_context.js";
9
- import { isConfigInit, isCurrentBranchProbe, isProfileCommand } from "../context.js";
9
+ import { isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand } from "../context.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { auth, refreshToken } from "../auth.js";
12
12
  import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
@@ -215,10 +215,12 @@ const ensureAuth = async (props) => {
215
215
  if (isCurrentBranchProbe(props)) return;
216
216
  if (isConfigInit(props)) return;
217
217
  if (isProfileCommand(props)) return;
218
+ if (isMcpOauth(props)) return;
218
219
  if (props._[0] === "open") return;
219
220
  const isLocalDev = props._[0] === "dev";
220
221
  const isBootstrap = props._[0] === "bootstrap";
221
222
  const isInit = props._[0] === "init";
223
+ const isMcp = isMcpCommand(props);
222
224
  if (props._[0] === "auth") {
223
225
  props.apiClient = getApiClient({
224
226
  apiKey: props.apiKey,
@@ -298,6 +300,10 @@ const ensureAuth = async (props) => {
298
300
  log.debug("init: skipping global auth; init manages its own auth flow");
299
301
  return;
300
302
  }
303
+ if (isMcp) {
304
+ log.debug("mcp: no usable credentials; minting requires auth or --oauth");
305
+ return;
306
+ }
301
307
  const apiKey = await authFlow({
302
308
  ...props,
303
309
  profile: selection.profile
@@ -20,6 +20,7 @@ import { t as init_exports } from "./init.js";
20
20
  import { t as inspect_exports } from "./inspect.js";
21
21
  import { t as ip_allow_exports } from "./ip_allow.js";
22
22
  import { t as logs_exports } from "./logs.js";
23
+ import { t as mcp_exports } from "./mcp.js";
23
24
  import { t as neon_auth_exports } from "./neon_auth.js";
24
25
  import { t as open_exports } from "./open.js";
25
26
  import { t as operations_exports } from "./operations.js";
@@ -58,6 +59,7 @@ var commands_default = [
58
59
  link_exports,
59
60
  open_exports,
60
61
  init_exports,
62
+ mcp_exports,
61
63
  data_api_exports,
62
64
  functions_exports,
63
65
  dev_exports,
@@ -0,0 +1,212 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { log } from "../log.js";
3
+ import { readContextFile } from "../context.js";
4
+ import { getCliName } from "../utils/cli_name.js";
5
+ import { writer } from "../writer.js";
6
+ import { noPassthrough, single } from "../utils/flags.js";
7
+ import { existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
8
+ import { mintMcpApiKey, withdrawMintedKey } from "../mcp/mint.js";
9
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
10
+ import { resolveInstallTargets } from "../mcp/targets.js";
11
+ import { confirmMcpInstall } from "../mcp/wizard.js";
12
+ import { resolveMcpPlan } from "../mcp/plan.js";
13
+ //#region src/commands/mcp.ts
14
+ var mcp_exports = /* @__PURE__ */ __exportAll({
15
+ builder: () => builder,
16
+ command: () => "mcp",
17
+ describe: () => describe,
18
+ handler: () => handler
19
+ });
20
+ const command = "mcp";
21
+ const describe = "Install the Neon MCP server into coding agents";
22
+ const builder = (argv) => argv.usage("$0 mcp [options]").options({
23
+ oauth: {
24
+ type: "boolean",
25
+ default: false,
26
+ describe: "Write the server URL only. The agent prompts for Neon sign-in on first use. No CLI login, no API key minted. Skips the auth question"
27
+ },
28
+ project: {
29
+ type: "boolean",
30
+ default: false,
31
+ describe: "Write project-level MCP config. Skips the config-location question. Does not change the minted key"
32
+ },
33
+ yes: {
34
+ alias: "y",
35
+ type: "boolean",
36
+ default: false,
37
+ describe: "Skip prompts. Defaults to global config, every detected agent and a minted account-wide API key. --project, --oauth, --agent, --read-only, --project-id and --category still apply"
38
+ },
39
+ agent: {
40
+ alias: "a",
41
+ type: "array",
42
+ string: true,
43
+ describe: "Coding agent to install into (repeatable). Skips the agent picker",
44
+ coerce: (value) => {
45
+ if (value === void 0) return [];
46
+ const list = Array.isArray(value) ? value : [value];
47
+ if (list.length === 0) throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
48
+ return list.map((item) => {
49
+ if (typeof item !== "string" || item.trim() === "") throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
50
+ return item;
51
+ });
52
+ }
53
+ },
54
+ "read-only": {
55
+ alias: "readonly",
56
+ type: "boolean",
57
+ default: false,
58
+ describe: "Restrict MCP tools to read-only (?readonly=true). Not prompted. Does not change the minted key"
59
+ },
60
+ "project-id": {
61
+ type: "string",
62
+ describe: "Pin MCP tools to one Neon project (?projectId=). Does not change the minted key. Interactive asks only for a linked project-folder install",
63
+ coerce: single("project-id")
64
+ },
65
+ category: {
66
+ type: "array",
67
+ string: true,
68
+ describe: "MCP tool category (repeatable or comma-separated). Default: all",
69
+ coerce: (value) => {
70
+ if (value === void 0) return [];
71
+ const list = Array.isArray(value) ? value : [value];
72
+ if (list.length === 0) throw new Error("--category needs a value. Pass one, or omit the flag entirely.");
73
+ const parts = [];
74
+ for (const item of list) {
75
+ if (typeof item !== "string" || item.trim() === "") throw new Error("--category needs a value. Pass one, or omit the flag entirely.");
76
+ for (const part of item.split(",")) {
77
+ const trimmed = part.trim();
78
+ if (trimmed === "") throw new Error("--category needs a value. Pass one, or omit the flag entirely.");
79
+ parts.push(trimmed);
80
+ }
81
+ }
82
+ return parseMcpCategories(parts);
83
+ }
84
+ }
85
+ }).example("$0 mcp", "Interactive: config location, agents, auth, then confirm").example("$0 mcp -y", "Global config, detected agents, minted API key").example("$0 mcp --oauth", "Install with OAuth; the agent signs in on first use").example("$0 mcp --project", "Write project-level config").example("$0 mcp --agent cursor --agent claude-code", "Install into specific agents").example("$0 mcp --read-only", "Hide write tools via ?readonly=true").example("$0 mcp --project-id <id>", "Pin tools to one project via ?projectId=").example("$0 mcp --category querying --category schema", "Limit tools to those categories").strict().check(noPassthrough("mcp"));
86
+ const handler = async (props) => {
87
+ const cwd = process.cwd();
88
+ const interactive = canPickAgentsInteractively() && props.yes !== true;
89
+ const linkedProjectId = readContextFile(props.contextFile).projectId;
90
+ const plan = await resolveMcpPlan({
91
+ project: props.project === true,
92
+ oauth: props.oauth === true,
93
+ agents: props.agent ?? [],
94
+ yes: props.yes === true,
95
+ cwd,
96
+ interactive,
97
+ readOnly: props.readOnly === true,
98
+ projectId: props.projectId,
99
+ categories: props.category ?? [],
100
+ linkedProjectId
101
+ });
102
+ const { install, skipped } = resolveInstallTargets({
103
+ agents: plan.agents,
104
+ scope: plan.scope
105
+ });
106
+ const url = neonMcpUrl({
107
+ readOnly: plan.readOnly,
108
+ projectId: plan.urlProjectId,
109
+ categories: plan.categories
110
+ });
111
+ if (plan.scope === "project" && plan.auth === "api-key") {
112
+ const tracked = trackedProjectMcpConfig({
113
+ agents: install,
114
+ cwd
115
+ });
116
+ if (tracked) throw new Error(`${tracked} is tracked by git. Untrack it before writing an API key, or pass --oauth.`);
117
+ }
118
+ const existing = plan.auth === "api-key" ? existingNeonApiKey({
119
+ agents: install,
120
+ scope: plan.scope,
121
+ cwd
122
+ }) : void 0;
123
+ if (plan.auth === "api-key" && !existing) {
124
+ if (!props.apiClient || !props.apiKey) throw new Error(`Authentication required. Run \`${getCliName()} auth\`, pass --api-key or use --oauth to install without a Neon credential.`);
125
+ if (!canPickAgentsInteractively() && props.yes !== true && (props.agent ?? []).length === 0) throw new Error("No interactive terminal. Pass -y to mint into every detected agent, --agent <name> to name them or --oauth to install without minting.");
126
+ }
127
+ if (interactive) {
128
+ if (!await confirmMcpInstall({
129
+ scope: plan.scope,
130
+ install,
131
+ skipped,
132
+ auth: plan.auth,
133
+ reuse: existing !== void 0,
134
+ url
135
+ })) {
136
+ log.info("Aborted. Nothing was written.");
137
+ return;
138
+ }
139
+ }
140
+ let auth;
141
+ let minted;
142
+ if (plan.auth === "oauth") auth = { kind: "oauth" };
143
+ else if (existing) {
144
+ auth = {
145
+ kind: "api-key",
146
+ apiKey: existing
147
+ };
148
+ log.info("Reusing the API key already configured for the Neon MCP server.");
149
+ } else {
150
+ minted = await mintMcpApiKey({ apiClient: props.apiClient });
151
+ auth = {
152
+ kind: "api-key",
153
+ apiKey: minted.key
154
+ };
155
+ log.warning("This key reaches everything your account can, in every organization.");
156
+ }
157
+ const rows = [];
158
+ let successes = 0;
159
+ const failedAgents = [];
160
+ for (const agent of install) {
161
+ const result = installNeonMcpServer({
162
+ agent,
163
+ scope: plan.scope,
164
+ cwd,
165
+ auth,
166
+ url
167
+ });
168
+ if (result.ok) {
169
+ successes += 1;
170
+ rows.push({
171
+ agent,
172
+ status: "installed"
173
+ });
174
+ log.info("Wrote %s", result.path);
175
+ } else {
176
+ rows.push({
177
+ agent,
178
+ status: result.unsupported ? "skipped" : "failed",
179
+ error: result.error
180
+ });
181
+ if (!result.unsupported) failedAgents.push(agent);
182
+ }
183
+ }
184
+ for (const row of skipped) rows.push({
185
+ agent: row.agent,
186
+ status: "skipped",
187
+ error: row.error
188
+ });
189
+ if (successes === 0) {
190
+ if (minted && props.apiClient) {
191
+ const withdrawn = await withdrawMintedKey(props.apiClient, minted);
192
+ throw new Error(withdrawn ? "Failed to write Neon MCP config to any agent. The minted API key has been revoked." : `Failed to write Neon MCP config to any agent. The minted API key could NOT be revoked. Remove it with \`${getCliName()} api-keys revoke ${minted.id}\`.`);
193
+ }
194
+ throw new Error("Failed to write Neon MCP config to any agent.");
195
+ }
196
+ log.info("URL: %s", url);
197
+ const out = writer(props);
198
+ out.write(rows, {
199
+ fields: [
200
+ "agent",
201
+ "status",
202
+ "error"
203
+ ],
204
+ title: "MCP"
205
+ });
206
+ out.end();
207
+ if (minted) log.info("Minted API key %s (id %d, account). Revoke with: %s", minted.name, minted.id, `${getCliName()} api-keys revoke ${minted.id}`);
208
+ if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
209
+ if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
210
+ };
211
+ //#endregion
212
+ export { builder, command, describe, handler, mcp_exports as t };
package/dist/context.js CHANGED
@@ -50,6 +50,27 @@ const isProfileCommand = (args) => args._[0] === "profile" || args._[0] === "pro
50
50
  * far a credential reaches must come from an explicit flag, never from `.neon`.
51
51
  */
52
52
  const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "api-key";
53
+ const isMcpCommand = (args) => args._[0] === "mcp";
54
+ /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
55
+ const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
56
+ const OAUTH_FALSE = /* @__PURE__ */ new Set([
57
+ "false",
58
+ "0",
59
+ "no"
60
+ ]);
61
+ function argvEnablesMcpOauth(argv) {
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const arg = argv[i];
64
+ if (arg === "--oauth=true" || arg === "--oauth=1") return true;
65
+ if (arg === "--oauth=false" || arg === "--oauth=0") continue;
66
+ if (arg === "--oauth") {
67
+ const next = argv[i + 1];
68
+ if (next !== void 0 && OAUTH_FALSE.has(next.toLowerCase())) continue;
69
+ return true;
70
+ }
71
+ }
72
+ return false;
73
+ }
53
74
  const CONTEXT_FILE = ".neon";
54
75
  const GITIGNORE_FILE = ".gitignore";
55
76
  const canAccessFile = (file) => {
@@ -108,6 +129,7 @@ const enrichFromContext = (args) => {
108
129
  if (args._[0] === "link" || args._[0] === "set-context") return;
109
130
  if (isApiKeysCommand(args)) return;
110
131
  if (isProfileCommand(args)) return;
132
+ if (isMcpCommand(args)) return;
111
133
  const context = readContextFile(args.contextFile);
112
134
  if (!args.orgId) args.orgId = context.orgId;
113
135
  if (!args.projectId) args.projectId = context.projectId;
@@ -213,4 +235,4 @@ const globToRegExp = (pattern) => {
213
235
  return new RegExp(`^${source}$`);
214
236
  };
215
237
  //#endregion
216
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isProfileCommand, readContextFile, setContext, updateContextFile, walkContextFile };
238
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand, readContextFile, setContext, updateContextFile, walkContextFile };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ const NO_SUBCOMMANDS_VERBS = [
29
29
  "link",
30
30
  "open",
31
31
  "init",
32
+ "mcp",
32
33
  "dev",
33
34
  "deploy",
34
35
  "diff",
@@ -1,32 +1,6 @@
1
- import { agents, detectGlobalAgents, getAgentTypes } from "add-mcp";
1
+ import { agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, listMcpAgentIds, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
+ import { agents } from "add-mcp";
2
3
  //#region src/init/agents.ts
3
- const AGENT_ALIASES = {
4
- cursor: "cursor",
5
- copilot: "vscode",
6
- "github-copilot": "vscode",
7
- "vs-code": "vscode",
8
- vscode: "vscode",
9
- claude: "claude-code",
10
- "claude-code": "claude-code",
11
- "claude-desktop": "claude-desktop",
12
- codex: "codex",
13
- opencode: "opencode",
14
- antigravity: "antigravity",
15
- cline: "cline",
16
- "cline-cli": "cline-cli",
17
- "cline-vscode": "cline",
18
- "gemini-cli": "gemini-cli",
19
- gemini: "gemini-cli",
20
- goose: "goose",
21
- windsurf: "windsurf",
22
- codeium: "windsurf",
23
- cascade: "windsurf",
24
- "github-copilot-cli": "github-copilot-cli",
25
- mcporter: "mcporter",
26
- zed: "zed",
27
- grok: "grok-build",
28
- "grok-build": "grok-build"
29
- };
30
4
  const SKILLS_AGENT_BY_TYPE = {
31
5
  cursor: "cursor",
32
6
  vscode: "github-copilot",
@@ -44,26 +18,6 @@ const SKILLS_AGENT_BY_TYPE = {
44
18
  zed: "zed",
45
19
  "grok-build": "grok"
46
20
  };
47
- function listMcpAgentIds() {
48
- return getAgentTypes();
49
- }
50
- function getAgentDisplayName(id) {
51
- return agents[id].displayName;
52
- }
53
- function agentSupportsProjectMcp(id) {
54
- return Boolean(agents[id].localConfigPath);
55
- }
56
- function agentSupportsHttpMcp(id) {
57
- return agents[id].supportedTransports.includes("http");
58
- }
59
- function tryResolveAddMcpAgentId(rawAgent) {
60
- return AGENT_ALIASES[rawAgent.toLowerCase()];
61
- }
62
- function resolveAddMcpAgentId(rawAgent) {
63
- const resolved = tryResolveAddMcpAgentId(rawAgent);
64
- if (!resolved) throw new Error(`Unknown agent: "${rawAgent}". Supported agents: ${listMcpAgentIds().join(", ")}`);
65
- return resolved;
66
- }
67
21
  function getSkillsAgentName(agent) {
68
22
  if (Object.prototype.hasOwnProperty.call(SKILLS_AGENT_BY_TYPE, agent)) return SKILLS_AGENT_BY_TYPE[agent];
69
23
  const id = tryResolveAddMcpAgentId(agent);
@@ -79,9 +33,6 @@ function agentPickerHint(id) {
79
33
  if (supportsSkills(id)) return "MCP server, skills";
80
34
  return "MCP server";
81
35
  }
82
- async function detectInstalledAgents() {
83
- return detectGlobalAgents();
84
- }
85
36
  function mcpPickerOptions() {
86
37
  return listMcpAgentIds().map((id) => ({
87
38
  value: id,
@@ -1,4 +1,5 @@
1
- import { detectInstalledAgents } from "./agents.js";
1
+ import { detectInstalledAgents } from "../mcp/agents.js";
2
+ import "./agents.js";
2
3
  import { resolve } from "node:path";
3
4
  //#region src/init/editors.ts
4
5
  function getVSCodeGlobalConfigDir(homeDir) {
@@ -1,11 +1,12 @@
1
1
  import { FALLBACK_TEMPLATES, fetchTemplates, scaffoldTemplate } from "./bootstrap.js";
2
2
  import { detectAgent, detectIde } from "./detect_agent.js";
3
3
  import { neonInitAgentCmd } from "./profile_cli.js";
4
- import { agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, getSkillsAgentName, mcpPickerOptions, tryResolveAddMcpAgentId } from "./agents.js";
4
+ import { installNeonMcpServer } from "../mcp/install.js";
5
+ import { agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
6
+ import { getSkillsAgentName, mcpPickerOptions } from "./agents.js";
5
7
  import { ensureNeonctlAuth, isAuthenticated } from "./auth.js";
6
8
  import { installExtension, isExtensionInstalled } from "./extension.js";
7
9
  import { inspectProject } from "./inspect.js";
8
- import { installNeonMcpServer } from "./install_mcp.js";
9
10
  import { ensureNeonctl } from "./neonctl.js";
10
11
  import { ensureSkillsUpToDate, installAgentSkills, skillsInstalledForAgent } from "./skills.js";
11
12
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -1,4 +1,5 @@
1
- import { supportsSkills, tryResolveAddMcpAgentId } from "./agents.js";
1
+ import { tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
+ import { supportsSkills } from "./agents.js";
2
3
  import { isAuthenticated } from "./auth.js";
3
4
  import { inspectProject } from "./inspect.js";
4
5
  import { getSkillList, missingSkillsForAgent, skillsInstalledForAgent } from "./skills.js";
@@ -1,6 +1,7 @@
1
- import { agentSupportsHttpMcp, agentSupportsProjectMcp, getAgentDisplayName, getSkillsAgentName, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../agents.js";
1
+ import { installNeonMcpServer } from "../../mcp/install.js";
2
+ import { agentSupportsHttpMcp, agentSupportsProjectMcp, getAgentDisplayName, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../../mcp/agents.js";
3
+ import { getSkillsAgentName } from "../agents.js";
2
4
  import { isAuthenticated } from "../auth.js";
3
- import { installNeonMcpServer } from "../install_mcp.js";
4
5
  //#region src/init/phases/mcp.ts
5
6
  function skillsFollowUp(agent) {
6
7
  if (agent && !getSkillsAgentName(agent)) return agent ? [
@@ -1,10 +1,11 @@
1
1
  import { FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate } from "../bootstrap.js";
2
2
  import { detectIde, isCursorInstalled, isVSCodeInstalled } from "../detect_agent.js";
3
- import { agentSupportsProjectMcp, getSkillsAgentName, listMcpAgentIds, resolveAddMcpAgentId, supportsSkills, tryResolveAddMcpAgentId } from "../agents.js";
3
+ import { installNeonMcpServer } from "../../mcp/install.js";
4
+ import { agentSupportsProjectMcp, listMcpAgentIds, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../../mcp/agents.js";
5
+ import { getSkillsAgentName, supportsSkills } from "../agents.js";
4
6
  import { NEON_EXTENSION_ID, downloadVsix } from "../vsix.js";
5
7
  import { findEditorCommand } from "../extension.js";
6
8
  import { inspectProject } from "../inspect.js";
7
- import { installNeonMcpServer } from "../install_mcp.js";
8
9
  import { ensureNeonctl } from "../neonctl.js";
9
10
  import { ensureSkillsUpToDate } from "../skills.js";
10
11
  import { writeFileSync } from "node:fs";
@@ -1,5 +1,6 @@
1
1
  import { neonInitAgentCmd } from "../profile_cli.js";
2
- import { supportsSkills, tryResolveAddMcpAgentId } from "../agents.js";
2
+ import { tryResolveAddMcpAgentId } from "../../mcp/agents.js";
3
+ import { supportsSkills } from "../agents.js";
3
4
  import { isAuthenticated } from "../auth.js";
4
5
  import { inspectProject } from "../inspect.js";
5
6
  //#region src/init/phases/status.ts
@@ -0,0 +1,64 @@
1
+ import { agents, detectGlobalAgents, getAgentTypes } from "add-mcp";
2
+ //#region src/mcp/agents.ts
3
+ const AGENT_ALIASES = {
4
+ cursor: "cursor",
5
+ copilot: "vscode",
6
+ "github-copilot": "vscode",
7
+ "vs-code": "vscode",
8
+ vscode: "vscode",
9
+ claude: "claude-code",
10
+ "claude-code": "claude-code",
11
+ "claude-desktop": "claude-desktop",
12
+ codex: "codex",
13
+ opencode: "opencode",
14
+ antigravity: "antigravity",
15
+ cline: "cline",
16
+ "cline-cli": "cline-cli",
17
+ "cline-vscode": "cline",
18
+ "gemini-cli": "gemini-cli",
19
+ gemini: "gemini-cli",
20
+ goose: "goose",
21
+ windsurf: "windsurf",
22
+ codeium: "windsurf",
23
+ cascade: "windsurf",
24
+ "github-copilot-cli": "github-copilot-cli",
25
+ mcporter: "mcporter",
26
+ zed: "zed",
27
+ grok: "grok-build",
28
+ "grok-build": "grok-build"
29
+ };
30
+ function listMcpAgentIds() {
31
+ return getAgentTypes();
32
+ }
33
+ function getAgentDisplayName(id) {
34
+ return agents[id].displayName;
35
+ }
36
+ function agentSupportsProjectMcp(id) {
37
+ return Boolean(agents[id].localConfigPath);
38
+ }
39
+ function agentSupportsHttpMcp(id) {
40
+ return agents[id].supportedTransports.includes("http");
41
+ }
42
+ function tryResolveAddMcpAgentId(rawAgent) {
43
+ return AGENT_ALIASES[rawAgent.toLowerCase()];
44
+ }
45
+ function resolveAddMcpAgentId(rawAgent) {
46
+ const resolved = tryResolveAddMcpAgentId(rawAgent);
47
+ if (!resolved) throw new Error(`Unknown agent: "${rawAgent}". Supported agents: ${listMcpAgentIds().join(", ")}`);
48
+ return resolved;
49
+ }
50
+ async function detectInstalledAgents() {
51
+ return detectGlobalAgents();
52
+ }
53
+ function uniqueAgentIds(ids) {
54
+ const seen = /* @__PURE__ */ new Set();
55
+ const out = [];
56
+ for (const id of ids) {
57
+ if (seen.has(id)) continue;
58
+ seen.add(id);
59
+ out.push(id);
60
+ }
61
+ return out;
62
+ }
63
+ //#endregion
64
+ export { agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, listMcpAgentIds, resolveAddMcpAgentId, tryResolveAddMcpAgentId, uniqueAgentIds };
@@ -0,0 +1,216 @@
1
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { agents, upsertServer } from "add-mcp";
5
+ //#region src/mcp/install.ts
6
+ const NEON_MCP_URL = "https://mcp.neon.tech/mcp";
7
+ const NEON_MCP_NAME = "Neon";
8
+ const NEON_MCP_CATEGORIES = [
9
+ "projects",
10
+ "branches",
11
+ "schema",
12
+ "querying",
13
+ "neon_auth",
14
+ "data_api",
15
+ "observability",
16
+ "docs"
17
+ ];
18
+ function isMcpCategory(value) {
19
+ for (const category of NEON_MCP_CATEGORIES) if (category === value) return true;
20
+ return false;
21
+ }
22
+ function parseMcpCategories(raw) {
23
+ const out = [];
24
+ for (const value of raw) {
25
+ if (!isMcpCategory(value)) throw new Error(`Unknown MCP category: "${value}". Supported categories: ${NEON_MCP_CATEGORIES.join(", ")}`);
26
+ out.push(value);
27
+ }
28
+ return out;
29
+ }
30
+ function isNeonMcpUrl(value) {
31
+ try {
32
+ const url = new URL(value);
33
+ return url.protocol === "https:" && url.hostname === "mcp.neon.tech" && url.pathname === "/mcp";
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ function neonMcpUrl(options = {}) {
39
+ const url = new URL(NEON_MCP_URL);
40
+ if (options.readOnly === true) url.searchParams.set("readonly", "true");
41
+ for (const category of options.categories ?? []) url.searchParams.append("category", category);
42
+ const projectId = options.projectId?.trim();
43
+ if (projectId) url.searchParams.set("projectId", projectId);
44
+ return url.href;
45
+ }
46
+ const BEARER_RE = /Bearer\s+([^\s"]+)/i;
47
+ function nestedValue(root, key) {
48
+ let current = root;
49
+ for (const part of key.split(".")) {
50
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
51
+ current = current[part];
52
+ }
53
+ return current;
54
+ }
55
+ function recordOf(value) {
56
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
57
+ return value;
58
+ }
59
+ function bearerFromNeonMcpConfig(config) {
60
+ const record = recordOf(config);
61
+ if (!record || typeof record.url !== "string" || !isNeonMcpUrl(record.url)) return;
62
+ const headerRecord = recordOf(record.headers ?? record.http_headers);
63
+ if (!headerRecord) return;
64
+ const authorization = headerRecord.Authorization ?? headerRecord.authorization;
65
+ if (typeof authorization !== "string") return;
66
+ return BEARER_RE.exec(authorization.trim())?.[1];
67
+ }
68
+ function neonServers(value) {
69
+ const record = recordOf(value);
70
+ if (!record) return;
71
+ return record["Neon"] ?? record.neon;
72
+ }
73
+ function neonApiKeyFromFile(path, configKey) {
74
+ let raw;
75
+ try {
76
+ raw = readFileSync(path, "utf8");
77
+ } catch {
78
+ return;
79
+ }
80
+ try {
81
+ return bearerFromNeonMcpConfig(neonServers(nestedValue(JSON.parse(raw), configKey)));
82
+ } catch {
83
+ const neonBlock = raw.match(/\[mcp_servers\.Neon\][\s\S]*?(?=\n\[|$)/)?.[0];
84
+ if (!neonBlock) return;
85
+ const url = neonBlock.match(/^url\s*=\s*"([^"]*)"/m)?.[1];
86
+ if (!url || !isNeonMcpUrl(url)) return;
87
+ return BEARER_RE.exec(neonBlock)?.[1];
88
+ }
89
+ }
90
+ function configPathFor(agent, scope, cwd) {
91
+ const spec = agents[agent];
92
+ if (scope === "project") {
93
+ if (!spec.localConfigPath) return;
94
+ return join(cwd, spec.localConfigPath);
95
+ }
96
+ return spec.configPath;
97
+ }
98
+ function configKeyFor(agent, scope) {
99
+ const spec = agents[agent];
100
+ if (scope === "project" && spec.localConfigKey) return spec.localConfigKey;
101
+ return spec.configKey;
102
+ }
103
+ function existingNeonApiKey(options) {
104
+ for (const agent of options.agents) {
105
+ const path = configPathFor(agent, options.scope, options.cwd);
106
+ if (!path) continue;
107
+ const key = neonApiKeyFromFile(path, configKeyFor(agent, options.scope));
108
+ if (key) return key;
109
+ }
110
+ }
111
+ function isTrackedByGit(cwd, relativePath) {
112
+ try {
113
+ execFileSync("git", [
114
+ "-C",
115
+ cwd,
116
+ "ls-files",
117
+ "--error-unmatch",
118
+ "--",
119
+ relativePath
120
+ ], { stdio: [
121
+ "ignore",
122
+ "pipe",
123
+ "ignore"
124
+ ] });
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ function trackedProjectMcpConfig(options) {
131
+ for (const agent of options.agents) {
132
+ const relativePath = agents[agent].localConfigPath;
133
+ if (!relativePath) continue;
134
+ if (isTrackedByGit(options.cwd, relativePath)) return relativePath;
135
+ }
136
+ }
137
+ function mcpUnsupportedReason(agent, scope) {
138
+ const spec = agents[agent];
139
+ if (!spec.supportedTransports.includes("http")) return spec.unsupportedTransportMessage ?? `${spec.displayName} does not support remote HTTP MCP servers.`;
140
+ if (scope === "project" && !spec.localConfigPath) return `${spec.displayName} does not support project-level MCP config.`;
141
+ }
142
+ function gitignoreProjectConfig(path) {
143
+ const dir = dirname(path);
144
+ const entry = basename(path);
145
+ const gitignorePath = join(dir, ".gitignore");
146
+ try {
147
+ if (!existsSync(gitignorePath)) {
148
+ writeFileSync(gitignorePath, `${entry}\n`);
149
+ return;
150
+ }
151
+ const current = readFileSync(gitignorePath, "utf8");
152
+ if (current.split(/\r?\n/).some((line) => line.trim() === entry)) return;
153
+ const needsLeadingNewline = current.length > 0 && !current.endsWith("\n");
154
+ writeFileSync(gitignorePath, `${current}${needsLeadingNewline ? "\n" : ""}${entry}\n`);
155
+ return;
156
+ } catch (err) {
157
+ return `Could not gitignore ${path}: ${err instanceof Error ? err.message : String(err)}`;
158
+ }
159
+ }
160
+ function protectWrittenConfig(path, scope) {
161
+ try {
162
+ chmodSync(path, 384);
163
+ } catch (err) {
164
+ return `Could not restrict permissions on ${path}: ${err instanceof Error ? err.message : String(err)}`;
165
+ }
166
+ if (scope === "project") return gitignoreProjectConfig(path);
167
+ }
168
+ function redactSecrets(text) {
169
+ return text.replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(/napi_[A-Za-z0-9_]+/gi, "napi_[redacted]");
170
+ }
171
+ function installNeonMcpServer(options) {
172
+ const auth = options.auth ?? { kind: "oauth" };
173
+ const url = options.url ?? "https://mcp.neon.tech/mcp";
174
+ const unsupported = mcpUnsupportedReason(options.agent, options.scope);
175
+ if (unsupported) return {
176
+ ok: false,
177
+ unsupported: true,
178
+ error: unsupported
179
+ };
180
+ const server = auth.kind === "api-key" ? {
181
+ type: "http",
182
+ url,
183
+ headers: { Authorization: `Bearer ${auth.apiKey}` }
184
+ } : {
185
+ type: "http",
186
+ url
187
+ };
188
+ const result = upsertServer(options.agent, NEON_MCP_NAME, server, {
189
+ local: options.scope === "project",
190
+ cwd: options.cwd
191
+ });
192
+ if (result.success) {
193
+ if (auth.kind === "api-key") {
194
+ const paths = [result.path, ...result.extraPaths ?? []];
195
+ for (const path of paths) {
196
+ const error = protectWrittenConfig(path, options.scope);
197
+ if (error) return {
198
+ ok: false,
199
+ unsupported: false,
200
+ error
201
+ };
202
+ }
203
+ }
204
+ return {
205
+ ok: true,
206
+ path: result.path
207
+ };
208
+ }
209
+ return {
210
+ ok: false,
211
+ unsupported: false,
212
+ error: redactSecrets(result.error ?? "Failed to write Neon MCP server config")
213
+ };
214
+ }
215
+ //#endregion
216
+ export { NEON_MCP_CATEGORIES, NEON_MCP_NAME, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, isMcpCategory, isNeonMcpUrl, mcpUnsupportedReason, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig };
@@ -0,0 +1,46 @@
1
+ import { log } from "../log.js";
2
+ import { isNeonApiError } from "../api.js";
3
+ import { getCliName } from "../utils/cli_name.js";
4
+ import { mintedKeyName } from "../profile_keys.js";
5
+ //#region src/mcp/mint.ts
6
+ const cannotMintMessage = "This CLI credential cannot mint API keys. Organization and project-scoped keys cannot create other keys. Sign in with `neon auth` or pass a personal API key.";
7
+ const withdraw = async (client, keyId) => {
8
+ if (!Number.isSafeInteger(keyId) || keyId === void 0 || keyId <= 0) return false;
9
+ try {
10
+ const { data } = await client.revokeApiKey(keyId);
11
+ return data.revoked === true && data.id === keyId;
12
+ } catch (err) {
13
+ log.error("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
14
+ return false;
15
+ }
16
+ };
17
+ const assertUsable = async (client, data, fallbackName) => {
18
+ const problem = typeof data.key !== "string" || data.key.trim() === "" ? "Neon returned no key." : data.project_id !== void 0 ? `Neon returned a key scoped to ${data.project_id} rather than the whole account.` : !Number.isSafeInteger(data.id) ? "Neon returned no key id." : null;
19
+ if (problem) {
20
+ const withdrawn = await withdraw(client, data.id);
21
+ throw new Error(`${problem} ${withdrawn ? "The key has been revoked; nothing was issued." : `The key could NOT be revoked and may still be live${data.id === void 0 ? "" : `. Remove it with \`${getCliName()} api-keys revoke ${data.id}\``}.`}`);
22
+ }
23
+ const id = data.id;
24
+ const key = data.key;
25
+ if (id === void 0 || key === void 0) throw new Error("Neon returned no key.");
26
+ return {
27
+ id,
28
+ name: data.name ?? fallbackName,
29
+ key
30
+ };
31
+ };
32
+ async function mintMcpApiKey(options) {
33
+ const name = mintedKeyName("mcp");
34
+ try {
35
+ const { data } = await options.apiClient.createApiKey({ key_name: name });
36
+ return assertUsable(options.apiClient, data, name);
37
+ } catch (err) {
38
+ if (isNeonApiError(err) && (err.status === 403 || err.status === 404)) throw new Error(cannotMintMessage);
39
+ throw err;
40
+ }
41
+ }
42
+ async function withdrawMintedKey(apiClient, key) {
43
+ return withdraw(apiClient, key.id);
44
+ }
45
+ //#endregion
46
+ export { mintMcpApiKey, withdrawMintedKey };
@@ -0,0 +1,43 @@
1
+ import { tryResolveAddMcpAgentId } from "./agents.js";
2
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
3
+ import { detectMcpAgents, mcpInstallableAgents } from "./targets.js";
4
+ import { pickMcpAuth, pickMcpProjectPin, pickMcpScope } from "./wizard.js";
5
+ //#region src/mcp/plan.ts
6
+ async function resolveMcpPlan(options) {
7
+ const prompt = options.interactive && !options.yes;
8
+ const scope = options.project ? "project" : prompt ? await (options.pickScope ?? pickMcpScope)() : "global";
9
+ const available = mcpInstallableAgents(scope);
10
+ const detected = await detectMcpAgents({
11
+ scope,
12
+ cwd: options.cwd
13
+ });
14
+ const agents = await resolveAgentSelection({
15
+ specified: options.agents,
16
+ choices: agentChoicesFrom(available, detected),
17
+ detected,
18
+ message: "Which coding agents should get the Neon MCP server? (space to toggle, enter to confirm)",
19
+ nonInteractiveMessage: scope === "project" ? `No coding agents detected in this project. Pass --agent <name>. Supported agents: ${available.join(", ")}` : `No coding agents detected. Pass --agent <name>. Supported agents: ${available.join(", ")}`,
20
+ resolveSpecified: (raw) => {
21
+ const id = tryResolveAddMcpAgentId(raw);
22
+ if (!id) throw new Error(`Unknown agent: "${raw}". Supported agents: ${available.join(", ")}`);
23
+ return id;
24
+ },
25
+ pick: prompt ? options.pickAgents ?? pickAgentsInteractively : void 0,
26
+ interactive: prompt
27
+ });
28
+ const auth = options.oauth ? "oauth" : prompt ? await (options.pickAuth ?? pickMcpAuth)() : "api-key";
29
+ let urlProjectId = options.projectId;
30
+ if (urlProjectId === void 0 && prompt && scope === "project" && options.linkedProjectId) {
31
+ if (await (options.pickProjectPin ?? pickMcpProjectPin)(options.linkedProjectId)) urlProjectId = options.linkedProjectId;
32
+ }
33
+ return {
34
+ scope,
35
+ agents,
36
+ auth,
37
+ readOnly: options.readOnly,
38
+ urlProjectId,
39
+ categories: options.categories
40
+ };
41
+ }
42
+ //#endregion
43
+ export { resolveMcpPlan };
@@ -0,0 +1,34 @@
1
+ import { mcpUnsupportedReason } from "./install.js";
2
+ import { detectInstalledAgents, listMcpAgentIds, resolveAddMcpAgentId, uniqueAgentIds } from "./agents.js";
3
+ import { detectProjectAgents } from "add-mcp";
4
+ //#region src/mcp/targets.ts
5
+ function mcpInstallableAgents(scope) {
6
+ return listMcpAgentIds().filter((id) => mcpUnsupportedReason(id, scope) === void 0);
7
+ }
8
+ async function detectMcpAgents(options) {
9
+ const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await detectInstalledAgents();
10
+ return uniqueAgentIds(detected).filter((id) => mcpUnsupportedReason(id, options.scope) === void 0);
11
+ }
12
+ function resolveInstallTargets(options) {
13
+ const requested = uniqueAgentIds(options.agents.map(resolveAddMcpAgentId));
14
+ const install = [];
15
+ const skipped = [];
16
+ for (const agent of requested) {
17
+ const error = mcpUnsupportedReason(agent, options.scope);
18
+ if (error) skipped.push({
19
+ agent,
20
+ error
21
+ });
22
+ else install.push(agent);
23
+ }
24
+ if (install.length === 0) {
25
+ const details = skipped.map((row) => row.error).join(" ");
26
+ throw new Error(details || `None of the selected agents can install the Neon MCP server. Supported agents: ${mcpInstallableAgents(options.scope).join(", ")}`);
27
+ }
28
+ return {
29
+ install,
30
+ skipped
31
+ };
32
+ }
33
+ //#endregion
34
+ export { detectMcpAgents, mcpInstallableAgents, resolveInstallTargets };
@@ -0,0 +1,91 @@
1
+ import { getAgentDisplayName } from "./agents.js";
2
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
3
+ import prompts from "prompts";
4
+ //#region src/mcp/wizard.ts
5
+ const restoreCursorOnAbort = (state) => {
6
+ if (state.aborted) {
7
+ process.stdout.write("\x1B[?25h");
8
+ process.stdout.write("\n");
9
+ process.exit(1);
10
+ }
11
+ };
12
+ const pickMcpScope = async () => {
13
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass --project for project config, or -y for global.");
14
+ const { scope } = await prompts({
15
+ onState: restoreCursorOnAbort,
16
+ type: "select",
17
+ name: "scope",
18
+ message: "Where should the Neon MCP server be installed?",
19
+ initial: 0,
20
+ choices: [{
21
+ title: "Global",
22
+ value: "global",
23
+ description: "User-level agent config"
24
+ }, {
25
+ title: "Project",
26
+ value: "project",
27
+ description: "This directory"
28
+ }]
29
+ });
30
+ if (scope !== "global" && scope !== "project") throw new Error("Aborted.");
31
+ return scope;
32
+ };
33
+ const pickMcpAuth = async () => {
34
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass --oauth for OAuth, or -y to mint an API key.");
35
+ const { auth } = await prompts({
36
+ onState: restoreCursorOnAbort,
37
+ type: "select",
38
+ name: "auth",
39
+ message: "How should agents authenticate to Neon?",
40
+ initial: 0,
41
+ choices: [{
42
+ title: "API key",
43
+ value: "api-key",
44
+ description: "Mint a key and write it into agent config"
45
+ }, {
46
+ title: "OAuth",
47
+ value: "oauth",
48
+ description: "The agent signs in on first use"
49
+ }]
50
+ });
51
+ if (auth !== "api-key" && auth !== "oauth") throw new Error("Aborted.");
52
+ return auth;
53
+ };
54
+ const pickMcpProjectPin = async (linkedProjectId) => {
55
+ if (!canPickAgentsInteractively()) return false;
56
+ const { pin } = await prompts({
57
+ onState: restoreCursorOnAbort,
58
+ type: "confirm",
59
+ name: "pin",
60
+ message: `Pin MCP tools to the linked project ${linkedProjectId}?`,
61
+ initial: true
62
+ });
63
+ return pin === true;
64
+ };
65
+ const mcpInstallSummary = (options) => {
66
+ const agents = options.install.map(getAgentDisplayName).join(", ");
67
+ const auth = options.auth === "oauth" ? "OAuth, agent signs in on first use" : options.reuse ? "reuse the API key already in agent config" : "mint an account-wide API key that reaches every organization";
68
+ const rows = [
69
+ ["Config", options.scope === "project" ? "this directory" : "user-level"],
70
+ ["Agents", agents],
71
+ ["Auth", auth],
72
+ ["URL", options.url]
73
+ ];
74
+ if (options.skipped.length > 0) rows.push(["Skipped", options.skipped.map((row) => `${getAgentDisplayName(row.agent)} (${row.error})`).join("; ")]);
75
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
76
+ return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join("\n");
77
+ };
78
+ const confirmMcpInstall = async (options) => {
79
+ if (!canPickAgentsInteractively()) return true;
80
+ process.stdout.write(`\n${mcpInstallSummary(options)}\n\n`);
81
+ const { ok } = await prompts({
82
+ onState: restoreCursorOnAbort,
83
+ type: "confirm",
84
+ name: "ok",
85
+ message: "Write the Neon MCP server into these agents?",
86
+ initial: true
87
+ });
88
+ return ok === true;
89
+ };
90
+ //#endregion
91
+ export { confirmMcpInstall, mcpInstallSummary, pickMcpAuth, pickMcpProjectPin, pickMcpScope };
@@ -57,9 +57,8 @@ const test = test$1.extend({
57
57
  apiHost,
58
58
  "--output",
59
59
  options.output ?? (options.outputTable ? "table" : "yaml"),
60
- "--api-key",
61
- "test-key",
62
60
  "--no-analytics",
61
+ ...options.apiKey === false ? [] : ["--api-key", options.apiKey ?? "test-key"],
63
62
  ...args
64
63
  ], {
65
64
  stdio: "pipe",
@@ -84,9 +83,13 @@ const test = test$1.extend({
84
83
  cp.on("close", (code) => {
85
84
  try {
86
85
  expect(code).toBe(options?.code ?? 0);
87
- expect(output).toMatchSnapshot();
86
+ if (options.snapshot !== false) expect(output).toMatchSnapshot();
88
87
  if (options.stderr !== void 0) expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string" ? options.stderr.toString().replace(/\s+/g, " ") : options.stderr);
89
- resolve();
88
+ resolve({
89
+ stdout: output,
90
+ stderr: error,
91
+ code
92
+ });
90
93
  } catch (err) {
91
94
  reject(err instanceof Error ? err : new Error(String(err)));
92
95
  }
@@ -0,0 +1,70 @@
1
+ import { isCi } from "../env.js";
2
+ import { getAgentDisplayName, resolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
3
+ import prompts from "prompts";
4
+ //#region src/utils/agent_picker.ts
5
+ const canPickAgentsInteractively = () => !isCi() && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
6
+ const pickAgentsInteractively = async (options) => {
7
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass --agent <name>, or run this command in a terminal to pick agents.");
8
+ if (options.choices.length === 0) throw new Error("No coding agents are available to pick.");
9
+ const selected = new Set(options.selected ?? []);
10
+ const { agents } = await prompts({
11
+ onState: (state) => {
12
+ if (state.aborted) {
13
+ process.stdout.write("\x1B[?25h");
14
+ process.stdout.write("\n");
15
+ process.exit(1);
16
+ }
17
+ },
18
+ type: "multiselect",
19
+ name: "agents",
20
+ message: options.message,
21
+ instructions: false,
22
+ min: 1,
23
+ choices: options.choices.map((choice) => ({
24
+ value: choice.id,
25
+ title: choice.title,
26
+ description: choice.description,
27
+ selected: selected.has(choice.id)
28
+ }))
29
+ });
30
+ if (!Array.isArray(agents)) throw new Error("Aborted: no agents selected.");
31
+ const picked = [];
32
+ for (const value of agents) {
33
+ const id = agentIdInChoices(value, options.choices);
34
+ if (id === void 0) throw new Error(`Unknown agent: "${String(value)}".`);
35
+ picked.push(id);
36
+ }
37
+ return uniqueAgentIds(picked);
38
+ };
39
+ const resolveAgentSelection = async (options) => {
40
+ if (options.specified.length > 0) {
41
+ const resolve = options.resolveSpecified ?? resolveAddMcpAgentId;
42
+ return uniqueAgentIds(options.specified.map(resolve));
43
+ }
44
+ const pick = options.pick ?? (options.interactive ?? canPickAgentsInteractively() ? pickAgentsInteractively : void 0);
45
+ if (pick) {
46
+ const selected = await pick({
47
+ message: options.message,
48
+ choices: options.choices,
49
+ selected: options.detected.filter((id) => options.choices.some((choice) => choice.id === id))
50
+ });
51
+ if (selected.length === 0) throw new Error("No agents selected. Pass --agent <name>, or pick at least one agent.");
52
+ return uniqueAgentIds(selected);
53
+ }
54
+ if (options.detected.length > 0) return uniqueAgentIds(options.detected);
55
+ throw new Error(options.nonInteractiveMessage);
56
+ };
57
+ const agentChoicesFrom = (available, detected) => {
58
+ const detectedSet = new Set(detected);
59
+ return [...available.filter((id) => detectedSet.has(id)), ...available.filter((id) => !detectedSet.has(id))].map((id) => ({
60
+ id,
61
+ title: getAgentDisplayName(id),
62
+ ...detectedSet.has(id) ? { description: "detected" } : {}
63
+ }));
64
+ };
65
+ const agentIdInChoices = (value, choices) => {
66
+ if (typeof value !== "string") return;
67
+ for (const choice of choices) if (choice.id === value) return choice.id;
68
+ };
69
+ //#endregion
70
+ export { agentChoicesFrom, canPickAgentsInteractively, pickAgentsInteractively, resolveAgentSelection };
@@ -111,6 +111,57 @@ const INSPECT_QUERIES = {
111
111
  ORDER BY now() - query_start DESC;
112
112
  `
113
113
  },
114
+ "stalled-queries": {
115
+ describe: "Active queries running longer than 30 seconds with parallel workers, waits, and blockers (compute-wide)",
116
+ scope: "compute",
117
+ fields: [
118
+ "duration",
119
+ "wait_event",
120
+ "blocking_pids",
121
+ "role",
122
+ "query_group",
123
+ "query"
124
+ ],
125
+ emptyMessage: "No active queries running longer than 30 seconds.",
126
+ sql: `
127
+ WITH activity AS MATERIALIZED (
128
+ SELECT *
129
+ FROM pg_stat_activity
130
+ WHERE state = 'active'
131
+ AND backend_type IN ('client backend', 'parallel worker')
132
+ AND pid <> pg_backend_pid()
133
+ ),
134
+ stalled_groups AS (
135
+ SELECT DISTINCT COALESCE(leader_pid, pid) AS query_group
136
+ FROM activity
137
+ WHERE query_start <= statement_timestamp() - interval '30 seconds'
138
+ )
139
+ SELECT
140
+ statement_timestamp() AS observed_at,
141
+ a.query_start,
142
+ g.query_group,
143
+ a.pid,
144
+ a.leader_pid,
145
+ CASE
146
+ WHEN a.leader_pid IS NULL THEN 'leader'
147
+ ELSE 'worker'
148
+ END AS role,
149
+ a.backend_type,
150
+ a.datname AS database,
151
+ a.application_name,
152
+ a.query_id::text AS query_id,
153
+ a.state,
154
+ a.wait_event_type,
155
+ a.wait_event,
156
+ array_to_string(pg_blocking_pids(a.pid), ',') AS blocking_pids,
157
+ (statement_timestamp() - a.query_start)::text AS duration,
158
+ a.query
159
+ FROM activity a
160
+ JOIN stalled_groups g
161
+ ON g.query_group = COALESCE(a.leader_pid, a.pid)
162
+ ORDER BY g.query_group, a.leader_pid NULLS FIRST, a.pid;
163
+ `
164
+ },
114
165
  locks: {
115
166
  describe: "Locks held with the acquiring query and its age (pg_locks + pg_stat_activity)",
116
167
  scope: "database",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,8 +63,8 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config-runtime": "1.0.2",
67
66
  "@neon/config": "1.0.2",
67
+ "@neon/config-runtime": "1.0.2",
68
68
  "@neon/sdk": "2.2.0"
69
69
  },
70
70
  "optionalDependencies": {
@@ -1,35 +0,0 @@
1
- import { agents, upsertServer } from "add-mcp";
2
- //#region src/init/install_mcp.ts
3
- const NEON_MCP_URL = "https://mcp.neon.tech/mcp";
4
- const NEON_MCP_NAME = "Neon";
5
- function installNeonMcpServer(options) {
6
- const agent = agents[options.agent];
7
- if (!agent.supportedTransports.includes("http")) return {
8
- ok: false,
9
- unsupported: true,
10
- error: agent.unsupportedTransportMessage ?? `${agent.displayName} does not support remote HTTP MCP servers.`
11
- };
12
- if (options.scope === "project" && !agent.localConfigPath) return {
13
- ok: false,
14
- unsupported: true,
15
- error: `${agent.displayName} does not support project-level MCP config.`
16
- };
17
- const result = upsertServer(options.agent, NEON_MCP_NAME, {
18
- type: "http",
19
- url: NEON_MCP_URL
20
- }, {
21
- local: options.scope === "project",
22
- cwd: options.cwd
23
- });
24
- if (result.success) return {
25
- ok: true,
26
- path: result.path
27
- };
28
- return {
29
- ok: false,
30
- unsupported: false,
31
- error: result.error ?? "Failed to write Neon MCP server config"
32
- };
33
- }
34
- //#endregion
35
- export { NEON_MCP_NAME, NEON_MCP_URL, installNeonMcpServer };