naro-mcp 0.18.1 → 0.19.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.
Files changed (3) hide show
  1. package/README.md +25 -24
  2. package/dist/index.js +189 -30
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -55,29 +55,30 @@ Headless/CI agents can skip `naro login` and set a token instead:
55
55
 
56
56
  ## Tools
57
57
 
58
- | Tool | Purpose |
59
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | `whoami`, `list_teams` | Account / team context |
61
- | `list_projects`, `get_project`, `link_project` | Project discovery + `.naro/project.json` linking |
62
- | `get_team_usage` | Team usage for a UTC month (`month`, the current one by default): requests, builds and custom domains per day and per project, the plan's `limits`, the enforcement mode, and `summary` (each metric against its limit, and `status` paused/over/near). `team` defaults to the team linked in `directory`, then your only team; otherwise `needsTeam` with the list. Owner, billing and enterprise_viewer only (403 `FORBIDDEN` otherwise). Read-only |
63
- | `deploy` | Local Next.js build → upload → live URL (preview, `production: true`, or a named environment with `environment: "qa"` — not together with `production`: both are refused with `VALIDATION_FAILED` `environment and production cannot be used together.` before any value is read, asked for or sent and before the deployment is created; `production` and `preview` are not environment names). The result's `target` is `production`, `preview` or the environment's slug, and `environment` is the slug or null. Local .env files are never uploaded; unset keys come back as `missingEnvKeys` |
64
- | `list_deployments`, `get_deployment` | Deployment status / history |
65
- | `get_build_logs`, `get_runtime_logs` | Debugging failed builds and live workers |
66
- | `list_environment_variables`, `set_environment_variable`, `remove_environment_variable` | Env management (secret values never returned). `target` is `production`, `preview`, `all` or an environment's slug (`all` by default when setting); without `target`, `remove_environment_variable` removes the `production`, `preview` and `all` rows and leaves an environment's value alone. A name that cannot be a slug is refused as `Invalid target "<value>" — use production, preview, all or an environment name.`. For `set_environment_variable`, a slug that is not an environment answers `404 ENVIRONMENT_NOT_FOUND`; `remove_environment_variable` reads the list and answers `ENV_NOT_FOUND` (`No KEY variable found for the <slug> environment.`). Without `target`, a key only environments hold answers `ENV_NOT_FOUND` `No KEY variable found outside named environments. Pass the environment to remove it: qa.` with `key` and `environments`, and nothing is removed |
67
- | `list_domains`, `add_domain`, `verify_domain`, `remove_domain` | Custom domains: the one CNAME to create (naro connects it within ~5 minutes), plus a re-check that says which of ownership / certificate / routing is still missing. An apex is attached as an apex+www pair by default (`withWww: false` to opt out) |
68
- | `promote_deployment`, `rollback_deployment` | Production traffic control |
69
- | `cancel_deployment` | Stops a build that is still running (QUEUED–BUILDING). Does not undo a deployment that already went live — use `rollback_deployment` for that |
70
- | `list_environments`, `create_environment`, `update_environment`, `delete_environment` | Named environments (QA, staging), served at `{subdomain}--{slug}` apart from production and the preview. `list_environments` returns `{ projectId, environments, limit }` (`limit` is what the plan allows per project, null for no limit); `create_environment` takes `slug` and optionally `branch` (pushes to it deploy there), `copyEnvFrom` (`preview` by default, `production` or `none`) and `database` (`schema` by default, or `empty`); `update_environment` takes the environment's slug and `branch` (an empty string takes it off) and/or `protectionEnabled`. **`update_environment` is destructive on its `protectionEnabled` argument** (`false` opens the environment to anyone with the link — like `set_preview_protection`) and **`delete_environment` is destructive** (removes its worker and protection key). Deploy to one with `deploy`'s `environment`; set its variables with the env tools' `target` |
71
- | `enable_database`, `get_database`, `list_tables` | The project's D1 database: turn it on, status/size/limits, schema (verbose: true for columns, indexes and foreign keys) |
72
- | `execute_sql` | Run SQL (argument `query`, optional `params` for a single statement; several statements run as one transaction). Destructive; under `--read-only` it stays but accepts a single read statement only |
73
- | `apply_migration`, `list_migrations`, `generate_typescript_types` | Numbered migrations in the `d1_migrations` ledger wrangler also uses (each file all-or-nothing), and TypeScript types for the schema (text only; its `Database` is what `createClient<Database>()` from `naro-js` takes) |
74
- | `create_table`, `alter_table`, `drop_table`, `create_index`, `drop_index` | Schema changes without SQL: naro renders checked DDL, applies it as one numbered migration (`NNNN_schema_<op>_<table>.sql`) and writes the file like `apply_migration`. `alter_table` adds, renames or drops a column or renames the table. Dropping a table or column needs `confirm`; `alter_table`, `drop_table` and `drop_index` are destructive and hidden under `--read-only`. `dryRun: true` returns the SQL only |
75
- | `export_database` | SQL dump of the database (`schemaOnly`, `dataOnly`, `tables`): waits up to 5 minutes and returns a signed download link plus `atBookmark`, the point the dump reflects |
76
- | `restore_database` | Time Travel restore to a `bookmark` or a `timestamp` (last 30 days). Destructive; hidden under `--read-only`. Returns `previousBookmark`, which undoes it |
77
- | `get_speed_insights`, `set_speed_insights` | Speed Insights: each Web Vital's p75, the selected metric by page, device, browser, operating system, country, element and deployment, or one route's paths; the page load's p75 and the mean of each step (`pageLoad`); production or preview (`environment`); narrowed to one page, country, device, browser or operating system (`path`, `country`, `device`, `browser`, `os`); turn measuring on or off (not destructive: nothing is deleted) |
78
- | `get_analytics` | Web Analytics: visitors and page views over `range` (24h, 7d or 30d), the trend, and the top 10 pages, referrers, countries, devices, browsers and operating systems by page views; narrowed to one value of each dimension (`path`, `referrer`, `country`, `device`, `browser`, `os`), all given must match. Production only: there is no `environment`, and one is refused. Read-only |
79
- | `get_preview_protection`, `set_preview_protection` | Preview protection: whether the preview URL (`{subdomain}-preview`) is open to project members only, and whether a bypass secret is set; turn it on or off. **`set_preview_protection` is destructive** (turning protection off publishes unreleased work). Bypass secrets are made in the dashboard or with `naro protection bypass`, never by a tool, so they stay out of the conversation |
80
- | `get_storage`, `enable_storage`, `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`, `delete_storage_bucket`, `list_storage_objects`, `create_storage_signed_url`, `delete_storage_objects` | Naro Storage: on or off, limits, usage, today's operations and whether uploads are stopped, and buckets; turn it on; make, change and remove buckets (`delete_storage_bucket` is destructive: `confirm` is the bucket's name, `empty: true` deletes its objects first; `update_storage_bucket` is destructive on its `public` argument, like `deploy` on `production`: `public: true` needs the same `confirm`); list a folder's files (`search` keeps the names that start with a text; a backend from before it answers `INVALID_REQUEST` — run `naro backend enable` once), make a signed link of up to an hour, and delete files (`delete_storage_objects` is destructive: `confirm` is the bucket's name) — through the control plane, so no service key reaches the MCP process. Files go in through naro-js or `naro storage cp`, not through MCP |
58
+ | Tool | Purpose |
59
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
60
+ | `whoami`, `list_teams` | Account / team context |
61
+ | `list_projects`, `get_project`, `link_project` | Project discovery + `.naro/project.json` linking |
62
+ | `get_team_usage` | Team usage for a UTC month (`month`, the current one by default): requests, builds and custom domains per day and per project, the plan's `limits`, the enforcement mode, and `summary` (each metric against its limit, and `status` paused/over/near). `team` defaults to the team linked in `directory`, then your only team; otherwise `needsTeam` with the list. Owner, billing and enterprise_viewer only (403 `FORBIDDEN` otherwise). Read-only |
63
+ | `deploy` | Local Next.js build → upload → live URL (preview, `production: true`, or a named environment with `environment: "qa"` — not together with `production`: both are refused with `VALIDATION_FAILED` `environment and production cannot be used together.` before any value is read, asked for or sent and before the deployment is created; `production` and `preview` are not environment names). The result's `target` is `production`, `preview` or the environment's slug, and `environment` is the slug or null. Local .env files are never uploaded; unset keys come back as `missingEnvKeys` |
64
+ | `list_deployments`, `get_deployment` | Deployment status / history |
65
+ | `get_build_logs`, `get_runtime_logs` | Debugging failed builds and live workers |
66
+ | `list_environment_variables`, `set_environment_variable`, `remove_environment_variable` | Env management (secret values never returned). `target` is `production`, `preview`, `all` or an environment's slug (`all` by default when setting); without `target`, `remove_environment_variable` removes the `production`, `preview` and `all` rows and leaves an environment's value alone. A name that cannot be a slug is refused as `Invalid target "<value>" — use production, preview, all or an environment name.`. For `set_environment_variable`, a slug that is not an environment answers `404 ENVIRONMENT_NOT_FOUND`; `remove_environment_variable` reads the list and answers `ENV_NOT_FOUND` (`No KEY variable found for the <slug> environment.`). Without `target`, a key only environments hold answers `ENV_NOT_FOUND` `No KEY variable found outside named environments. Pass the environment to remove it: qa.` with `key` and `environments`, and nothing is removed |
67
+ | `list_domains`, `add_domain`, `verify_domain`, `remove_domain` | Custom domains: the one CNAME to create (naro connects it within ~5 minutes), plus a re-check that says which of ownership / certificate / routing is still missing. An apex is attached as an apex+www pair by default (`withWww: false` to opt out) |
68
+ | `promote_deployment` | Publishes a READY deployment to production, or to a named environment with `target: "qa"` (`target` is `production` by default; `preview` and an empty name are refused with `VALIDATION_FAILED` `target must be production or an environment name.` before any request). Promoting between surfaces when one of them is an environment (preview → qa, qa → production) rebuilds the same commit with the target surface's variables (`mode: "rebuild"`, waits up to 15 minutes; `reupload` publishes the stored build and waits up to 5). The result's `target` is the name you gave or null, and `mode` is `reupload` or `rebuild`. A control plane that predates promote targets answers `PROMOTE_TARGET_UNSUPPORTED` and promotes nothing |
69
+ | `rollback_deployment` | Rolls production back to the previous READY production build, or to the `deploymentId` you name. The result's `mode` is `reupload` or `rebuild` |
70
+ | `cancel_deployment` | Stops a build that is still running (QUEUED–BUILDING). Does not undo a deployment that already went live — use `rollback_deployment` for that |
71
+ | `list_environments`, `create_environment`, `update_environment`, `delete_environment` | Named environments (QA, staging), served at `{subdomain}--{slug}` apart from production and the preview. `list_environments` returns `{ projectId, environments, limit }` (`limit` is what the plan allows per project, null for no limit); `create_environment` takes `slug` and optionally `branch` (pushes to it deploy there), `copyEnvFrom` (`preview` by default, `production` or `none`) and `database` (`schema` by default, or `empty`); `update_environment` takes the environment's slug and `branch` (an empty string takes it off) and/or `protectionEnabled`. **`update_environment` is destructive on its `protectionEnabled` argument** (`false` opens the environment to anyone with the link — like `set_preview_protection`) and **`delete_environment` is destructive** (removes its worker and protection key). Deploy to one with `deploy`'s `environment`; set its variables with the env tools' `target` |
72
+ | `enable_database`, `get_database`, `list_tables` | The project's D1 database: turn it on, status/size/limits, schema (verbose: true for columns, indexes and foreign keys) |
73
+ | `execute_sql` | Run SQL (argument `query`, optional `params` for a single statement; several statements run as one transaction). Destructive; under `--read-only` it stays but accepts a single read statement only |
74
+ | `apply_migration`, `list_migrations`, `generate_typescript_types` | Numbered migrations in the `d1_migrations` ledger wrangler also uses (each file all-or-nothing), and TypeScript types for the schema (text only; its `Database` is what `createClient<Database>()` from `naro-js` takes) |
75
+ | `create_table`, `alter_table`, `drop_table`, `create_index`, `drop_index` | Schema changes without SQL: naro renders checked DDL, applies it as one numbered migration (`NNNN_schema_<op>_<table>.sql`) and writes the file like `apply_migration`. `alter_table` adds, renames or drops a column or renames the table. Dropping a table or column needs `confirm`; `alter_table`, `drop_table` and `drop_index` are destructive and hidden under `--read-only`. `dryRun: true` returns the SQL only |
76
+ | `export_database` | SQL dump of the database (`schemaOnly`, `dataOnly`, `tables`): waits up to 5 minutes and returns a signed download link plus `atBookmark`, the point the dump reflects |
77
+ | `restore_database` | Time Travel restore to a `bookmark` or a `timestamp` (last 30 days). Destructive; hidden under `--read-only`. Returns `previousBookmark`, which undoes it |
78
+ | `get_speed_insights`, `set_speed_insights` | Speed Insights: each Web Vital's p75, the selected metric by page, device, browser, operating system, country, element and deployment, or one route's paths; the page load's p75 and the mean of each step (`pageLoad`); production or preview (`environment`); narrowed to one page, country, device, browser or operating system (`path`, `country`, `device`, `browser`, `os`); turn measuring on or off (not destructive: nothing is deleted) |
79
+ | `get_analytics` | Web Analytics: visitors and page views over `range` (24h, 7d or 30d), the trend, and the top 10 pages, referrers, countries, devices, browsers and operating systems by page views; narrowed to one value of each dimension (`path`, `referrer`, `country`, `device`, `browser`, `os`), all given must match. Production only: there is no `environment`, and one is refused. Read-only |
80
+ | `get_preview_protection`, `set_preview_protection` | Preview protection: whether the preview URL (`{subdomain}-preview`) is open to project members only, and whether a bypass secret is set; turn it on or off. **`set_preview_protection` is destructive** (turning protection off publishes unreleased work). Bypass secrets are made in the dashboard or with `naro protection bypass`, never by a tool, so they stay out of the conversation |
81
+ | `get_storage`, `enable_storage`, `disable_storage`, `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`, `delete_storage_bucket`, `list_storage_objects`, `create_storage_signed_url`, `delete_storage_objects` | Naro Storage: on or off, limits, usage, today's operations and whether uploads are stopped, and buckets; turn it on; turn it off and keep the files (`disable_storage` is destructive: `confirm` is the project's Storage bucket, `naro-storage-<projectId>`; the app's Storage answers 404 until `enable_storage`, and bucket writes answer `STORAGE_OFF`); make, change and remove buckets (`delete_storage_bucket` is destructive: `confirm` is the bucket's name, `empty: true` deletes its objects first; `update_storage_bucket` is destructive on its `public` argument, like `deploy` on `production`: `public: true` needs the same `confirm`); list a folder's files (`search` keeps the names that start with a text; a backend from before it answers `INVALID_REQUEST` — run `naro backend enable` once), make a signed link of up to an hour, and delete files (`delete_storage_objects` is destructive: `confirm` is the bucket's name) — through the control plane, so no service key reaches the MCP process. Files go in through naro-js or `naro storage cp`, not through MCP |
81
82
 
