@mcpcloud/cli 0.9.1 → 0.10.1-next-20260715014243

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 +350 -142
  2. package/dist/index.js +576 -205
  3. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1972,6 +1972,9 @@ function configFile() {
1972
1972
  return join(configDir(), "config.json");
1973
1973
  }
1974
1974
  var DEFAULT_PROFILE_NAME = "default";
1975
+ var DEFAULT_BASE_URL = "https://majestic-lobster-858.convex.site";
1976
+ var PRODUCTION_BASE_URL = DEFAULT_BASE_URL;
1977
+ var DEFAULT_APP_URL = "https://mcpcloud.sh";
1975
1978
 
1976
1979
  class MissingApiKeyError extends Error {
1977
1980
  constructor() {
@@ -2100,6 +2103,14 @@ function getActiveProfileName() {
2100
2103
  const first = Object.keys(persisted.profiles ?? {})[0];
2101
2104
  return first ?? saved;
2102
2105
  }
2106
+ function resolveActiveProfile() {
2107
+ if (profileOverride)
2108
+ return { name: profileOverride, source: "flag" };
2109
+ const fromEnv = process.env["MCPCLOUD_PROFILE"]?.trim();
2110
+ if (fromEnv)
2111
+ return { name: fromEnv, source: "env" };
2112
+ return { name: getActiveProfileName(), source: "config" };
2113
+ }
2103
2114
  function getActiveProfile(persisted, name) {
2104
2115
  return persisted.profiles?.[name] ?? {};
2105
2116
  }
@@ -2121,11 +2132,26 @@ function writeConfig(config) {
2121
2132
  function getApiKey() {
2122
2133
  return process.env["MCPCLOUD_API_KEY"] ?? readConfig().apiKey;
2123
2134
  }
2135
+ function resolveBaseUrl() {
2136
+ if (baseUrlOverride)
2137
+ return { url: baseUrlOverride, source: "flag" };
2138
+ const env = process.env["MCPCLOUD_BASE_URL"]?.trim();
2139
+ if (env)
2140
+ return { url: env, source: "env" };
2141
+ const profile = readConfig().baseUrl;
2142
+ if (profile)
2143
+ return { url: profile, source: "profile" };
2144
+ return { url: DEFAULT_BASE_URL, source: "default" };
2145
+ }
2124
2146
  function getBaseUrl() {
2125
- const url = baseUrlOverride ?? process.env["MCPCLOUD_BASE_URL"] ?? readConfig().baseUrl;
2126
- if (!url)
2127
- throw new MissingBaseUrlError;
2128
- return url;
2147
+ return resolveBaseUrl().url;
2148
+ }
2149
+ function isProductionBaseUrl(url) {
2150
+ try {
2151
+ return new URL(url).host === new URL(PRODUCTION_BASE_URL).host;
2152
+ } catch {
2153
+ return false;
2154
+ }
2129
2155
  }
2130
2156
  function requireApiKey() {
2131
2157
  const key = getApiKey();
@@ -2152,10 +2178,10 @@ function getAppUrl() {
2152
2178
  if (host === "localhost" || host === "127.0.0.1")
2153
2179
  return "http://localhost:4000";
2154
2180
  if (host.endsWith(".convex.site") || host.endsWith(".convex.cloud"))
2155
- return "https://mcpcloud.sh";
2181
+ return DEFAULT_APP_URL;
2156
2182
  } catch {}
2157
2183
  }
2158
- return "https://mcpcloud.sh";
2184
+ return DEFAULT_APP_URL;
2159
2185
  }
2160
2186
  function listProfiles() {
2161
2187
  const persisted = readPersistedConfig();
@@ -2386,12 +2412,20 @@ function getRetryDelayMs(err) {
2386
2412
  }
2387
2413
  return 250 + Math.floor(Math.random() * 250);
2388
2414
  }
2415
+ var GATEWAY_STATUS_MESSAGES = {
2416
+ 502: "The gateway returned no response (HTTP 502 Bad Gateway). A long-running request — such as a first deploy that is still generating and uploading the bundle — can exceed the gateway timeout even though the operation keeps running server-side. Retry in a few seconds; for a deploy, re-run with `--wait` or follow progress with `mcp deployments logs <id> --follow`.",
2417
+ 503: "The service is temporarily unavailable (HTTP 503 Service Unavailable). This is usually transient — retry in a few seconds.",
2418
+ 504: "The gateway timed out waiting for the server (HTTP 504 Gateway Timeout). A long-running request may still be processing server-side. Retry in a few seconds; for a deploy, re-run with `--wait` or follow progress with `mcp deployments logs <id> --follow`."
2419
+ };
2420
+ function synthesizeStatusMessage(status, statusText) {
2421
+ return GATEWAY_STATUS_MESSAGES[status] ?? (statusText || `HTTP ${status}`);
2422
+ }
2389
2423
  async function parseErrorBody(res) {
2390
2424
  const headerRequestId = res.headers.get("x-request-id") ?? undefined;
2391
2425
  const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
2392
2426
  const fallback = {
2393
2427
  code: `http_${res.status}`,
2394
- message: res.statusText || `HTTP ${res.status}`
2428
+ message: synthesizeStatusMessage(res.status, res.statusText)
2395
2429
  };
2396
2430
  if (headerRequestId)
2397
2431
  fallback.requestId = headerRequestId;
@@ -2834,7 +2868,12 @@ var ERROR_REMEDIATION = {
2834
2868
  invalid_status: "Invalid --status. Check the command help for accepted values.",
2835
2869
  deployment_not_found: "No such deployment in this org. Verify with `mcp servers list`.",
2836
2870
  server_not_found: "No such server in this org. Verify with `mcp servers list`.",
2871
+ artifact_not_found: "No such server/artifact in that project. If you switched servers, stale .mcpcloud/state.json can cause this — run `mcp dev init` to re-select.",
2872
+ bundle_not_found: "The server has no generated bundle yet. Run `mcp servers generate <id>` or deploy once, then retry.",
2837
2873
  missing_deployment_id: "Pass --deployment <id> or look it up with `mcp servers list`.",
2874
+ http_502: "The API gateway dropped the request. Retry once; if it persists, check https://mcpcloud.sh/status.",
2875
+ http_503: "Service temporarily unavailable. Honor Retry-After if present, then retry.",
2876
+ http_504: "The request timed out at the gateway. Retry; long operations may still be running — check with the matching `get` command.",
2838
2877
  rate_limited: "Rate limit hit. Honor the Retry-After header and retry.",
2839
2878
  rate_limit_exceeded: "Rate limit hit. Wait the Retry-After interval before retrying.",
2840
2879
  quota_exceeded: "Plan quota exceeded. Check usage in the dashboard or upgrade.",
@@ -2868,6 +2907,14 @@ function getRemediation(code) {
2868
2907
  return;
2869
2908
  return ERROR_REMEDIATION[code];
2870
2909
  }
2910
+ var DOCS_ORIGIN = "https://mcpcloud.sh";
2911
+ function absolutizeDocsUrl(url) {
2912
+ if (!url)
2913
+ return url;
2914
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url))
2915
+ return url;
2916
+ return `${DOCS_ORIGIN}${url.startsWith("/") ? "" : "/"}${url}`;
2917
+ }
2871
2918
  function describeError(err) {
2872
2919
  if (err instanceof McpCloudApiError) {
2873
2920
  const out = {
@@ -2877,8 +2924,11 @@ function describeError(err) {
2877
2924
  };
2878
2925
  if (err.error.requestId)
2879
2926
  out.requestId = err.error.requestId;
2880
- if (err.error.docsUrl)
2881
- out.docsUrl = err.error.docsUrl;
2927
+ if (err.error.docsUrl) {
2928
+ const docsUrl = absolutizeDocsUrl(err.error.docsUrl);
2929
+ if (docsUrl)
2930
+ out.docsUrl = docsUrl;
2931
+ }
2882
2932
  if (err.error.details)
2883
2933
  out.details = err.error.details;
2884
2934
  return out;
@@ -3371,6 +3421,16 @@ function previewKey(key) {
3371
3421
  return `${key.slice(0, 4)}…`;
3372
3422
  return `${key.slice(0, 8)}…${key.slice(-4)}`;
3373
3423
  }
3424
+ function profileSourceLabel(source) {
3425
+ switch (source) {
3426
+ case "flag":
3427
+ return "--profile flag";
3428
+ case "env":
3429
+ return "MCPCLOUD_PROFILE";
3430
+ case "config":
3431
+ return "config default";
3432
+ }
3433
+ }
3374
3434
  function registerAuthCommands(program2) {
3375
3435
  program2.command("login").description("Sign in via browser (default) or paste an API key").option("--key <apiKey>", "Save this key directly (skips browser + prompt)").option("--paste", "Skip the browser flow and prompt for an API key instead").option("--no-browser", "Alias for --paste").option("--port <number>", "Loopback port to receive the callback (default: auto)").option("--app-url <url>", "Override the dashboard origin for this run (default: from MCPCLOUD_APP_URL or config)").option("--timeout <seconds>", "How long to wait for the browser flow (default 300)").addHelpText("after", [
3376
3436
  "",
@@ -3510,12 +3570,17 @@ function registerAuthCommands(program2) {
3510
3570
  `)).action(runAction(async () => {
3511
3571
  const me = await api.get("/api/v1/me");
3512
3572
  const orgs = await api.get("/api/v1/organizations");
3573
+ const profile = resolveActiveProfile();
3574
+ const baseUrl = getBaseUrl();
3575
+ const isProd = isProductionBaseUrl(baseUrl);
3513
3576
  if (isJsonMode()) {
3514
3577
  printJson({
3515
3578
  user: me.user,
3516
3579
  authMethod: me.authMethod,
3517
3580
  apiKey: me.apiKey,
3518
- baseUrl: getBaseUrl(),
3581
+ profile: { name: profile.name, source: profile.source },
3582
+ baseUrl,
3583
+ isProductionBaseUrl: isProd,
3519
3584
  defaultOrganizationId: me.defaultOrganizationId,
3520
3585
  organizations: orgs.organizations
3521
3586
  });
@@ -3526,7 +3591,8 @@ function registerAuthCommands(program2) {
3526
3591
  user: me.user.email ?? me.user.name ?? me.user.id,
3527
3592
  "auth method": me.authMethod ?? "—",
3528
3593
  "api key": apiKeyDisplay,
3529
- "base url": getBaseUrl(),
3594
+ profile: `${profile.name} (${profileSourceLabel(profile.source)})`,
3595
+ "base url": isProd ? `${baseUrl} (production)` : baseUrl,
3530
3596
  "default org": me.defaultOrganizationId ?? "—",
3531
3597
  "org count": orgs.organizations.length
3532
3598
  });
@@ -3875,18 +3941,68 @@ var TERMINAL_STAGES = new Set([
3875
3941
  "terminated"
3876
3942
  ]);
3877
3943
  var TERMINAL_LEVELS = new Set(["success", "failure", "error"]);
3944
+ var TERMINAL_DEPLOYMENT_STATUSES = new Set([
3945
+ "active",
3946
+ "failed",
3947
+ "paused",
3948
+ "undeployed"
3949
+ ]);
3950
+ function isTerminalDeploymentStatus(status) {
3951
+ return status !== undefined && TERMINAL_DEPLOYMENT_STATUSES.has(status);
3952
+ }
3878
3953
  function isTerminalEvent(event) {
3879
3954
  if (TERMINAL_STAGES.has(event.stage))
3880
3955
  return true;
3881
3956
  if (TERMINAL_LEVELS.has(event.level))
3882
3957
  return true;
3883
3958
  const m = event.message.toLowerCase();
3884
- if (m.includes("deployment active"))
3959
+ if (m.includes("deployment is active") || m.includes("deployment active"))
3960
+ return true;
3961
+ if (m.includes("deployment failed"))
3885
3962
  return true;
3886
3963
  if (m.includes("deploy succeeded") || m.includes("deploy failed"))
3887
3964
  return true;
3888
3965
  return false;
3889
3966
  }
3967
+ var SUCCESS_STAGES = new Set(["active"]);
3968
+ var SUCCESS_LEVELS = new Set(["success"]);
3969
+ function isSuccessEvent(event) {
3970
+ if (SUCCESS_STAGES.has(event.stage))
3971
+ return true;
3972
+ if (SUCCESS_LEVELS.has(event.level))
3973
+ return true;
3974
+ const m = event.message.toLowerCase();
3975
+ if (m.includes("deployment active") || m.includes("deploy succeeded"))
3976
+ return true;
3977
+ return false;
3978
+ }
3979
+ function isFailureEvent(event) {
3980
+ return isTerminalEvent(event) && !isSuccessEvent(event);
3981
+ }
3982
+ function formatEventDetails(details) {
3983
+ if (details == null)
3984
+ return null;
3985
+ if (typeof details === "string") {
3986
+ const trimmed = details.trim();
3987
+ return trimmed.length > 0 ? trimmed : null;
3988
+ }
3989
+ if (typeof details === "object") {
3990
+ const record = details;
3991
+ for (const key of ["error", "message", "reason", "detail"]) {
3992
+ const value = record[key];
3993
+ if (typeof value === "string" && value.trim().length > 0) {
3994
+ return value.trim();
3995
+ }
3996
+ }
3997
+ try {
3998
+ const json = JSON.stringify(details);
3999
+ return json && json !== "{}" ? json : null;
4000
+ } catch {
4001
+ return null;
4002
+ }
4003
+ }
4004
+ return null;
4005
+ }
3890
4006
  async function followDeploymentEvents(args) {
3891
4007
  const isDone = args.isDone ?? isTerminalEvent;
3892
4008
  const totalTimeoutMs = args.totalTimeoutMs ?? 5 * 60000;
@@ -3922,7 +4038,9 @@ function formatEventLine(event) {
3922
4038
  const ts = new Date(event.timestamp).toISOString();
3923
4039
  const level = (event.level || "info").toUpperCase().padEnd(7);
3924
4040
  const stage = (event.stage || "").padEnd(12);
3925
- return `[${ts}] ${level} ${stage} ${event.message}`;
4041
+ const base = `[${ts}] ${level} ${stage} ${event.message}`;
4042
+ const details = formatEventDetails(event.details);
4043
+ return details && details !== event.message ? `${base} — ${details}` : base;
3926
4044
  }
3927
4045
  async function tailDeploymentLogs(args) {
3928
4046
  const params = {
@@ -4456,11 +4574,12 @@ function validateChoice(name, value, allowed) {
4456
4574
  }
4457
4575
  return v;
4458
4576
  }
4459
- function formatEventLine2(event) {
4460
- const ts = new Date(event.timestamp).toISOString();
4461
- const level = event.level.padEnd(7);
4462
- const stage = event.stage.padEnd(12);
4463
- return `[${ts}] ${level} ${stage} ${event.message}`;
4577
+ function reportDeployFailure(terminalEvent) {
4578
+ if (!terminalEvent || !isFailureEvent(terminalEvent))
4579
+ return false;
4580
+ const reason = formatEventDetails(terminalEvent.details);
4581
+ printError(`Deploy failed: ${reason ?? terminalEvent.message}`);
4582
+ return true;
4464
4583
  }
4465
4584
  function registerServerLifecycleCommands(servers) {
4466
4585
  servers.command("deploy <serverId>").description("Deploy a server's generated bundle to Cloudflare Workers (closes the spec → deploy loop)").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Project ID (defaults to the server's project)").option("--target <target>", `Deploy target: ${DEPLOY_TARGETS.join(" | ")}`, "workersDev").option("--access-mode <mode>", `Access mode: ${ACCESS_MODES.join(" | ")}`, "public").option("--upstream-base-url <url>", "Upstream API base URL bound into the worker").option("--upstream-api-key <key>", "Upstream API key (encrypted at rest server-side)").option("--upstream-api-key-header <header>", "Header name to inject the upstream API key on (default: Authorization)").option("--upstream-api-key-prefix <prefix>", 'Prefix for the upstream API key value (e.g. "Bearer ")').option("--wait", "Tail deployment events until the deploy reaches a terminal state").option("--wait-timeout <seconds>", "Maximum seconds to wait for terminal state (default 300)", "300").addHelpText("after", [
@@ -4513,36 +4632,41 @@ function registerServerLifecycleCommands(servers) {
4513
4632
  let terminalEvent = null;
4514
4633
  let waitDone = !opts.wait;
4515
4634
  if (opts.wait) {
4516
- const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
4517
- if (!isJsonMode()) {
4518
- printStep(`Following events for ${c.bold(dep.deploymentId)} (timeout ${Math.round(totalTimeoutMs / 1000)}s)…`);
4519
- }
4520
- const result = await followDeploymentEvents({
4521
- organizationId: orgId,
4522
- deploymentId: dep.deploymentId,
4523
- totalTimeoutMs,
4524
- onEvent: (event) => {
4525
- if (!isJsonMode()) {
4526
- printInfo(formatEventLine2(event));
4635
+ if (isTerminalDeploymentStatus(dep.status)) {
4636
+ waitDone = true;
4637
+ } else {
4638
+ const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
4639
+ if (!isJsonMode()) {
4640
+ printStep(`Following events for ${c.bold(dep.deploymentId)} (timeout ${Math.round(totalTimeoutMs / 1000)}s)…`);
4641
+ }
4642
+ const result = await followDeploymentEvents({
4643
+ organizationId: orgId,
4644
+ deploymentId: dep.deploymentId,
4645
+ totalTimeoutMs,
4646
+ onEvent: (event) => {
4647
+ if (!isJsonMode()) {
4648
+ printInfo(formatEventLine(event));
4649
+ }
4527
4650
  }
4651
+ });
4652
+ terminalEvent = result.terminalEvent;
4653
+ waitDone = result.done;
4654
+ if (!waitDone && !isJsonMode()) {
4655
+ printError(`Deploy did not reach a terminal state within ${Math.round(totalTimeoutMs / 1000)}s. Check \`mcp deployments get ${dep.deploymentId}\` and \`mcp deployments health ${dep.deploymentId}\`, or tail \`mcp deployments logs ${dep.deploymentId} --follow\`.`);
4528
4656
  }
4529
- });
4530
- terminalEvent = result.terminalEvent;
4531
- waitDone = result.done;
4532
- if (!waitDone && !isJsonMode()) {
4533
- printError(`Deploy did not reach a terminal state within ${Math.round(totalTimeoutMs / 1000)}s. Run \`mcp --json servers test-runs get ${dep.deploymentId}\` (or the dashboard) to keep watching.`);
4534
4657
  }
4535
4658
  }
4659
+ const deployFailed = dep.status === "failed" || Boolean(terminalEvent && isFailureEvent(terminalEvent));
4536
4660
  if (isJsonMode()) {
4537
4661
  printJson({ deployment: dep, waitDone, terminalEvent });
4538
- if (opts.wait && !waitDone)
4662
+ if (opts.wait && (!waitDone || deployFailed))
4539
4663
  throw new CliExitError(1);
4540
4664
  return;
4541
4665
  }
4542
4666
  printSuccess(`Deployment ${c.bold(dep.deploymentId)} created.`);
4543
4667
  printKeyValue({
4544
4668
  id: dep.deploymentId,
4545
- url: dep.deploymentUrl,
4669
+ ...dep.deploymentUrl ? { url: dep.deploymentUrl } : {},
4546
4670
  target: dep.target,
4547
4671
  "access mode": dep.accessMode,
4548
4672
  status: dep.status,
@@ -4552,8 +4676,17 @@ function registerServerLifecycleCommands(servers) {
4552
4676
  "terminal at": formatDate(terminalEvent.timestamp)
4553
4677
  } : {}
4554
4678
  });
4679
+ if (!opts.wait && dep.status === "queued") {
4680
+ printInfo(`Deploy is running in the background — follow with \`mcp deployments get ${dep.deploymentId}\` or rerun with --wait.`);
4681
+ }
4555
4682
  if (opts.wait && !waitDone)
4556
4683
  throw new CliExitError(1);
4684
+ if (deployFailed) {
4685
+ if (!reportDeployFailure(terminalEvent)) {
4686
+ printError("Deploy failed.");
4687
+ }
4688
+ throw new CliExitError(1);
4689
+ }
4557
4690
  }));
4558
4691
  servers.command("push-spec <serverId>").description("Push a local OpenAPI spec to the cloud (regenerates the bundle; optionally deploys)").option("--org <organizationId>", "Organization ID").option("--spec <path>", "Path to the OpenAPI/Swagger spec file", "./openapi.yaml").option("--enrich-on-change", "Preserve per-tool enrichment when the spec changes").option("--deploy", "Immediately deploy the regenerated bundle to Cloudflare Workers").option("--target <target>", `Deploy target (with --deploy): ${DEPLOY_TARGETS.join(" | ")}`, "workersDev").option("--access-mode <mode>", `Access mode (with --deploy): ${ACCESS_MODES.join(" | ")}`, "public").option("--wait", "With --deploy: tail deployment events until terminal state").option("--wait-timeout <seconds>", "Maximum seconds to wait for terminal state (default 300)", "300").addHelpText("after", [
4559
4692
  "",
@@ -4643,25 +4776,30 @@ function registerServerLifecycleCommands(servers) {
4643
4776
  let terminalEvent = null;
4644
4777
  let waitDone = !opts.wait;
4645
4778
  if (opts.wait) {
4646
- const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
4647
- if (!isJsonMode()) {
4648
- printStep(`Following events for ${c.bold(dep.deploymentId)}…`);
4649
- }
4650
- const r = await followDeploymentEvents({
4651
- organizationId: orgId,
4652
- deploymentId: dep.deploymentId,
4653
- totalTimeoutMs,
4654
- onEvent: (event) => {
4655
- if (!isJsonMode())
4656
- printInfo(formatEventLine2(event));
4779
+ if (isTerminalDeploymentStatus(dep.status)) {
4780
+ waitDone = true;
4781
+ } else {
4782
+ const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
4783
+ if (!isJsonMode()) {
4784
+ printStep(`Following events for ${c.bold(dep.deploymentId)}…`);
4785
+ }
4786
+ const r = await followDeploymentEvents({
4787
+ organizationId: orgId,
4788
+ deploymentId: dep.deploymentId,
4789
+ totalTimeoutMs,
4790
+ onEvent: (event) => {
4791
+ if (!isJsonMode())
4792
+ printInfo(formatEventLine(event));
4793
+ }
4794
+ });
4795
+ terminalEvent = r.terminalEvent;
4796
+ waitDone = r.done;
4797
+ if (!waitDone && !isJsonMode()) {
4798
+ printError(`Deploy did not reach a terminal state within ${Math.round(totalTimeoutMs / 1000)}s. Check \`mcp deployments get ${dep.deploymentId}\` and \`mcp deployments health ${dep.deploymentId}\`, or tail \`mcp deployments logs ${dep.deploymentId} --follow\`.`);
4657
4799
  }
4658
- });
4659
- terminalEvent = r.terminalEvent;
4660
- waitDone = r.done;
4661
- if (!waitDone && !isJsonMode()) {
4662
- printError(`Deploy did not reach a terminal state within ${Math.round(totalTimeoutMs / 1000)}s.`);
4663
4800
  }
4664
4801
  }
4802
+ const deployFailed = dep.status === "failed" || Boolean(terminalEvent && isFailureEvent(terminalEvent));
4665
4803
  if (isJsonMode()) {
4666
4804
  printJson({
4667
4805
  pushed: true,
@@ -4671,14 +4809,14 @@ function registerServerLifecycleCommands(servers) {
4671
4809
  waitDone,
4672
4810
  terminalEvent
4673
4811
  });
4674
- if (opts.wait && !waitDone)
4812
+ if (opts.wait && (!waitDone || deployFailed))
4675
4813
  throw new CliExitError(1);
4676
4814
  return;
4677
4815
  }
4678
4816
  printSuccess(`Deployment ${c.bold(dep.deploymentId)} created.`);
4679
4817
  printKeyValue({
4680
4818
  id: dep.deploymentId,
4681
- url: dep.deploymentUrl,
4819
+ ...dep.deploymentUrl ? { url: dep.deploymentUrl } : {},
4682
4820
  target: dep.target,
4683
4821
  "access mode": dep.accessMode,
4684
4822
  status: dep.status,
@@ -4688,8 +4826,17 @@ function registerServerLifecycleCommands(servers) {
4688
4826
  "terminal at": formatDate(terminalEvent.timestamp)
4689
4827
  } : {}
4690
4828
  });
4829
+ if (!opts.wait && dep.status === "queued") {
4830
+ printInfo(`Deploy is running in the background — follow with \`mcp deployments get ${dep.deploymentId}\` or rerun with --wait.`);
4831
+ }
4691
4832
  if (opts.wait && !waitDone)
4692
4833
  throw new CliExitError(1);
4834
+ if (deployFailed) {
4835
+ if (!reportDeployFailure(terminalEvent)) {
4836
+ printError("Deploy failed.");
4837
+ }
4838
+ throw new CliExitError(1);
4839
+ }
4693
4840
  }));
4694
4841
  servers.command("regenerate <serverId>").description("Re-fetch a server's source spec and rebuild its tools with the current codegen (preserves customizations). Wraps POST /api/v1/server/regenerate.").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Project ID (defaults to the server's project)").option("--source-url <url>", "Override the stored spec source URL (needed for GraphQL servers whose introspection endpoint wasn't persisted)").addHelpText("after", [
4695
4842
  "",
@@ -5533,6 +5680,9 @@ import { dirname as dirname5, join as join8 } from "node:path";
5533
5680
  // src/lib/dev/state.ts
5534
5681
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
5535
5682
  import { join as join6, resolve as resolve5 } from "node:path";
5683
+ function cachedStateMatchesTarget(cached2, target) {
5684
+ return cached2 !== null && cached2.serverId === target.serverId && cached2.organizationId === target.organizationId;
5685
+ }
5536
5686
  function devRoot(cwd) {
5537
5687
  return join6(cwd, ".mcpcloud");
5538
5688
  }
@@ -7441,7 +7591,7 @@ function diffLine2(label, applied, suggested) {
7441
7591
  // src/commands/tools.ts
7442
7592
  function registerToolCommands(program2) {
7443
7593
  const tools = program2.command("tools").description("Inspect and edit tools on a project or server");
7444
- tools.command("list").description("List tools for a project (or pass --server to look up its project automatically)").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Project ID to list tools for").option("--server <serverId>", "Server ID the CLI fetches the server's projectId before listing tools").action(runAction(async (opts) => {
7594
+ tools.command("list").description("List tools for a project, or scope to a single server with --server").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "List every tool in this project").option("--server <serverId>", "List only the tools that belong to this server (the CLI resolves its project, fetches the project tools, then filters to this server)").action(runAction(async (opts) => {
7445
7595
  const orgId = await resolveOrgId(opts.org);
7446
7596
  if (!opts.project && !opts.server) {
7447
7597
  throw new Error("Pass either --project <projectId> or --server <serverId>.");
@@ -7458,10 +7608,11 @@ function registerToolCommands(program2) {
7458
7608
  organizationId: orgId,
7459
7609
  projectId
7460
7610
  });
7461
- printList(data.tools.map((t) => ({
7611
+ const rows = opts.server ? data.tools.filter((t) => t.serverId === opts.server) : data.tools;
7612
+ printList(rows.map((t) => ({
7462
7613
  name: t.name,
7463
- method: t.method,
7464
- path: t.path,
7614
+ method: t.endpoint?.method ?? t.method ?? "—",
7615
+ path: t.endpoint?.path ?? t.path ?? "—",
7465
7616
  enrichment: t.enrichmentStatus ?? "—",
7466
7617
  description: t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "")
7467
7618
  })), [
@@ -7470,7 +7621,7 @@ function registerToolCommands(program2) {
7470
7621
  { key: "path", label: "Path", width: 32 },
7471
7622
  { key: "enrichment", label: "Enrichment", width: 12 },
7472
7623
  { key: "description", label: "Description", width: 60 }
7473
- ], data);
7624
+ ], opts.server ? { ...data, tools: rows } : data);
7474
7625
  }));
7475
7626
  registerToolsShowCommand(tools);
7476
7627
  registerToolsDiffCommand(tools);
@@ -9035,7 +9186,7 @@ function previewKey2(key) {
9035
9186
  }
9036
9187
  function registerConfigCommands(program2) {
9037
9188
  const config = program2.command("config").description("Manage CLI configuration in ~/.mcpcloud/config.json");
9038
- config.command("show").description("Print the active profile (API key is redacted)").action(runAction(() => {
9189
+ const showActiveProfile = runAction(() => {
9039
9190
  const cfg = readConfig();
9040
9191
  const profile2 = getActiveProfileName();
9041
9192
  const safe = {
@@ -9052,14 +9203,16 @@ function registerConfigCommands(program2) {
9052
9203
  printKeyValue({
9053
9204
  path: safe.path,
9054
9205
  profile: safe.profile,
9055
- "base url": safe.baseUrl ?? "—",
9206
+ "base url": safe.baseUrl ?? "— (built-in default)",
9056
9207
  "app url": cfg.appUrl ?? "— (auto-derived)",
9057
9208
  "default org": safe.defaultOrganizationId ?? "—",
9058
9209
  "api key": safe.apiKeyPreview ?? "—",
9059
9210
  editor: cfg.editorCommand ?? "code",
9060
9211
  "editor open": cfg.editorOpenPreference ?? "ask"
9061
9212
  });
9062
- }));
9213
+ });
9214
+ config.command("show").description("Print the active profile (API key is redacted)").action(showActiveProfile);
9215
+ config.command("current").description("Alias for `config show` — print the active profile").action(showActiveProfile);
9063
9216
  config.command("set-url <url>").description("Save the API base URL to ~/.mcpcloud/config.json").action(runAction((url) => {
9064
9217
  try {
9065
9218
  new URL(url);
@@ -9629,7 +9782,7 @@ async function runDevReplay(opts) {
9629
9782
  printInfo("No matching records in .mcpcloud/inspector/. Use --from / --since / --filter, or run something first.");
9630
9783
  return;
9631
9784
  }
9632
- const baseUrl = opts.url ?? resolveBaseUrl(cwd);
9785
+ const baseUrl = opts.url ?? resolveBaseUrl2(cwd);
9633
9786
  if (!opts.dryRun && !baseUrl) {
9634
9787
  printError("No running mcp dev session in this directory. Start `mcp dev`, or pass --url <baseUrl>.");
9635
9788
  throw new CliExitError(1);
@@ -9707,7 +9860,7 @@ function parseLimit(raw) {
9707
9860
  }
9708
9861
  return n;
9709
9862
  }
9710
- function resolveBaseUrl(cwd) {
9863
+ function resolveBaseUrl2(cwd) {
9711
9864
  const sessions = listSessions().filter((s) => s.cwd === cwd);
9712
9865
  if (sessions.length === 0)
9713
9866
  return null;
@@ -11220,8 +11373,13 @@ async function fetchBundle(args) {
11220
11373
  } catch {
11221
11374
  body = null;
11222
11375
  }
11223
- const message = body?.error?.message ?? `Bundle download failed (${res.status}).`;
11224
- throw new Error(message);
11376
+ const envelope = body?.error;
11377
+ const requestId = envelope?.requestId ?? res.headers?.get?.("x-request-id") ?? null;
11378
+ throw new McpCloudApiError(res.status, {
11379
+ code: envelope?.code ?? `http_${res.status}`,
11380
+ message: envelope?.message ?? `Bundle download failed (${res.status}).`,
11381
+ ...requestId ? { requestId } : {}
11382
+ });
11225
11383
  }
11226
11384
  const text = await res.text();
11227
11385
  const parsed = JSON.parse(text);
@@ -25781,7 +25939,7 @@ if (!root) throw new Error("Missing #root mount node");
25781
25939
  clientExports.createRoot(root).render(
25782
25940
  /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, {}) })
25783
25941
  );</script>
25784
- <style rel="stylesheet" crossorigin>/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
25942
+ <style rel="stylesheet" crossorigin>/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
25785
25943
  @layer properties {
25786
25944
  @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
25787
25945
  *, :before, :after, ::backdrop {
@@ -26116,7 +26274,7 @@ clientExports.createRoot(root).render(
26116
26274
  }
26117
26275
 
26118
26276
  .top-0 {
26119
- top: calc(var(--spacing) * 0);
26277
+ top: 0;
26120
26278
  }
26121
26279
 
26122
26280
  .z-10 {
@@ -26124,7 +26282,7 @@ clientExports.createRoot(root).render(
26124
26282
  }
26125
26283
 
26126
26284
  .m-0 {
26127
- margin: calc(var(--spacing) * 0);
26285
+ margin: 0;
26128
26286
  }
26129
26287
 
26130
26288
  .mx-auto {
@@ -26136,7 +26294,7 @@ clientExports.createRoot(root).render(
26136
26294
  }
26137
26295
 
26138
26296
  .mt-1 {
26139
- margin-top: calc(var(--spacing) * 1);
26297
+ margin-top: var(--spacing);
26140
26298
  }
26141
26299
 
26142
26300
  .mt-3 {
@@ -26144,7 +26302,7 @@ clientExports.createRoot(root).render(
26144
26302
  }
26145
26303
 
26146
26304
  .mb-1 {
26147
- margin-bottom: calc(var(--spacing) * 1);
26305
+ margin-bottom: var(--spacing);
26148
26306
  }
26149
26307
 
26150
26308
  .mb-2\\.5 {
@@ -26152,7 +26310,7 @@ clientExports.createRoot(root).render(
26152
26310
  }
26153
26311
 
26154
26312
  .ml-1 {
26155
- margin-left: calc(var(--spacing) * 1);
26313
+ margin-left: var(--spacing);
26156
26314
  }
26157
26315
 
26158
26316
  .ml-2 {
@@ -26201,7 +26359,7 @@ clientExports.createRoot(root).render(
26201
26359
  }
26202
26360
 
26203
26361
  .min-w-0 {
26204
- min-width: calc(var(--spacing) * 0);
26362
+ min-width: 0;
26205
26363
  }
26206
26364
 
26207
26365
  .min-w-\\[240px\\] {
@@ -26253,7 +26411,7 @@ clientExports.createRoot(root).render(
26253
26411
  }
26254
26412
 
26255
26413
  .gap-1 {
26256
- gap: calc(var(--spacing) * 1);
26414
+ gap: var(--spacing);
26257
26415
  }
26258
26416
 
26259
26417
  .gap-1\\.5 {
@@ -26329,7 +26487,7 @@ clientExports.createRoot(root).render(
26329
26487
  }
26330
26488
 
26331
26489
  .p-0 {
26332
- padding: calc(var(--spacing) * 0);
26490
+ padding: 0;
26333
26491
  }
26334
26492
 
26335
26493
  .p-2 {
@@ -26341,7 +26499,7 @@ clientExports.createRoot(root).render(
26341
26499
  }
26342
26500
 
26343
26501
  .px-1 {
26344
- padding-inline: calc(var(--spacing) * 1);
26502
+ padding-inline: var(--spacing);
26345
26503
  }
26346
26504
 
26347
26505
  .px-1\\.5 {
@@ -26369,7 +26527,7 @@ clientExports.createRoot(root).render(
26369
26527
  }
26370
26528
 
26371
26529
  .py-1 {
26372
- padding-block: calc(var(--spacing) * 1);
26530
+ padding-block: var(--spacing);
26373
26531
  }
26374
26532
 
26375
26533
  .py-2 {
@@ -27495,8 +27653,12 @@ async function prepareDev(opts) {
27495
27653
  const env = envFile(cwd);
27496
27654
  const backups = backupsDir(cwd);
27497
27655
  const cached2 = readState(cwd);
27498
- let projectId = cached2?.projectId;
27499
- let serverName = cached2?.serverName;
27656
+ const cacheIsForTarget = cachedStateMatchesTarget(cached2, {
27657
+ serverId: opts.serverId,
27658
+ organizationId: opts.organizationId
27659
+ });
27660
+ let projectId = cacheIsForTarget ? cached2.projectId : undefined;
27661
+ let serverName = cacheIsForTarget ? cached2.serverName : undefined;
27500
27662
  let gitNative = false;
27501
27663
  let repoDir;
27502
27664
  const gitLink = opts.offline ? null : await resolveServerGitLink({
@@ -28463,7 +28625,8 @@ async function runDev(opts) {
28463
28625
  const cwd = process.cwd();
28464
28626
  const cached2 = readState(cwd);
28465
28627
  let serverId = opts.server ?? cached2?.serverId;
28466
- let organizationId = opts.org ?? cached2?.organizationId ?? undefined;
28628
+ const serverCameFromCache = !opts.server && Boolean(cached2?.serverId);
28629
+ let organizationId = opts.org ?? (serverCameFromCache ? cached2?.organizationId ?? undefined : undefined);
28467
28630
  if (!opts.server && cached2?.serverId && canRunInteractive()) {
28468
28631
  const choice = await promptResumeOrSwitch({
28469
28632
  serverId: cached2.serverId,
@@ -29575,19 +29738,15 @@ function checkActiveProfile() {
29575
29738
  ms: Date.now() - t0
29576
29739
  };
29577
29740
  }
29741
+ var BASE_URL_SOURCE_LABEL = {
29742
+ flag: "--base-url flag",
29743
+ env: "env var",
29744
+ profile: "profile",
29745
+ default: "built-in default"
29746
+ };
29578
29747
  function checkBaseUrl() {
29579
29748
  const t0 = Date.now();
29580
- const fromOverride = process.env["MCPCLOUD_BASE_URL"];
29581
- const fromConfig = readConfig().baseUrl;
29582
- const url = fromOverride ?? fromConfig;
29583
- if (!url) {
29584
- return {
29585
- name: "Base URL",
29586
- status: "fail",
29587
- detail: "Not configured. Run `mcp config set-url <url>` or set MCPCLOUD_BASE_URL.",
29588
- ms: Date.now() - t0
29589
- };
29590
- }
29749
+ const { url, source } = resolveBaseUrl();
29591
29750
  try {
29592
29751
  new URL(url);
29593
29752
  } catch {
@@ -29598,11 +29757,11 @@ function checkBaseUrl() {
29598
29757
  ms: Date.now() - t0
29599
29758
  };
29600
29759
  }
29601
- const source = fromOverride ? "env var" : "profile";
29760
+ const prodMarker = isProductionBaseUrl(url) ? " (production)" : "";
29602
29761
  return {
29603
29762
  name: "Base URL",
29604
29763
  status: "pass",
29605
- detail: `${url} (from ${source})`,
29764
+ detail: `${url}${prodMarker} (from ${BASE_URL_SOURCE_LABEL[source]})`,
29606
29765
  ms: Date.now() - t0
29607
29766
  };
29608
29767
  }
@@ -30006,7 +30165,7 @@ function registerHelpCommand(program2) {
30006
30165
  });
30007
30166
  }
30008
30167
 
30009
- // src/commands/init.ts
30168
+ // src/commands/init-shared.ts
30010
30169
  import { createInterface as createInterface2 } from "node:readline";
30011
30170
  function prompt2(question) {
30012
30171
  if (isNonInteractive()) {
@@ -30022,7 +30181,10 @@ function prompt2(question) {
30022
30181
  }
30023
30182
  async function pickProject(orgId, override) {
30024
30183
  if (override) {
30025
- const data = await api.get("/api/v1/project", { organizationId: orgId, projectId: override });
30184
+ const data = await api.get("/api/v1/project", {
30185
+ organizationId: orgId,
30186
+ projectId: override
30187
+ });
30026
30188
  return { project: data.project, created: false };
30027
30189
  }
30028
30190
  const list = await api.get("/api/v1/projects", { organizationId: orgId, limit: "25" });
@@ -30075,67 +30237,15 @@ async function pickServerName(override) {
30075
30237
  throw new Error("Server name must not be empty.");
30076
30238
  return name;
30077
30239
  }
30078
- function registerInitCommand(program2) {
30079
- program2.command("init").description("Guided onboarding: pick org → pick/create project → create a server (empty skeleton, or generated from an OpenAPI/GraphQL spec via --from-spec)").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Reuse an existing project instead of creating one").option("--name <serverName>", "Name for the new server (skeleton path only)").option("--from-spec <file|url>", "Build from an OpenAPI spec (local file, http(s) URL, or @- for stdin), or — with --source-type graphql — a GraphQL endpoint URL to introspect").option("--source-type <type>", `Override spec classification: ${SOURCE_TYPES.join(" | ")} (OpenAPI auto-detected; graphql requires an endpoint URL)`).option("--deploy", "After importing the spec, deploy the server").option("--wait", "With --deploy, follow events until the deploy is terminal").option("--wait-timeout <seconds>", "Max seconds to wait for a terminal deploy state (default 300)", "300").addHelpText("after", [
30080
- "",
30081
- "Two modes:",
30082
- " Skeleton (default): resolve org → pick/create project → create an empty server.",
30083
- " Spec (--from-spec): resolve org → pick/create project → ingest the spec into a",
30084
- " generated server, then optionally --deploy [--wait].",
30085
- "",
30086
- "Examples:",
30087
- " $ mcp init",
30088
- ' $ mcp init --org org_acme --project proj_123 --name "Stripe MCP"',
30089
- " $ mcp init --from-spec ./openapi.yaml --project proj_123",
30090
- " $ mcp init --from-spec https://api.example.com/openapi.json --project proj_123 --deploy --wait"
30091
- ].join(`
30092
- `)).action(runAction(async (opts) => {
30093
- const orgId = await resolveOrgId(opts.org);
30094
- if (opts.fromSpec) {
30095
- await runSpecInit({ orgId, opts });
30096
- return;
30097
- }
30098
- if (!isJsonMode()) {
30099
- const orgs = await api.get("/api/v1/organizations");
30100
- const org = orgs.organizations.find((o) => o.id === orgId);
30101
- printStep(`Using organization ${c.bold(org?.name ?? orgId)} ${c.dim("(" + orgId + ")")}`);
30102
- }
30103
- const { project, created: projectCreated } = await pickProject(orgId, opts.project);
30104
- if (!isJsonMode()) {
30105
- if (projectCreated) {
30106
- printSuccess(`Project ${c.bold(project.name)} created (${project.id}).`);
30107
- } else {
30108
- printStep(`Using project ${c.bold(project.name)} ${c.dim("(" + project.id + ")")}`);
30109
- }
30110
- }
30111
- const serverName = await pickServerName(opts.name);
30112
- if (!isJsonMode()) {
30113
- printStep(`Creating server skeleton ${c.bold(serverName)}…`);
30114
- }
30115
- const serverData = await api.post("/api/v1/servers", {
30116
- organizationId: orgId,
30117
- projectId: project.id,
30118
- name: serverName
30119
- });
30120
- const server = serverData.server;
30121
- if (isJsonMode()) {
30122
- printJson({
30123
- organizationId: orgId,
30124
- project,
30125
- projectCreated,
30126
- server
30127
- });
30128
- return;
30129
- }
30130
- printSuccess(`Server ${c.bold(server.id)} ready.`);
30131
- printInfo("");
30132
- printInfo("Next steps:");
30133
- printInfo(` 1. Import an API spec: \`mcp servers ingest --project ${project.id} --spec ./openapi.yaml\``);
30134
- printInfo(` ${c.dim("(or re-run `mcp init --from-spec ./openapi.yaml` for the guided flow)")}`);
30135
- printInfo(` 2. Run \`mcp dev --spec ./openapi.yaml\` to iterate locally.`);
30136
- printInfo(` 3. \`mcp servers deploy ${server.id} --wait\` to ship it.`);
30137
- }));
30240
+ function reportProject(project, created) {
30241
+ if (isJsonMode())
30242
+ return;
30243
+ if (created) {
30244
+ printSuccess(`Project ${c.bold(project.name)} created (${project.id}).`);
30245
+ }
30138
30246
  }
30247
+
30248
+ // src/commands/init-spec.ts
30139
30249
  function validateSourceType2(value) {
30140
30250
  if (value === undefined)
30141
30251
  return;
@@ -30145,6 +30255,52 @@ function validateSourceType2(value) {
30145
30255
  }
30146
30256
  return v2;
30147
30257
  }
30258
+ async function deployIngestedServer(args) {
30259
+ return await api.post("/api/v1/server/deploy", {
30260
+ organizationId: args.orgId,
30261
+ projectId: args.projectId,
30262
+ serverId: args.serverId,
30263
+ target: "workersDev",
30264
+ accessMode: "public",
30265
+ runtimeConfig: {
30266
+ upstreamBaseUrl: null,
30267
+ upstreamApiKey: null,
30268
+ upstreamApiKeyHeader: null,
30269
+ upstreamApiKeyPrefix: null,
30270
+ upstreamCustomHeaders: null,
30271
+ additionalPlainTextBindings: null,
30272
+ additionalSecretBindings: null
30273
+ }
30274
+ });
30275
+ }
30276
+ async function runDeployStep(args) {
30277
+ const { orgId, projectId, serverId, opts } = args;
30278
+ if (!isJsonMode())
30279
+ printStep(`Deploying ${c.bold(serverId)}…`);
30280
+ const deployData = await deployIngestedServer({ orgId, projectId, serverId });
30281
+ const dep = deployData.deployment;
30282
+ let waitDone = !opts.wait;
30283
+ let terminalEvent = null;
30284
+ if (opts.wait) {
30285
+ if (isTerminalDeploymentStatus(dep.status)) {
30286
+ waitDone = true;
30287
+ } else {
30288
+ const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
30289
+ const followed = await followDeploymentEvents({
30290
+ organizationId: orgId,
30291
+ deploymentId: dep.deploymentId,
30292
+ totalTimeoutMs,
30293
+ onEvent: (event) => {
30294
+ if (!isJsonMode())
30295
+ printInfo(formatEventLine(event));
30296
+ }
30297
+ });
30298
+ waitDone = followed.done;
30299
+ terminalEvent = followed.terminalEvent;
30300
+ }
30301
+ }
30302
+ return { deployment: dep, waitDone, terminalEvent };
30303
+ }
30148
30304
  async function runSpecInit(args) {
30149
30305
  const { orgId, opts } = args;
30150
30306
  if (opts.name && !isJsonMode()) {
@@ -30153,46 +30309,36 @@ async function runSpecInit(args) {
30153
30309
  const sourceType = validateSourceType2(opts.sourceType);
30154
30310
  const payload = await readSpecSource(opts.fromSpec, sourceType);
30155
30311
  const { project, created: projectCreated } = await pickProject(orgId, opts.project);
30156
- if (!isJsonMode() && projectCreated) {
30157
- printSuccess(`Project ${c.bold(project.name)} created (${project.id}).`);
30158
- }
30312
+ reportProject(project, projectCreated);
30159
30313
  if (!isJsonMode()) {
30160
30314
  printStep(`Importing ${c.bold(payload.sourceType)} spec into ${c.bold(project.name)}…`);
30161
30315
  }
30162
- const ingest = await api.post("/api/v1/server/ingest", buildIngestBody({
30163
- organizationId: orgId,
30164
- projectId: project.id,
30165
- payload
30166
- }));
30316
+ const ingest = await api.post("/api/v1/server/ingest", buildIngestBody({ organizationId: orgId, projectId: project.id, payload }));
30167
30317
  const server = ingest.server;
30168
30318
  if (!isJsonMode()) {
30169
30319
  printSuccess(`Server ${c.bold(server.id)} created from spec.`);
30170
30320
  }
30171
30321
  let deployResult = null;
30172
30322
  if (opts.deploy) {
30173
- if (!isJsonMode())
30174
- printStep(`Deploying ${c.bold(server.id)}…`);
30175
- const deployData = await api.post("/api/v1/server/deploy", {
30176
- organizationId: orgId,
30177
- serverId: server.id
30178
- });
30179
- let waitDone = !opts.wait;
30180
- let terminalEvent = null;
30181
- if (opts.wait) {
30182
- const totalTimeoutMs = Math.max(10, Number.parseInt(opts.waitTimeout, 10) || 300) * 1000;
30183
- const followed = await followDeploymentEvents({
30184
- organizationId: orgId,
30185
- deploymentId: deployData.deployment.deploymentId,
30186
- totalTimeoutMs,
30187
- onEvent: (event) => {
30188
- if (!isJsonMode())
30189
- printInfo(formatEventLine(event));
30190
- }
30323
+ try {
30324
+ deployResult = await runDeployStep({
30325
+ orgId,
30326
+ projectId: project.id,
30327
+ serverId: server.id,
30328
+ opts
30191
30329
  });
30192
- waitDone = followed.done;
30193
- terminalEvent = followed.terminalEvent;
30330
+ } catch (err) {
30331
+ reportDeployFailure2({
30332
+ serverId: server.id,
30333
+ projectId: project.id,
30334
+ project,
30335
+ projectCreated,
30336
+ apiSource: ingest.apiSource,
30337
+ server,
30338
+ err
30339
+ });
30340
+ throw err;
30194
30341
  }
30195
- deployResult = { deployment: deployData.deployment, waitDone, terminalEvent };
30196
30342
  }
30197
30343
  if (isJsonMode()) {
30198
30344
  printJson({
@@ -30203,26 +30349,19 @@ async function runSpecInit(args) {
30203
30349
  server,
30204
30350
  deploy: deployResult
30205
30351
  });
30352
+ if (deployResult?.terminalEvent && isFailureEvent(deployResult.terminalEvent))
30353
+ throw new CliExitError(1);
30206
30354
  if (deployResult && opts.wait && !deployResult.waitDone)
30207
30355
  throw new CliExitError(1);
30208
30356
  return;
30209
30357
  }
30210
30358
  if (deployResult) {
30211
- const dep = deployResult.deployment;
30212
- printSuccess(`Deployment ${c.bold(dep.deploymentId)} created.`);
30213
- printKeyValue({
30214
- "server id": server.id,
30215
- "deployment id": dep.deploymentId,
30216
- url: dep.deploymentUrl ?? "—",
30217
- target: dep.target,
30218
- "access mode": dep.accessMode,
30219
- status: dep.status
30359
+ reportDeploySuccess({
30360
+ serverId: server.id,
30361
+ projectId: project.id,
30362
+ result: deployResult,
30363
+ opts
30220
30364
  });
30221
- if (opts.wait && !deployResult.waitDone) {
30222
- printError(`Deploy did not reach a terminal state in time. Watch with \`mcp deployments logs ${dep.deploymentId} --follow\`.`);
30223
- throw new CliExitError(1);
30224
- }
30225
- printStep(`Try it: \`mcp invoke ${server.id} <tool>\`.`);
30226
30365
  return;
30227
30366
  }
30228
30367
  printInfo("");
@@ -30230,6 +30369,117 @@ async function runSpecInit(args) {
30230
30369
  printStep(`Generate the bundle: \`mcp servers generate ${server.id} --out ./build\``);
30231
30370
  printStep(`Deploy it: \`mcp servers deploy ${server.id} --wait\``);
30232
30371
  }
30372
+ function resumeDeployCommand(serverId, projectId) {
30373
+ return `mcp servers deploy ${serverId} --project ${projectId} --wait`;
30374
+ }
30375
+ function reportDeployFailure2(args) {
30376
+ const resume = resumeDeployCommand(args.serverId, args.projectId);
30377
+ if (isJsonMode()) {
30378
+ const message = args.err instanceof Error ? args.err.message : String(args.err);
30379
+ printJson({
30380
+ project: args.project,
30381
+ projectCreated: args.projectCreated,
30382
+ apiSource: args.apiSource,
30383
+ server: args.server,
30384
+ deploy: null,
30385
+ deployFailed: true,
30386
+ deployError: { message },
30387
+ resumeCommand: resume
30388
+ });
30389
+ return;
30390
+ }
30391
+ printError(`Server ${c.bold(args.serverId)} was created, but the deploy step failed.`);
30392
+ printInfo("");
30393
+ printInfo("Created so far:");
30394
+ printKeyValue({ "server id": args.serverId, "project id": args.projectId });
30395
+ printInfo("");
30396
+ printInfo(`Resume the deploy once the cause is cleared:`);
30397
+ printStep(resume);
30398
+ printInfo("");
30399
+ }
30400
+ function reportDeploySuccess(args) {
30401
+ const { serverId, projectId, result, opts } = args;
30402
+ const dep = result.deployment;
30403
+ printSuccess(`Deployment ${c.bold(dep.deploymentId)} created.`);
30404
+ printKeyValue({
30405
+ "server id": serverId,
30406
+ "deployment id": dep.deploymentId,
30407
+ url: dep.deploymentUrl ?? "—",
30408
+ target: dep.target,
30409
+ "access mode": dep.accessMode,
30410
+ status: dep.status
30411
+ });
30412
+ if (result.terminalEvent && isFailureEvent(result.terminalEvent)) {
30413
+ const reason = formatEventDetails(result.terminalEvent.details);
30414
+ printError(`Deploy failed: ${reason ?? result.terminalEvent.message}`);
30415
+ printStep(`Resume once the cause is cleared: \`mcp servers deploy ${serverId} --project ${projectId} --wait\``);
30416
+ throw new CliExitError(1);
30417
+ }
30418
+ if (opts.wait && !result.waitDone) {
30419
+ printError(`Deploy did not reach a terminal state in time. Check \`mcp deployments get ${dep.deploymentId}\` and \`mcp deployments health ${dep.deploymentId}\`, or tail \`mcp deployments logs ${dep.deploymentId} --follow\`.`);
30420
+ throw new CliExitError(1);
30421
+ }
30422
+ printStep(`Try it: \`mcp invoke ${serverId} <tool>\`.`);
30423
+ }
30424
+
30425
+ // src/commands/init.ts
30426
+ function registerInitCommand(program2) {
30427
+ program2.command("init").description("Guided onboarding: pick org → pick/create project → create a server (empty skeleton, or generated from an OpenAPI/GraphQL spec via --from-spec)").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Reuse an existing project instead of creating one").option("--name <serverName>", "Name for the new server (skeleton path only)").option("--from-spec <file|url>", "Build from an OpenAPI spec (local file, http(s) URL, or @- for stdin), or — with --source-type graphql — a GraphQL endpoint URL to introspect").option("--source-type <type>", `Override spec classification: ${SOURCE_TYPES.join(" | ")} (OpenAPI auto-detected; graphql requires an endpoint URL)`).option("--deploy", "After importing the spec, deploy the server").option("--wait", "With --deploy, follow events until the deploy is terminal").option("--wait-timeout <seconds>", "Max seconds to wait for a terminal deploy state (default 300)", "300").addHelpText("after", [
30428
+ "",
30429
+ "Two modes:",
30430
+ " Skeleton (default): resolve org → pick/create project → create an empty server.",
30431
+ " Spec (--from-spec): resolve org → pick/create project → ingest the spec into a",
30432
+ " generated server, then optionally --deploy [--wait].",
30433
+ "",
30434
+ "Examples:",
30435
+ " $ mcp init",
30436
+ ' $ mcp init --org org_acme --project proj_123 --name "Stripe MCP"',
30437
+ " $ mcp init --from-spec ./openapi.yaml --project proj_123",
30438
+ " $ mcp init --from-spec https://api.example.com/openapi.json --project proj_123 --deploy --wait"
30439
+ ].join(`
30440
+ `)).action(runAction(runInit));
30441
+ }
30442
+ async function runInit(opts) {
30443
+ const orgId = await resolveOrgId(opts.org);
30444
+ if (opts.fromSpec) {
30445
+ await runSpecInit({ orgId, opts });
30446
+ return;
30447
+ }
30448
+ await runSkeletonInit({ orgId, opts });
30449
+ }
30450
+ async function runSkeletonInit(args) {
30451
+ const { orgId, opts } = args;
30452
+ if (!isJsonMode()) {
30453
+ const orgs = await api.get("/api/v1/organizations");
30454
+ const org = orgs.organizations.find((o) => o.id === orgId);
30455
+ printStep(`Using organization ${c.bold(org?.name ?? orgId)} ${c.dim("(" + orgId + ")")}`);
30456
+ }
30457
+ const { project, created: projectCreated } = await pickProject(orgId, opts.project);
30458
+ if (!isJsonMode()) {
30459
+ if (projectCreated) {
30460
+ printSuccess(`Project ${c.bold(project.name)} created (${project.id}).`);
30461
+ } else {
30462
+ printStep(`Using project ${c.bold(project.name)} ${c.dim("(" + project.id + ")")}`);
30463
+ }
30464
+ }
30465
+ const serverName = await pickServerName(opts.name);
30466
+ if (!isJsonMode()) {
30467
+ printStep(`Creating server skeleton ${c.bold(serverName)}…`);
30468
+ }
30469
+ const serverData = await api.post("/api/v1/servers", { organizationId: orgId, projectId: project.id, name: serverName });
30470
+ const server = serverData.server;
30471
+ if (isJsonMode()) {
30472
+ printJson({ organizationId: orgId, project, projectCreated, server });
30473
+ return;
30474
+ }
30475
+ printSuccess(`Server ${c.bold(server.id)} ready.`);
30476
+ printInfo("");
30477
+ printInfo("Next steps:");
30478
+ printInfo(` 1. Import an API spec: \`mcp servers ingest --project ${project.id} --spec ./openapi.yaml\``);
30479
+ printInfo(` ${c.dim("(or re-run `mcp init --from-spec ./openapi.yaml` for the guided flow)")}`);
30480
+ printInfo(` 2. Run \`mcp dev --spec ./openapi.yaml\` to iterate locally.`);
30481
+ printInfo(` 3. \`mcp servers deploy ${server.id} --wait\` to ship it.`);
30482
+ }
30233
30483
 
30234
30484
  // src/commands/installation.ts
30235
30485
  function registerInstallationCommands(program2) {
@@ -30920,6 +31170,7 @@ function registerUsageCommands(program2) {
30920
31170
  allowed: t.allowedCount,
30921
31171
  "billable invocations": usageData.usage.invocations?.billableCount ?? t.allowedToolCallCount ?? 0,
30922
31172
  "invocation cost": formatMicros(usageData.usage.invocations?.billedMicros),
31173
+ "projected monthly": formatMicros(usageData.usage.invocations?.projectedMonthlyMicros),
30923
31174
  "rate-limited": t.rateLimitedCount,
30924
31175
  unauthorized: t.unauthorizedCount,
30925
31176
  failed: t.failedCount,
@@ -31127,6 +31378,73 @@ function registerOrgCommands(program2) {
31127
31378
  }));
31128
31379
  }
31129
31380
 
31381
+ // src/commands/connections.ts
31382
+ async function readKeyFromStdin() {
31383
+ const chunks = [];
31384
+ for await (const chunk of process.stdin) {
31385
+ chunks.push(Buffer.from(chunk));
31386
+ }
31387
+ return Buffer.concat(chunks).toString("utf8").trim();
31388
+ }
31389
+ function registerConnectionsCommands(program2) {
31390
+ const connections = program2.command("connections").description("Manage your per-server upstream API keys (BYOK) from the terminal").addHelpText("after", [
31391
+ "",
31392
+ "Examples:",
31393
+ " $ mcp connections list",
31394
+ " $ mcp connections add <serverId> --key sk_live_…",
31395
+ ' $ echo "$UPSTREAM_KEY" | mcp connections add <serverId> --key-stdin',
31396
+ " $ mcp connections remove <serverId>"
31397
+ ].join(`
31398
+ `));
31399
+ connections.command("list").description("List your stored upstream keys across servers (hints only)").action(runAction(async () => {
31400
+ const data = await api.get("/api/v1/upstream-key");
31401
+ if (isJsonMode()) {
31402
+ printJson(data);
31403
+ return;
31404
+ }
31405
+ printList(data.keys.map((entry) => ({
31406
+ server: entry.serverName ?? entry.serverId,
31407
+ key: `••••${entry.keyHint}`,
31408
+ status: entry.status,
31409
+ "last used": entry.lastUsedAt ? formatDate(entry.lastUsedAt) : "—",
31410
+ updated: formatDate(entry.updatedAt)
31411
+ })), [
31412
+ { key: "server", label: "Server", width: 32 },
31413
+ { key: "key", label: "Key", width: 10 },
31414
+ { key: "status", label: "Status", width: 10 },
31415
+ { key: "last used", label: "Last used", width: 22 },
31416
+ { key: "updated", label: "Updated", width: 22 }
31417
+ ], data);
31418
+ }));
31419
+ connections.command("add <serverId>").description("Save (or replace) your upstream API key for a server — rotation is just adding again").option("--key <apiKey>", "The upstream API key (visible in shell history)").option("--key-stdin", "Read the key from stdin instead (recommended for CI)").action(runAction(async (serverId, opts) => {
31420
+ const apiKey = opts.keyStdin ? await readKeyFromStdin() : opts.key ?? "";
31421
+ if (!apiKey.trim()) {
31422
+ throw new Error("Provide the key via --key <apiKey> or pipe it with --key-stdin.");
31423
+ }
31424
+ const data = await api.post("/api/v1/upstream-key", { serverId, apiKey });
31425
+ if (isJsonMode()) {
31426
+ printJson(data);
31427
+ return;
31428
+ }
31429
+ printKeyValue({
31430
+ saved: String(data.saved),
31431
+ key: `••••${data.keyHint}`,
31432
+ note: "Stored encrypted; your next tool call uses it immediately."
31433
+ });
31434
+ }));
31435
+ connections.command("remove <serverId>").description("Revoke your stored upstream key for a server").action(runAction(async (serverId) => {
31436
+ const data = await api.delete("/api/v1/upstream-key", { serverId });
31437
+ if (isJsonMode()) {
31438
+ printJson(data);
31439
+ return;
31440
+ }
31441
+ printKeyValue({
31442
+ revoked: String(data.revoked),
31443
+ note: data.revoked ? "Tool calls will prompt for a fresh key on next use." : "No active key was stored for this server."
31444
+ });
31445
+ }));
31446
+ }
31447
+
31130
31448
  // src/lib/plugins.ts
31131
31449
  import { spawn as spawn7 } from "node:child_process";
31132
31450
  import {
@@ -35733,7 +36051,7 @@ function registerUpdateCommand(program2) {
35733
36051
  // src/create-program.ts
35734
36052
  function createProgram() {
35735
36053
  const program2 = new Command;
35736
- program2.name("mcp").description("The official CLI for MCPCloud — manage projects, MCP servers, skills, and API keys from the terminal").version(getCliVersion(), "-v, --version", "Print the CLI version").option("--json", "Output raw JSON (useful for scripting)").option("--base-url <url>", "API base URL for this invocation. Falls back to MCPCLOUD_BASE_URL or `mcp config set-url`.").option("--profile <name>", "Run against a named config profile for this invocation (overrides MCPCLOUD_PROFILE and the saved current profile).").option("--ci", "Force CI mode for this invocation. Auto-detected when CI=true. Disables color, swaps Unicode glyphs for ASCII, refuses interactive prompts, and always surfaces request IDs.").option("--warnings-as-errors", "Exit non-zero if any warning is emitted during the command (CI ergonomics).").option("--non-interactive", "Refuse interactive prompts (fail fast on missing input) without the color/glyph changes of --ci.").option("--quiet", "Suppress success/step/info chrome; data and errors still print. Pairs with exit codes for health/existence gates.").option("--format <mode>", "Output format for list commands: table (default), tsv, jsonl, json.").option("--no-table", "Shorthand for --format tsv. Tab-separated rows for awk/cut/grep pipelines.").option("--field <names>", "Comma-separated list of fields to include in list output (repeatable).", (value, prev) => prev ? [...prev, value] : [value]).option("--filter <key=substring>", "Substring filter for list rows (repeatable). Case-insensitive.", (value, prev) => prev ? [...prev, value] : [value]).option("--idempotency-key <key>", "Stable Idempotency-Key for mutations (1–255 ASCII chars, no whitespace). When omitted, the CLI auto-generates a UUID per invocation so internal retries on transient failures stay safe.").option("--debug", "Print every HTTP request/response with secrets redacted. Equivalent to MCPCLOUD_LOG=debug.").hook("preAction", (thisCommand) => {
36054
+ program2.name("mcp").description("The official CLI for MCPCloud — manage projects, MCP servers, skills, and API keys from the terminal").version(getCliVersion(), "-v, --version", "Print the CLI version").option("--json", "Output raw JSON (useful for scripting)").option("--base-url <url>", "API base URL for this invocation. Falls back to MCPCLOUD_BASE_URL, `mcp config set-url`, then the built-in MCPCloud production default.").option("--profile <name>", "Run against a named config profile for this invocation (overrides MCPCLOUD_PROFILE and the saved current profile).").option("--ci", "Force CI mode for this invocation. Auto-detected when CI=true. Disables color, swaps Unicode glyphs for ASCII, refuses interactive prompts, and always surfaces request IDs.").option("--warnings-as-errors", "Exit non-zero if any warning is emitted during the command (CI ergonomics).").option("--non-interactive", "Refuse interactive prompts (fail fast on missing input) without the color/glyph changes of --ci.").option("--quiet", "Suppress success/step/info chrome; data and errors still print. Pairs with exit codes for health/existence gates.").option("--format <mode>", "Output format for list commands: table (default), tsv, jsonl, json.").option("--no-table", "Shorthand for --format tsv. Tab-separated rows for awk/cut/grep pipelines.").option("--field <names>", "Comma-separated list of fields to include in list output (repeatable).", (value, prev) => prev ? [...prev, value] : [value]).option("--filter <key=substring>", "Substring filter for list rows (repeatable). Case-insensitive.", (value, prev) => prev ? [...prev, value] : [value]).option("--idempotency-key <key>", "Stable Idempotency-Key for mutations (1–255 ASCII chars, no whitespace). When omitted, the CLI auto-generates a UUID per invocation so internal retries on transient failures stay safe.").option("--debug", "Print every HTTP request/response with secrets redacted. Equivalent to MCPCLOUD_LOG=debug.").hook("preAction", (thisCommand) => {
35737
36055
  const opts = thisCommand.optsWithGlobals();
35738
36056
  setJsonMode(Boolean(opts.json));
35739
36057
  setBaseUrlOverride(opts.baseUrl);
@@ -35753,21 +36071,22 @@ function createProgram() {
35753
36071
  }).addHelpText("after", [
35754
36072
  "",
35755
36073
  "Environment variables:",
35756
- " MCPCLOUD_API_KEY API key used for authentication (required)",
35757
- " MCPCLOUD_BASE_URL API base URL (required no default is shipped)",
36074
+ " MCPCLOUD_API_KEY API key used for authentication (or run `mcp login`)",
36075
+ " MCPCLOUD_BASE_URL Override the API base URL (defaults to MCPCloud production)",
35758
36076
  " MCPCLOUD_ORG_ID Default organization ID",
35759
36077
  ' MCPCLOUD_PROFILE Active config profile name (default: "default")',
35760
36078
  " CI When truthy, enables CI mode automatically (no color, ASCII glyphs, no prompts)",
35761
36079
  "",
35762
36080
  "Examples:",
35763
- " $ export MCPCLOUD_BASE_URL=https://your-deployment.example.com",
35764
- " $ mcp config set-url https://your-deployment.example.com # persist it",
35765
- " $ mcp login",
36081
+ " $ mcp login # sign in (production, out of the box)",
35766
36082
  " $ mcp whoami",
35767
36083
  " $ mcp skills connect skill_123 --agent claude-code --apply",
35768
36084
  " $ mcp --json servers list",
35769
36085
  " $ mcp servers list --field id,name --format jsonl",
35770
- " $ mcp servers list --filter status=active --no-table"
36086
+ " $ mcp servers list --filter status=active --no-table",
36087
+ "",
36088
+ "Self-host / staging: point the CLI elsewhere with MCPCLOUD_BASE_URL,",
36089
+ "`mcp config set-url <url>`, or --base-url <url> (each overrides the default)."
35771
36090
  ].join(`
35772
36091
  `));
35773
36092
  registerAuthCommands(program2);
@@ -35781,6 +36100,7 @@ function createProgram() {
35781
36100
  registerDeploymentCommands(program2);
35782
36101
  registerOrgCommands(program2);
35783
36102
  registerUsageCommands(program2);
36103
+ registerConnectionsCommands(program2);
35784
36104
  registerMetricsCommands(program2);
35785
36105
  registerDevCommand(program2);
35786
36106
  registerCompletionCommand(program2);
@@ -35798,8 +36118,59 @@ function createProgram() {
35798
36118
  return program2;
35799
36119
  }
35800
36120
 
36121
+ // src/lib/first-run.ts
36122
+ function shouldShowFirstRunGreeting(input) {
36123
+ if (input.hasCommand)
36124
+ return false;
36125
+ if (!input.isTty)
36126
+ return false;
36127
+ if (input.ciMode)
36128
+ return false;
36129
+ if (input.jsonMode)
36130
+ return false;
36131
+ if (input.authenticated)
36132
+ return false;
36133
+ if (input.hasSavedConfig)
36134
+ return false;
36135
+ return true;
36136
+ }
36137
+ function argvHasCommand(argv) {
36138
+ return argv.slice(2).some((token) => !token.startsWith("-"));
36139
+ }
36140
+ function firstRunGreeting() {
36141
+ return [
36142
+ "Welcome to MCPCloud — turn an API spec into a deployed MCP server.",
36143
+ "",
36144
+ "Get started:",
36145
+ " 1. mcp login Sign in (opens your browser)",
36146
+ " 2. mcp init Scaffold a server from an OpenAPI / GraphQL spec",
36147
+ " 3. mcp servers deploy Ship it to the edge",
36148
+ "",
36149
+ "Run `mcp --help` for the full command reference.",
36150
+ ""
36151
+ ].join(`
36152
+ `);
36153
+ }
36154
+
35801
36155
  // src/index.ts
36156
+ function maybeGreetFirstRun(argv) {
36157
+ const cfg = readConfig();
36158
+ const show = shouldShowFirstRunGreeting({
36159
+ hasCommand: argvHasCommand(argv),
36160
+ isTty: Boolean(process.stdout.isTTY),
36161
+ ciMode: isCiEnv() || argv.includes("--ci"),
36162
+ jsonMode: argv.includes("--json"),
36163
+ authenticated: Boolean(getApiKey()),
36164
+ hasSavedConfig: Boolean(cfg.baseUrl || cfg.apiKey || cfg.defaultOrganizationId)
36165
+ });
36166
+ if (!show)
36167
+ return false;
36168
+ process.stdout.write(firstRunGreeting());
36169
+ return true;
36170
+ }
35802
36171
  async function main() {
36172
+ if (maybeGreetFirstRun(process.argv))
36173
+ return;
35803
36174
  const program2 = createProgram();
35804
36175
  if (process.env["MCPSH_DISABLE_PLUGINS"] !== "1") {
35805
36176
  await loadPlugins(program2);