@mcpcloud/cli 0.1.0 → 0.1.3

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 +42 -11
  2. package/dist/index.js +134 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,10 +4,14 @@ The official command-line interface for [MCPCloud](https://mcpcloud.sh) — mana
4
4
 
5
5
  ```sh
6
6
  npm install -g @mcpcloud/cli
7
+ export MCPCLOUD_BASE_URL=https://your-deployment.example.com # ask your admin
7
8
  mcpsh login
8
9
  mcpsh whoami
9
10
  ```
10
11
 
12
+ > **Note**
13
+ > MCPCloud is in private beta. The CLI does **not** ship with a default API base URL — set `MCPCLOUD_BASE_URL` (or `mcpsh config set-url <url>`) using the URL provided by your MCPCloud admin. A built-in default will be added when the platform launches publicly.
14
+
11
15
  The package installs two equivalent binaries: `mcpsh` (short) and `mcpcloud` (full). Use whichever you prefer.
12
16
 
13
17
  ## Requirements
@@ -27,20 +31,39 @@ npx -y @mcpcloud/cli whoami
27
31
  bunx --bun @mcpcloud/cli whoami
28
32
  ```
29
33
 
30
- ## Authentication
34
+ ## Configuration
35
+
36
+ The CLI needs **two** values to talk to your MCPCloud deployment: an API base URL and an API key. Either or both can come from environment variables, the saved config file, or per-invocation flags.
37
+
38
+ ### 1. Set the API base URL
31
39
 
32
- You can authenticate two ways. The CLI prefers the environment variable when both are set.
40
+ The CLI does not assume any default base URL. Use whichever method fits your environment:
33
41
 
34
42
  ```sh
35
- # 1. Save a key to ~/.mcpcloud/config.json (mode 0600)
43
+ # environment variable (recommended for CI / scripts)
44
+ export MCPCLOUD_BASE_URL=https://your-deployment.example.com
45
+
46
+ # persist for your user account (writes to ~/.mcpcloud/config.json)
47
+ mcpsh config set-url https://your-deployment.example.com
48
+
49
+ # one-off override
50
+ mcpsh --base-url https://your-deployment.example.com whoami
51
+ ```
52
+
53
+ The lookup order is: `--base-url` flag → `MCPCLOUD_BASE_URL` env var → saved `baseUrl` in `~/.mcpcloud/config.json`. If none are set the CLI exits with code `3` and a clear message.
54
+
55
+ ### 2. Authenticate
56
+
57
+ ```sh
58
+ # Save a key to ~/.mcpcloud/config.json (mode 0600)
36
59
  mcpsh login # interactive prompt (recommended)
37
60
  mcpsh login --key mck_xxx # non-interactive (avoid: writes to shell history)
38
61
 
39
- # 2. Use an environment variable (recommended for CI)
62
+ # Or use an environment variable (recommended for CI)
40
63
  export MCPCLOUD_API_KEY=mck_xxx
41
64
  ```
42
65
 
43
- `mcpsh logout` removes the saved key.
66
+ `mcpsh logout` removes the saved key. `mcpsh config show` prints the current resolved config (with the API key redacted).
44
67
 
45
68
  ## Quick start
46
69
 
@@ -75,7 +98,7 @@ Run `mcpsh <command> --help` on any command for the full option list.
75
98
  | Variable | Description |
76
99
  |---|---|
77
100
  | `MCPCLOUD_API_KEY` | API key used for authentication (overrides the saved config). |
78
- | `MCPCLOUD_BASE_URL` | Override the API base URL. Default: `https://mcpcloud.sh`. |
101
+ | `MCPCLOUD_BASE_URL` | API base URL. **Required** — there is no built-in default. |
79
102
  | `MCPCLOUD_ORG_ID` | Default organization ID for commands that require one. |
80
103
 
81
104
  The organization for each command is resolved in this order:
@@ -87,12 +110,18 @@ The organization for each command is resolved in this order:
87
110
 
88
111
  ## Commands
89
112
 
90
- ### Authentication
113
+ ### Authentication & config
91
114
 
92
115
  ```sh
93
116
  mcpsh login [--key <secret>] # save an API key locally
94
117
  mcpsh logout # remove the saved key
95
118
  mcpsh whoami # show current key, base URL, and accessible orgs
119
+
120
+ mcpsh config show # print resolved config (key redacted)
121
+ mcpsh config set-url <url> # persist the API base URL
122
+ mcpsh config clear-url # remove the saved base URL
123
+ mcpsh config set-org <orgId> # persist a default organization
124
+ mcpsh config clear-org # remove the saved default organization
96
125
  ```
97
126
 
98
127
  ### Projects
@@ -113,7 +142,8 @@ mcpsh servers logs <serverId> [--org <id>] [--limit <n>]
113
142
  ### Tools
114
143
 
115
144
  ```sh
116
- mcpsh tools list <serverId> [--org <id>]
145
+ mcpsh tools list --project <projectId> [--org <id>]
146
+ mcpsh tools list --server <serverId> [--org <id>] # looks up the project for you
117
147
  ```
118
148
 
119
149
  ### Skills
@@ -158,6 +188,7 @@ mcpsh --json skills connect skl_123 --agent cursor \
158
188
  | `0` | Success. |
159
189
  | `1` | Generic failure (API error, validation error, network error). |
160
190
  | `2` | No API key configured. Run `mcpsh login` or set `MCPCLOUD_API_KEY`. |
191
+ | `3` | No API base URL configured. Set `MCPCLOUD_BASE_URL` or run `mcpsh config set-url <url>`. |
161
192
 
162
193
  When an API call fails, the CLI prints the response code, status, and request id (when present) to make debugging easier:
163
194
 
@@ -173,9 +204,9 @@ The CLI stores its config at `~/.mcpcloud/config.json` with mode `0600`. Recogni
173
204
 
174
205
  ```jsonc
175
206
  {
176
- "apiKey": "mck_...", // saved by `mcpsh login`
177
- "baseUrl": "https://staging.mcpcloud.sh", // overrides the default
178
- "defaultOrganizationId": "org_..." // used when --org is omitted
207
+ "apiKey": "mck_...", // saved by `mcpsh login`
208
+ "baseUrl": "https://your-deployment...", // saved by `mcpsh config set-url`
209
+ "defaultOrganizationId": "org_..." // saved by `mcpsh config set-org`
179
210
  }
180
211
  ```
181
212
 
package/dist/index.js CHANGED
@@ -1878,7 +1878,6 @@ var {
1878
1878
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
1879
1879
  import { homedir } from "node:os";
1880
1880
  import { join } from "node:path";
1881
- var DEFAULT_BASE_URL = "https://mcpcloud.sh";
1882
1881
  function configDir() {
1883
1882
  return join(homedir(), ".mcpcloud");
1884
1883
  }
@@ -1892,6 +1891,13 @@ class MissingApiKeyError extends Error {
1892
1891
  this.name = "MissingApiKeyError";
1893
1892
  }
1894
1893
  }
1894
+
1895
+ class MissingBaseUrlError extends Error {
1896
+ constructor() {
1897
+ super("No API base URL configured. Set MCPCLOUD_BASE_URL, pass --base-url, or save one with `mcpsh config set-url <url>`.");
1898
+ this.name = "MissingBaseUrlError";
1899
+ }
1900
+ }
1895
1901
  function readConfig() {
1896
1902
  if (!existsSync(configFile()))
1897
1903
  return {};
@@ -1923,7 +1929,10 @@ function setBaseUrlOverride(url) {
1923
1929
  baseUrlOverride = url;
1924
1930
  }
1925
1931
  function getBaseUrl() {
1926
- return baseUrlOverride ?? process.env["MCPCLOUD_BASE_URL"] ?? readConfig().baseUrl ?? DEFAULT_BASE_URL;
1932
+ const url = baseUrlOverride ?? process.env["MCPCLOUD_BASE_URL"] ?? readConfig().baseUrl;
1933
+ if (!url)
1934
+ throw new MissingBaseUrlError;
1935
+ return url;
1927
1936
  }
1928
1937
  function requireApiKey() {
1929
1938
  const key = getApiKey();
@@ -1968,6 +1977,9 @@ function buildUrl(path, params) {
1968
1977
  function shouldRetry(attempt, err) {
1969
1978
  if (attempt >= MAX_RETRIES)
1970
1979
  return false;
1980
+ if (err instanceof Error && (err.name === "MissingApiKeyError" || err.name === "MissingBaseUrlError")) {
1981
+ return false;
1982
+ }
1971
1983
  if (err instanceof McpCloudNetworkError)
1972
1984
  return true;
1973
1985
  if (err instanceof McpCloudApiError && RETRYABLE_STATUSES.has(err.status))
@@ -2032,9 +2044,10 @@ async function performRequest(method, path, options) {
2032
2044
  init.body = JSON.stringify(options.body);
2033
2045
  }
2034
2046
  try {
2047
+ const url = buildUrl(path, options.params);
2035
2048
  let res;
2036
2049
  try {
2037
- res = await fetch(buildUrl(path, options.params), init);
2050
+ res = await fetch(url, init);
2038
2051
  } catch (err) {
2039
2052
  if (controller.signal.aborted && !externalSignal?.aborted) {
2040
2053
  throw new McpCloudNetworkError(`Request to ${path} timed out after ${timeoutMs}ms.`, err);
@@ -2144,6 +2157,9 @@ function describeError(err) {
2144
2157
  if (err instanceof MissingApiKeyError) {
2145
2158
  return { message: err.message, code: "missing_api_key" };
2146
2159
  }
2160
+ if (err instanceof MissingBaseUrlError) {
2161
+ return { message: err.message, code: "missing_base_url" };
2162
+ }
2147
2163
  if (err instanceof Error) {
2148
2164
  return { message: err.message };
2149
2165
  }
@@ -2165,7 +2181,11 @@ function handleError(err) {
2165
2181
  if (info.docsUrl)
2166
2182
  console.error(` docs: ${info.docsUrl}`);
2167
2183
  }
2168
- process.exit(info.code === "missing_api_key" ? 2 : 1);
2184
+ if (info.code === "missing_api_key")
2185
+ process.exit(2);
2186
+ if (info.code === "missing_base_url")
2187
+ process.exit(3);
2188
+ process.exit(1);
2169
2189
  }
2170
2190
 
2171
2191
  // src/lib/version.ts
@@ -2373,6 +2393,11 @@ function registerProjectCommands(program2) {
2373
2393
  }
2374
2394
 
2375
2395
  // src/commands/servers.ts
2396
+ function formatEventTimestamp(ms) {
2397
+ if (typeof ms !== "number" || !Number.isFinite(ms))
2398
+ return "—".padEnd(24);
2399
+ return new Date(ms).toISOString();
2400
+ }
2376
2401
  function registerServerCommands(program2) {
2377
2402
  const servers = program2.command("servers").description("Manage MCP servers");
2378
2403
  servers.command("list").description("List servers").option("--org <organizationId>", "Organization ID").option("--project <projectId>", "Filter by project ID").option("--limit <n>", "Maximum results (default 25)", parsePositiveIntOption("limit"), "25").action(runAction(async (opts) => {
@@ -2454,20 +2479,33 @@ function registerServerCommands(program2) {
2454
2479
  return;
2455
2480
  }
2456
2481
  for (const event of data.events) {
2457
- const ts = new Date(event.createdAt).toISOString();
2458
- console.log(`${ts} [${event.type}] ${event.message}`);
2482
+ const ts = formatEventTimestamp(event.timestamp);
2483
+ const level = (event.level || "info").toUpperCase().padEnd(5);
2484
+ const stage = (event.stage || "").padEnd(8);
2485
+ console.log(`${ts} ${level} ${stage} ${event.message}`);
2459
2486
  }
2460
2487
  }));
2461
2488
  }
2462
2489
 
2463
2490
  // src/commands/tools.ts
2464
2491
  function registerToolCommands(program2) {
2465
- const tools = program2.command("tools").description("Inspect tools on an MCP server");
2466
- tools.command("list <serverId>").description("List tools for a server").option("--org <organizationId>", "Organization ID").action(runAction(async (serverId, opts) => {
2492
+ const tools = program2.command("tools").description("Inspect tools on a project or server");
2493
+ 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) => {
2467
2494
  const orgId = await resolveOrgId(opts.org);
2495
+ if (!opts.project && !opts.server) {
2496
+ throw new Error("Pass either --project <projectId> or --server <serverId>.");
2497
+ }
2498
+ let projectId = opts.project;
2499
+ if (!projectId && opts.server) {
2500
+ const serverData = await api.get("/api/v1/server", {
2501
+ organizationId: orgId,
2502
+ serverId: opts.server
2503
+ });
2504
+ projectId = serverData.server.projectId;
2505
+ }
2468
2506
  const data = await api.get("/api/v1/project/tools", {
2469
2507
  organizationId: orgId,
2470
- serverId
2508
+ projectId
2471
2509
  });
2472
2510
  if (isJsonMode()) {
2473
2511
  printJson(data);
@@ -2812,29 +2850,112 @@ function registerApiKeyCommands(program2) {
2812
2850
  }));
2813
2851
  }
2814
2852
 
2853
+ // src/commands/config.ts
2854
+ import { homedir as homedir2 } from "node:os";
2855
+ import { join as join3 } from "node:path";
2856
+ function configFilePath() {
2857
+ return join3(homedir2(), ".mcpcloud", "config.json");
2858
+ }
2859
+ function previewKey2(key) {
2860
+ if (!key)
2861
+ return;
2862
+ if (key.length <= 12)
2863
+ return `${key.slice(0, 4)}…`;
2864
+ return `${key.slice(0, 8)}…${key.slice(-4)}`;
2865
+ }
2866
+ function registerConfigCommands(program2) {
2867
+ const config = program2.command("config").description("Manage CLI configuration in ~/.mcpcloud/config.json");
2868
+ config.command("show").description("Print the current saved config (API key is redacted)").action(runAction(() => {
2869
+ const c = readConfig();
2870
+ const safe = {
2871
+ path: configFilePath(),
2872
+ baseUrl: c.baseUrl ?? null,
2873
+ defaultOrganizationId: c.defaultOrganizationId ?? null,
2874
+ apiKeyPreview: previewKey2(c.apiKey) ?? null
2875
+ };
2876
+ if (isJsonMode()) {
2877
+ printJson(safe);
2878
+ return;
2879
+ }
2880
+ printKeyValue({
2881
+ path: safe.path,
2882
+ "base url": safe.baseUrl ?? "—",
2883
+ "default org": safe.defaultOrganizationId ?? "—",
2884
+ "api key": safe.apiKeyPreview ?? "—"
2885
+ });
2886
+ }));
2887
+ config.command("set-url <url>").description("Save the API base URL to ~/.mcpcloud/config.json").action(runAction((url) => {
2888
+ try {
2889
+ new URL(url);
2890
+ } catch {
2891
+ throw new Error(`Invalid URL: ${url}`);
2892
+ }
2893
+ const c = readConfig();
2894
+ c.baseUrl = url;
2895
+ writeConfig(c);
2896
+ if (isJsonMode()) {
2897
+ printJson({ baseUrl: url });
2898
+ return;
2899
+ }
2900
+ printSuccess(`Saved base URL: ${url}`);
2901
+ }));
2902
+ config.command("clear-url").description("Remove the saved base URL (falls back to MCPCLOUD_BASE_URL)").action(runAction(() => {
2903
+ const c = readConfig();
2904
+ delete c.baseUrl;
2905
+ writeConfig(c);
2906
+ if (isJsonMode()) {
2907
+ printJson({ baseUrl: null });
2908
+ return;
2909
+ }
2910
+ printSuccess("Cleared saved base URL.");
2911
+ }));
2912
+ config.command("set-org <organizationId>").description("Save a default organization ID").action(runAction((organizationId) => {
2913
+ const c = readConfig();
2914
+ c.defaultOrganizationId = organizationId;
2915
+ writeConfig(c);
2916
+ if (isJsonMode()) {
2917
+ printJson({ defaultOrganizationId: organizationId });
2918
+ return;
2919
+ }
2920
+ printSuccess(`Saved default organization: ${organizationId}`);
2921
+ }));
2922
+ config.command("clear-org").description("Remove the saved default organization ID").action(runAction(() => {
2923
+ const c = readConfig();
2924
+ delete c.defaultOrganizationId;
2925
+ writeConfig(c);
2926
+ if (isJsonMode()) {
2927
+ printJson({ defaultOrganizationId: null });
2928
+ return;
2929
+ }
2930
+ printSuccess("Cleared saved default organization.");
2931
+ }));
2932
+ }
2933
+
2815
2934
  // src/create-program.ts
2816
2935
  function createProgram() {
2817
2936
  const program2 = new Command;
2818
- program2.name("mcpsh").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>", "Override the API base URL (defaults to MCPCLOUD_BASE_URL or https://mcpcloud.sh)").hook("preAction", (thisCommand) => {
2937
+ program2.name("mcpsh").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 `mcpsh config set-url`.").hook("preAction", (thisCommand) => {
2819
2938
  const opts = thisCommand.optsWithGlobals();
2820
2939
  setJsonMode(Boolean(opts.json));
2821
2940
  setBaseUrlOverride(opts.baseUrl);
2822
2941
  }).addHelpText("after", [
2823
2942
  "",
2824
2943
  "Environment variables:",
2825
- " MCPCLOUD_API_KEY API key used for authentication",
2826
- " MCPCLOUD_BASE_URL Override the API base URL",
2944
+ " MCPCLOUD_API_KEY API key used for authentication (required)",
2945
+ " MCPCLOUD_BASE_URL API base URL (required — no default is shipped)",
2827
2946
  " MCPCLOUD_ORG_ID Default organization ID",
2828
2947
  "",
2829
2948
  "Examples:",
2949
+ " $ export MCPCLOUD_BASE_URL=https://your-deployment.example.com",
2950
+ " $ mcpsh config set-url https://your-deployment.example.com # persist it",
2830
2951
  " $ mcpsh login",
2831
2952
  " $ mcpsh whoami",
2832
- " $ mcpsh projects list",
2833
2953
  " $ mcpsh skills connect skill_123 --agent claude-code --apply",
2834
2954
  " $ mcpsh --json servers list"
2835
2955
  ].join(`
2836
2956
  `));
2837
2957
  registerAuthCommands(program2);
2958
+ registerConfigCommands(program2);
2838
2959
  registerProjectCommands(program2);
2839
2960
  registerServerCommands(program2);
2840
2961
  registerToolCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpcloud/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "The official CLI for MCPCloud — manage projects, servers, skills, and API keys from the terminal",
5
5
  "type": "module",
6
6
  "bin": {