82
83
  Destructive tools say so in their descriptions; agents should confirm with
83
84
  the user before calling them. `set_preview_protection` is one of them: turning
@@ -109,7 +110,7 @@ strategy's failure is in the message.
109
110
  | `--features <a,b,…>` | `NARO_MCP_FEATURES` | Only these groups: `account`, `deployments`, `logs`, `env`, `domains`, `git`, `database`, `insights`, `protection`, `storage`. An unknown group is a startup error, not an empty server |
110
111
 
111
112
  Results of the `database` tools, of `get_speed_insights`, of `get_analytics`,
112
- of every Storage tool that answers bucket rows (`get_storage`, `enable_storage`,
113
+ of every Storage tool that answers bucket rows (`get_storage`, `enable_storage`, `disable_storage`,
113
114
  `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`) and of
114
115
  `list_storage_objects` are wrapped in `<untrusted-data-…>` boundaries with
115
116
  a random id per call — rows, identifiers and SQLite error messages are bytes
package/dist/index.js CHANGED
@@ -40,6 +40,14 @@ var NaroError = class extends Error {
40
40
  if (options2?.details !== void 0) this.details = options2.details;
41
41
  }
42
42
  };
43
+ var PROMOTE_TARGET_UNSUPPORTED_MESSAGE = "This control plane cannot promote to an environment yet. Deploy the latest web first, then try again.";
44
+ function promoteTargetUnsupported(cause) {
45
+ return new NaroError(
46
+ "PROMOTE_TARGET_UNSUPPORTED",
47
+ PROMOTE_TARGET_UNSUPPORTED_MESSAGE,
48
+ cause !== void 0 ? { cause } : void 0
49
+ );
50
+ }
43
51
  function isNaroError(error51) {
44
52
  return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
45
53
  }
@@ -142,7 +150,7 @@ var STYLES = `
142
150
  *{box-sizing:border-box}
143
151
  :root{
144
152
  --bg:#000;--surface:#0a0a0a;--surface-2:#111;--border:#232323;
145
- --text:#ededed;--muted:#a1a1a1;--faint:#6f6f6f;
153
+ --text:#ededed;--muted:#a1a1a1;--faint:#808080;
146
154
  --sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
147
155
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
148
156
  }
@@ -580,6 +588,9 @@ function schemaMigrationTitle(operation) {
580
588
  return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
581
589
  }
582
590
 
591
+ // ../../packages/core/src/platform-mail.ts
592
+ var encoder = new TextEncoder();
593
+
583
594
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
584
595
  var external_exports = {};
585
596
  __export(external_exports, {
@@ -15289,6 +15300,8 @@ function deploymentInfo(row) {
15289
15300
  errorMessage: row.error_message,
15290
15301
  gitCommitSha: row.git_commit_sha,
15291
15302
  gitCommitMessage: row.git_commit_message,
15303
+ pinnedCommitSha: row.pinned_commit_sha ?? null,
15304
+ rebuiltFromDeploymentId: row.rebuilt_from_deployment_id ?? null,
15292
15305
  createdAt: row.created_at,
15293
15306
  startedAt: row.started_at,
15294
15307
  finishedAt: row.finished_at
@@ -15540,16 +15553,65 @@ var DeploymentsApi = class {
15540
15553
  return { current, target: production.slice(1).find(differs) ?? null };
15541
15554
  }
15542
15555
  /**
15543
- * Redeploys an existing READY deployment's artifact to production.
15544
- * Used by both `naro promote` (preview → production) and `naro rollback`
15545
- * (older production build → production).
15556
+ * Promotes an existing READY deployment: with no `target` (or
15557
+ * `production`) it goes to production — `naro promote` (preview →
15558
+ * production) and `naro rollback` (older production build → production).
15559
+ * A deployment from an environment is rebuilt from its commit with
15560
+ * production's variables (`mode: "rebuild"`); any other is published as
15561
+ * stored (`mode: "reupload"`). With an environment `target` the control
15562
+ * plane likewise rebuilds the deployment's commit with that environment's
15563
+ * variables or publishes the stored build.
15564
+ *
15565
+ * No `target` sends no body, exactly as clients before targets did. A
15566
+ * control plane from before targets ignores `target` and promotes to
15567
+ * production, so an environment target is first checked with
15568
+ * `promoteCheck`: a 405 there (no such route) stops with
15569
+ * `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
15570
+ * trimmed once, as the control plane trims it, so `" production"` is
15571
+ * production and asks no preview.
15546
15572
  */
15547
- async promote(projectId, deploymentId) {
15573
+ async promote(projectId, deploymentId, options2 = {}) {
15574
+ const target = options2.target?.trim();
15575
+ if (target !== void 0 && target !== "production") {
15576
+ try {
15577
+ await this.promoteCheck(projectId, deploymentId, target);
15578
+ } catch (error51) {
15579
+ if (isNaroError(error51) && error51.status === 405) {
15580
+ throw promoteTargetUnsupported(error51);
15581
+ }
15582
+ throw error51;
15583
+ }
15584
+ }
15585
+ const body = await this.client.request(this.promotePath(projectId, deploymentId), {
15586
+ method: "POST",
15587
+ ...target !== void 0 ? { body: { target } } : {},
15588
+ context: "Promote deployment"
15589
+ });
15590
+ return {
15591
+ ...deploymentInfo(body.deployment),
15592
+ mode: body.mode ?? "reupload"
15593
+ };
15594
+ }
15595
+ /**
15596
+ * What promoting to `target` would do, changing nothing: how it gets its
15597
+ * build, the migrations the target's database has not applied (null when
15598
+ * they cannot be compared) and the warnings to show first. Answers 405 from
15599
+ * a control plane that predates promote targets.
15600
+ */
15601
+ async promoteCheck(projectId, deploymentId, target) {
15602
+ const query = target !== void 0 ? `?target=${encodeURIComponent(target)}` : "";
15548
15603
  const body = await this.client.request(
15549
- `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
15550
- { method: "POST", context: "Promote deployment" }
15604
+ `${this.promotePath(projectId, deploymentId)}${query}`,
15605
+ { context: "Check promotion" }
15551
15606
  );
15552
- return deploymentInfo(body.deployment);
15607
+ return {
15608
+ mode: body.mode ?? "reupload",
15609
+ pendingMigrations: body.pendingMigrations ?? null,
15610
+ warnings: body.warnings ?? []
15611
+ };
15612
+ }
15613
+ promotePath(projectId, deploymentId) {
15614
+ return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
15553
15615
  }
15554
15616
  /**
15555
15617
  * Stops an in-flight build (QUEUED through BUILDING) and marks the row
@@ -16304,6 +16366,7 @@ function storageInfo(body) {
16304
16366
  return {
16305
16367
  enabled: body.enabled,
16306
16368
  bucket: body.bucket ?? null,
16369
+ disabledAt: body.disabledAt ?? null,
16307
16370
  limits: {
16308
16371
  bytes: body.limits.bytes,
16309
16372
  objectBytes: body.limits.objectBytes,
@@ -16315,6 +16378,18 @@ function storageInfo(body) {
16315
16378
  buckets: (body.buckets ?? []).map(storageBucket)
16316
16379
  };
16317
16380
  }
16381
+ function storageRunFailure(error51) {
16382
+ const details = isNaroError(error51) ? error51.details : void 0;
16383
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
16384
+ return new NaroError(error51.code, error51.message, {
16385
+ ...error51.status !== void 0 ? { status: error51.status } : {},
16386
+ details: {
16387
+ ...details,
16388
+ backend: backendInfo(details.backend)
16389
+ }
16390
+ });
16391
+ return error51;
16392
+ }
16318
16393
  function bucketFields(fields, into = {}) {
16319
16394
  for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
16320
16395
  if (fields[key] !== void 0) into[key] = fields[key];
@@ -16743,16 +16818,45 @@ var StorageApi = class {
16743
16818
  }
16744
16819
  );
16745
16820
  } catch (error51) {
16746
- const details = isNaroError(error51) ? error51.details : void 0;
16747
- if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
16748
- throw new NaroError(error51.code, error51.message, {
16749
- ...error51.status !== void 0 ? { status: error51.status } : {},
16750
- details: {
16751
- ...details,
16752
- backend: backendInfo(details.backend)
16753
- }
16754
- });
16755
- throw error51;
16821
+ throw storageRunFailure(error51);
16822
+ }
16823
+ return storageInfo(body.storage);
16824
+ }
16825
+ /**
16826
+ * Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
16827
+ * uploaded again without the bucket, so the app's Storage calls and file
16828
+ * URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
16829
+ * bucket again. The bucket, its files and their storage cost stay; while
16830
+ * it is off, bucket writes and the object routes answer `409
16831
+ * STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
16832
+ * already off, comes back as it is, and a turn-off that stopped is
16833
+ * finished. Two minutes, like `enable`.
16834
+ *
16835
+ * Refusals, in the order they are checked: `BACKEND_DISABLED`,
16836
+ * `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
16837
+ * `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
16838
+ * change holds the backend — or turning Storage on again took this
16839
+ * turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
16840
+ * turn-off could not be recorded; nothing changed), and
16841
+ * `BACKEND_PROVISION_FAILED` (502) when it is
16842
+ * recorded but the worker's upload — or a step after it — stopped:
16843
+ * `details` holds `{ storage: { applied: false, enabled: false }, backend
16844
+ * }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
16845
+ * plane's Storage switch does not hold a removal back.
16846
+ */
16847
+ async disable(projectId) {
16848
+ let body;
16849
+ try {
16850
+ body = await this.client.request(
16851
+ this.path(projectId),
16852
+ {
16853
+ method: "DELETE",
16854
+ context: "Turn off Storage",
16855
+ timeoutMs: PROVISION_TIMEOUT_MS
16856
+ }
16857
+ );
16858
+ } catch (error51) {
16859
+ throw storageRunFailure(error51);
16756
16860
  }
16757
16861
  return storageInfo(body.storage);
16758
16862
  }
@@ -20630,7 +20734,7 @@ var DATABASE_TOOLS = [
20630
20734
  destructiveHint: true,
20631
20735
  idempotentHint: false
20632
20736
  },
20633
- description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20737
+ description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. An ALTER TABLE that adds a generated column computes it for the rows already there, as in \`apply_migration\`, and takes no params either. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20634
20738
  readOnlyVariant: {
20635
20739
  description: `Executes a single read-only SQL statement in the D1 database. This server runs with --read-only: writes, DDL and multi-statement batches are refused with DATABASE_READ_ONLY. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20636
20740
  annotations: {
@@ -20691,7 +20795,7 @@ var DATABASE_TOOLS = [
20691
20795
  destructiveHint: true,
20692
20796
  idempotentHint: false
20693
20797
  },
20694
- description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
20798
+ description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ALTER TABLE t ADD COLUMN c \u2026 AS (\u2026) computes the generated column c for the rows already in t, as Postgres does: a row it cannot be computed for (json_extract over text that is not JSON) fails the whole migration with SQLite's error (malformed JSON) and nothing is applied, so fix those rows in an earlier statement. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
20695
20799
  inputSchema: {
20696
20800
  type: "object",
20697
20801
  properties: {
@@ -21593,7 +21697,7 @@ var STORAGE_TOOLS = [
21593
21697
  },
21594
21698
  untrusted: true,
21595
21699
  untrustedSubject: "the Storage query",
21596
- description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
21700
+ description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), disabledAt \u2014 when Storage was turned off with its files kept (then enabled is false, and enable_storage turns it on again only when the user asks) \u2014 ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
21597
21701
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21598
21702
  handler: async (args, deps) => {
21599
21703
  const projectId = await projectOf(args, deps);
@@ -21613,7 +21717,7 @@ var STORAGE_TOOLS = [
21613
21717
  },
21614
21718
  untrusted: true,
21615
21719
  untrustedSubject: "the Storage query",
21616
- description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Safe to call again: it resumes a run that stopped and applies a plan change's limits. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
21720
+ description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Turned off with disable_storage, it binds the same bucket again and the files come back. Safe to call again: it resumes a run that stopped and applies a plan change's limits. Storage that is off (disabledAt) was turned off on purpose: turn it on again only when the user asks. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
21617
21721
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21618
21722
  handler: async (args, deps) => {
21619
21723
  const projectId = await projectOf(args, deps);
@@ -21623,6 +21727,40 @@ var STORAGE_TOOLS = [
21623
21727
  };
21624
21728
  }
21625
21729
  },
21730
+ {
21731
+ name: "disable_storage",
21732
+ group: "storage",
21733
+ annotations: {
21734
+ readOnlyHint: false,
21735
+ destructiveHint: true,
21736
+ idempotentHint: true
21737
+ },
21738
+ untrusted: true,
21739
+ untrustedSubject: "the Storage query",
21740
+ description: `${DESTRUCTIVE}turns Naro Storage off for the project and keeps its files: the backend is uploaded again without its R2 bucket, which can take up to 2 minutes, and from then on the app's Storage calls and file URLs answer 404 until enable_storage turns it on again with the same bucket. The buckets and files stay, and so does their storage cost; while it is off, bucket writes and the file tools answer STORAGE_OFF and get_storage shows disabledAt. To delete the files instead, call delete_storage_bucket with empty: true while Storage is still on. Needs confirm set to the project's Storage bucket as get_storage shows it (naro-storage-<projectId>) \u2014 confirm with the user first. Storage that was never turned on is answered as it is; calling it on Storage that is already off finishes a turn-off that stopped. The project's backend must be on (BACKEND_OFF otherwise). It answers the Storage state as get_storage does. ${UNTRUSTED_BUCKETS}`,
21741
+ inputSchema: {
21742
+ type: "object",
21743
+ properties: {
21744
+ ...PROJECT_ARGS,
21745
+ confirm: {
21746
+ type: "string",
21747
+ description: "The project's Storage bucket, as get_storage shows it (naro-storage-<projectId>). Needed while Storage is on."
21748
+ }
21749
+ }
21750
+ },
21751
+ handler: async (args, deps) => {
21752
+ const projectId = await projectOf(args, deps);
21753
+ const client = await deps.client();
21754
+ const current = await client.storage.info(projectId);
21755
+ if (current.bucket === null) return { projectId, storage: current };
21756
+ if (current.disabledAt === null && args.confirm !== current.bucket)
21757
+ throw new NaroError(
21758
+ "CONFIRMATION_REQUIRED",
21759
+ `disable_storage needs confirm set to the project's Storage bucket, ${current.bucket}.`
21760
+ );
21761
+ return { projectId, storage: await client.storage.disable(projectId) };
21762
+ }
21763
+ },
21626
21764
  {
21627
21765
  name: "list_storage_buckets",
21628
21766
  group: "storage",
@@ -21653,7 +21791,7 @@ var STORAGE_TOOLS = [
21653
21791
  },
21654
21792
  untrusted: true,
21655
21793
  untrustedSubject: "the Storage query",
21656
- description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage is off: call enable_storage first. Objects go in with naro-js or naro storage cp, under the storage.objects policies. It answers the bucket as stored. ${UNTRUSTED_BUCKETS}`,
21794
+ description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage was never turned on: call enable_storage first. STORAGE_OFF means the user turned Storage off on purpose (disable_storage): tell the user, and call enable_storage only if they ask to turn it back on. Objects go in with naro-js or naro storage cp, under the storage.objects policies. It answers the bucket as stored. ${UNTRUSTED_BUCKETS}`,
21657
21795
  inputSchema: {
21658
21796
  type: "object",
21659
21797
  properties: { ...PROJECT_ARGS, name: NAME, ...FIELDS },
@@ -21984,6 +22122,18 @@ function envTarget(value) {
21984
22122
  `Invalid target ${JSON.stringify(value)} \u2014 use production, preview, all or an environment name.`
21985
22123
  );
21986
22124
  }
22125
+ var REBUILD_TIMEOUT_MS = 15 * 6e4;
22126
+ var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
22127
+ function promoteTarget(value) {
22128
+ if (value === void 0 || value === null) return void 0;
22129
+ const name = typeof value === "string" ? value.trim() : "";
22130
+ if (name === "" || name === "preview")
22131
+ throw new NaroError(
22132
+ "VALIDATION_FAILED",
22133
+ "target must be production or an environment name."
22134
+ );
22135
+ return name;
22136
+ }
21987
22137
  var REGISTERED = [
21988
22138
  {
21989
22139
  name: "whoami",
@@ -22607,7 +22757,7 @@ var REGISTERED = [
22607
22757
  destructiveHint: true,
22608
22758
  idempotentHint: false
22609
22759
  },
22610
- description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build) to production. Production traffic switches to it once READY.`,
22760
+ description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build). Without target it goes to production, and production traffic switches to it once READY; with target set to an environment's name (list_environments) it goes to that environment instead. The result's mode says how it got its build: reupload publishes the stored build again, rebuild builds the same commit again with the target's variables (a promotion between surfaces when one of them is an environment, such as preview \u2192 qa or qa \u2192 production) and only works for a deployment built from Git (PROMOTE_NEEDS_GIT_SOURCE otherwise). target in the result is null when it was left out. If the call times out on the client side, the promotion keeps going: look it up with list_deployments or get_deployment. A control plane that predates promote targets answers PROMOTE_TARGET_UNSUPPORTED and promotes nothing: ask the user to deploy the latest web first.`,
22611
22761
  inputSchema: {
22612
22762
  type: "object",
22613
22763
  properties: {
@@ -22615,6 +22765,10 @@ var REGISTERED = [
22615
22765
  deploymentId: {
22616
22766
  type: "string",
22617
22767
  description: "The READY deployment to promote."
22768
+ },
22769
+ target: {
22770
+ type: "string",
22771
+ description: `Where to promote: production (the default when omitted; switches production traffic to the deployment), or an environment name such as qa (list_environments); preview is not a target. Promoting between surfaces when one of them is an environment (preview \u2192 qa, qa \u2192 production) rebuilds the same commit with the target surface's variables; a rebuild takes up to 15 minutes.`
22618
22772
  }
22619
22773
  },
22620
22774
  required: ["deploymentId"]
@@ -22623,6 +22777,7 @@ var REGISTERED = [
22623
22777
  const deploymentId = str(args.deploymentId);
22624
22778
  if (!deploymentId)
22625
22779
  throw new NaroError("VALIDATION_FAILED", "deploymentId is required.");
22780
+ const target = promoteTarget(args.target);
22626
22781
  const projectId = await deps.resolveProjectId({
22627
22782
  projectId: str(args.projectId),
22628
22783
  directory: str(args.directory)
@@ -22630,17 +22785,20 @@ var REGISTERED = [
22630
22785
  const client = await deps.client();
22631
22786
  const promoted = await client.deployments.promote(
22632
22787
  projectId,
22633
- deploymentId
22788
+ deploymentId,
22789
+ target !== void 0 ? { target } : void 0
22634
22790
  );
22635
22791
  const detail = await client.deployments.wait(promoted.id, {
22636
- timeoutMs: 5 * 6e4
22792
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22637
22793
  });
22638
22794
  return {
22639
22795
  deploymentId: promoted.id,
22640
22796
  sourceDeploymentId: deploymentId,
22641
22797
  status: detail.deployment.status,
22642
22798
  url: detail.deployment.url,
22643
- errorCode: detail.deployment.errorCode
22799
+ errorCode: detail.deployment.errorCode,
22800
+ target: target ?? null,
22801
+ mode: promoted.mode
22644
22802
  };
22645
22803
  }
22646
22804
  },
@@ -22687,14 +22845,15 @@ var REGISTERED = [
22687
22845
  }
22688
22846
  const promoted = await client.deployments.promote(projectId, target);
22689
22847
  const detail = await client.deployments.wait(promoted.id, {
22690
- timeoutMs: 5 * 6e4
22848
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22691
22849
  });
22692
22850
  return {
22693
22851
  deploymentId: promoted.id,
22694
22852
  rolledBackTo: target,
22695
22853
  status: detail.deployment.status,
22696
22854
  url: detail.deployment.url,
22697
- errorCode: detail.deployment.errorCode
22855
+ errorCode: detail.deployment.errorCode,
22856
+ mode: promoted.mode
22698
22857
  };
22699
22858
  }
22700
22859
  },
@@ -22851,7 +23010,7 @@ async function openClaims(client, projectId) {
22851
23010
  }
22852
23011
 
22853
23012
  // src/server.ts
22854
- var SERVER_VERSION = "0.18.1";
23013
+ var SERVER_VERSION = "0.19.0";
22855
23014
  function visibleTools(options2) {
22856
23015
  const { features, readOnly } = options2;
22857
23016
  return TOOLS.flatMap((tool) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-mcp",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro-mcp": "./dist/index.js"
@@ -18,15 +18,15 @@
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.0.0",
20
20
  "@supabase/supabase-js": "2.112.3",
21
- "naro-cli": "0.19.1"
21
+ "naro-cli": "0.20.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "tsup": "8.5.0",
25
25
  "tsx": "4.23.12",
26
26
  "typescript": "7.0.2",
27
27
  "vitest": "3.2.7",
28
- "@next-deploy/sdk": "0.1.0",
29
- "@next-deploy/config": "0.1.0"
28
+ "@next-deploy/config": "0.1.0",
29
+ "@next-deploy/sdk": "0.1.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup",