@mcp-s/cli 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -263,7 +263,7 @@ mcp-s-cli clear-auth
263
263
 
264
264
  ## Config File Format
265
265
 
266
- The config file lives at `~/.config/mcp-s-cli/config.json` (or a custom path via `-c`/`MCP_CONFIG_PATH`).
266
+ The config file lives at `~/.config/mcp-s-cli/config.json` (or a custom path via `-c`/`MCP_S_CLI_CONFIG_PATH`).
267
267
 
268
268
  ```json
269
269
  {
@@ -296,10 +296,36 @@ For stdio mode (using a user access key):
296
296
  | `userAccessKey` | User access key — triggers stdio mode via `npx @mcp-s/mcp` |
297
297
  | `allowedTools` | Glob patterns of tools to allow (optional) |
298
298
  | `disabledTools` | Glob patterns of tools to exclude (optional) |
299
+ | `settings` | Per-agent behavioral settings (see below) |
299
300
 
300
301
  Environment variable substitution is supported: `"token": "${MY_TOKEN}"`.
301
302
 
302
- ---
303
+ ### `settings` block
304
+
305
+ The optional `settings` block lets you tune behavioral settings per config file — useful when different AI agents use different configs and need different tuning:
306
+
307
+ ```json
308
+ {
309
+ "org": "my-org",
310
+ "settings": {
311
+ "timeout": 60,
312
+ "maxRetries": 1,
313
+ "retryDelay": 500,
314
+ "daemon": false,
315
+ "daemonTimeout": 120,
316
+ "history": true
317
+ }
318
+ }
319
+ ```
320
+
321
+ | Field | Description | Default |
322
+ | --------------- | --------------------------------------------------- | ------- |
323
+ | `timeout` | Request timeout (seconds) | `1800` |
324
+ | `maxRetries` | Retry attempts for transient errors (`0` = disable) | `3` |
325
+ | `retryDelay` | Base retry delay (milliseconds) | `1000` |
326
+ | `daemon` | Enable connection caching (daemon mode) | `true` |
327
+ | `daemonTimeout` | Idle timeout for cached connections (seconds) | `300` |
328
+ | `history` | Append each invocation to `history.jsonl` | `false` |
303
329
 
304
330
  ## Working with Complex JSON Arguments
305
331
 
@@ -355,15 +381,11 @@ fi
355
381
 
356
382
  ### Environment Variables
357
383
 
358
- | Variable | Description | Default |
359
- | -------------------- | ------------------------------------------------- | ------- |
360
- | `MCP_DEBUG` | Enable debug output | `false` |
361
- | `MCP_TIMEOUT` | Request timeout (seconds) | `1800` |
362
- | `MCP_MAX_RETRIES` | Retry attempts for transient errors (0 = disable) | `3` |
363
- | `MCP_RETRY_DELAY` | Base retry delay (milliseconds) | `1000` |
364
- | `MCP_STRICT_ENV` | Error on missing `${VAR}` in config | `true` |
365
- | `MCP_DAEMON` | Enable connection caching (daemon mode) | `true` |
366
- | `MCP_DAEMON_TIMEOUT` | Idle timeout for cached connections (seconds) | `300` |
384
+ | Variable | Description | Default |
385
+ | ----------------------- | ----------------------------------------------------- | ------- |
386
+ | `MCP_S_CLI_DEBUG` | Enable debug output | |
387
+ | `MCP_S_CLI_STRICT_ENV` | Error on missing `${VAR}` in config (`false` to warn) | `true` |
388
+ | `MCP_S_CLI_CONFIG_PATH` | Override config file path | |
367
389
 
368
390
  ---
369
391
 
@@ -431,14 +453,15 @@ Each CLI invocation connects to the single configured server.
431
453
 
432
454
  ### Connection Pooling (Daemon)
433
455
 
434
- Daemon mode is enabled by default. The daemon keeps the MCP server connection open for `MCP_DAEMON_TIMEOUT` seconds (default: 300s) after the last request, avoiding repeated startup latency on subsequent calls.
456
+ Daemon mode is enabled by default. The daemon keeps the server connection open for `settings.daemonTimeout` seconds (default: 300s) after the last request, avoiding repeated startup latency on subsequent calls.
435
457
 
436
458
  ```bash
437
459
  mcp-s-cli call some_tool '{}' # Uses cached connection (default)
438
- MCP_DAEMON=0 mcp-s-cli info # Disable daemon, use a fresh connection
439
- MCP_DEBUG=1 mcp-s-cli info # See connection debug output
460
+ MCP_S_CLI_DEBUG=1 mcp-s-cli info # See connection debug output
440
461
  ```
441
462
 
463
+ To disable daemon for a specific config, add `"settings": { "daemon": false }` to `config.json`.
464
+
442
465
  With daemon disabled, each CLI call opens a fresh connection and closes it when done.
443
466
 
444
467
  ### Error Handling and Retry
package/SKILL.md CHANGED
@@ -9,21 +9,44 @@ Access a single MCP server through the command line. MCP enables interaction wit
9
9
 
10
10
  ## Commands
11
11
 
12
- | Command | Output |
13
- | -------------------------------- | -------------------------------------------- |
14
- | `mcp-s-cli` | List all available tools |
15
- | `mcp-s-cli info` | Show server details |
16
- | `mcp-s-cli info <tool>` | Get tool JSON schema |
17
- | `mcp-s-cli grep "<pattern>"` | Search tools by name |
18
- | `mcp-s-cli call <tool>` | Call tool (reads JSON from stdin if no args) |
19
- | `mcp-s-cli call <tool> '<json>'` | Call tool with arguments |
12
+ | Command | Output |
13
+ | -------------------------------- | --------------------------------------------------- |
14
+ | `mcp-s-cli` | List all available tools |
15
+ | `mcp-s-cli info <tool>` | Get tool JSON schema |
16
+ | `mcp-s-cli grep "<pattern>"` | Search tools by name (names only, not descriptions) |
17
+ | `mcp-s-cli call <tool>` | Call tool (reads JSON from stdin if no args) |
18
+ | `mcp-s-cli call <tool> '<json>'` | Call tool with arguments |
20
19
 
21
20
  ## Workflow
22
21
 
23
- 1. **Discover**: `mcp-s-cli` → see all available tools
22
+ 1. **Discover**: `mcp-s-cli grep "<pattern>"` → find tools by keyword; fall back to `mcp-s-cli` only when you need a full inventory
24
23
  2. **Inspect**: `mcp-s-cli info <tool>` → get full JSON schema
25
24
  3. **Execute**: `mcp-s-cli call <tool> '<json>'` → run with arguments
26
25
 
26
+ ## Cost-Aware Usage
27
+
28
+ Every command consumes tokens. Pick the cheapest operation that gets the job done.
29
+
30
+ ### Discovery: start narrow
31
+
32
+ - **`grep "<pattern>"`** — cheapest. Use when you can guess a keyword from the user's request (e.g., "read a github file" → `grep "file"` or `grep "content"`).
33
+ - **`mcp-s-cli`** (names only) — moderate. Use when you need a full inventory (e.g., "what tools do I have?" or "analyze logs from all tools").
34
+ - **`mcp-s-cli -d`** (names + descriptions) — most expensive. Use only when names alone aren't enough to pick the right tool.
35
+
36
+ **Rule: if you can guess a keyword, grep first.**
37
+
38
+ ### Execution: keep responses small
39
+
40
+ - Use **filter/query parameters** to request only what's needed (e.g., Jira JQL, GitHub search queries, field selectors) instead of fetching everything.
41
+ - **Pipe output** through shell tools (`head`, `grep`, `jq`) to trim large responses before they enter context.
42
+ - Never fetch a full list when the user only needs a few fields (names, links, IDs).
43
+
44
+ ### Decision heuristic
45
+
46
+ - User mentions a specific tool or domain keyword → `grep` for it.
47
+ - User asks a broad question across all tools → list tools first, then act.
48
+ - Tool may return large payloads → use filter params + pipe to trim output.
49
+
27
50
  ## Examples
28
51
 
29
52
  ```bash
@@ -33,17 +56,14 @@ mcp-s-cli
33
56
  # With descriptions
34
57
  mcp-s-cli -d
35
58
 
36
- # Show server details
37
- mcp-s-cli info
38
-
39
59
  # Get tool schema
40
- mcp-s-cli info read_file
60
+ mcp-s-cli info my-tool
41
61
 
42
62
  # Call tool
43
- mcp-s-cli call read_file '{"path": "./README.md"}'
63
+ mcp-s-cli call my-tool '{"arg-name": "arg-value"}'
44
64
 
45
65
  # Pipe from stdin (no '-' needed!)
46
- cat args.json | mcp-s-cli call read_file
66
+ cat args.json | mcp-s-cli call my-tool
47
67
 
48
68
  # Search for tools
49
69
  mcp-s-cli grep "*file*"
@@ -68,24 +88,7 @@ mcp-s-cli call list_directory '{"path": "./src"}' \
68
88
  mcp-s-cli call get_file_contents '{"owner": "x", "repo": "y", "path": "z"}' > output.txt
69
89
  ```
70
90
 
71
- **Note:** `call` outputs raw text content directly (no jq needed for text extraction)
72
-
73
- ## Options
74
-
75
- | Flag | Purpose |
76
- | ----------- | -------------------- |
77
- | `-d` | Include descriptions |
78
- | `-c <path>` | Specify config file |
79
-
80
- ## Config
81
-
82
- The config file (`~/.config/mcp-s-cli/config.json`) uses a flat structure:
83
-
84
- ```json
85
- { "org": "myorg" }
86
- ```
87
-
88
- This derives the server URL as `https://myorg.mcp-s.com/mcp`. Alternatively, use `baseUrl` for a custom URL, or `userAccessKey` for stdio mode.
91
+ **Note:** `call` extracts text content from MCP responses and outputs it directly (no jq needed). Falls back to pretty-printed JSON when the response has no text parts.
89
92
 
90
93
  ## Common Errors
91
94
 
@@ -95,9 +98,14 @@ This derives the server URL as `https://myorg.mcp-s.com/mcp`. Alternatively, use
95
98
  | `mcp-s-cli call` | MISSING_ARGUMENT | Add tool name |
96
99
  | `mcp-s-cli call tool {bad}` | INVALID_JSON | Use valid JSON with quoted string keys |
97
100
 
101
+ ## Debugging
102
+
103
+ Set `MCP_S_CLI_DEBUG=1` to enable verbose debug output to stderr.
104
+
98
105
  ## Exit Codes
99
106
 
100
107
  - `0`: Success
101
108
  - `1`: Client error (bad args, missing config)
102
109
  - `2`: Server error (tool failed)
103
- - `3`: Network error
110
+ - `3`: Network error (connection failed)
111
+ - `4`: Auth error (authentication required)
package/dist/SKILL.md CHANGED
@@ -9,21 +9,44 @@ Access a single MCP server through the command line. MCP enables interaction wit
9
9
 
10
10
  ## Commands
11
11
 
12
- | Command | Output |
13
- | -------------------------------- | -------------------------------------------- |
14
- | `mcp-s-cli` | List all available tools |
15
- | `mcp-s-cli info` | Show server details |
16
- | `mcp-s-cli info <tool>` | Get tool JSON schema |
17
- | `mcp-s-cli grep "<pattern>"` | Search tools by name |
18
- | `mcp-s-cli call <tool>` | Call tool (reads JSON from stdin if no args) |
19
- | `mcp-s-cli call <tool> '<json>'` | Call tool with arguments |
12
+ | Command | Output |
13
+ | -------------------------------- | --------------------------------------------------- |
14
+ | `mcp-s-cli` | List all available tools |
15
+ | `mcp-s-cli info <tool>` | Get tool JSON schema |
16
+ | `mcp-s-cli grep "<pattern>"` | Search tools by name (names only, not descriptions) |
17
+ | `mcp-s-cli call <tool>` | Call tool (reads JSON from stdin if no args) |
18
+ | `mcp-s-cli call <tool> '<json>'` | Call tool with arguments |
20
19
 
21
20
  ## Workflow
22
21
 
23
- 1. **Discover**: `mcp-s-cli` → see all available tools
22
+ 1. **Discover**: `mcp-s-cli grep "<pattern>"` → find tools by keyword; fall back to `mcp-s-cli` only when you need a full inventory
24
23
  2. **Inspect**: `mcp-s-cli info <tool>` → get full JSON schema
25
24
  3. **Execute**: `mcp-s-cli call <tool> '<json>'` → run with arguments
26
25
 
26
+ ## Cost-Aware Usage
27
+
28
+ Every command consumes tokens. Pick the cheapest operation that gets the job done.
29
+
30
+ ### Discovery: start narrow
31
+
32
+ - **`grep "<pattern>"`** — cheapest. Use when you can guess a keyword from the user's request (e.g., "read a github file" → `grep "file"` or `grep "content"`).
33
+ - **`mcp-s-cli`** (names only) — moderate. Use when you need a full inventory (e.g., "what tools do I have?" or "analyze logs from all tools").
34
+ - **`mcp-s-cli -d`** (names + descriptions) — most expensive. Use only when names alone aren't enough to pick the right tool.
35
+
36
+ **Rule: if you can guess a keyword, grep first.**
37
+
38
+ ### Execution: keep responses small
39
+
40
+ - Use **filter/query parameters** to request only what's needed (e.g., Jira JQL, GitHub search queries, field selectors) instead of fetching everything.
41
+ - **Pipe output** through shell tools (`head`, `grep`, `jq`) to trim large responses before they enter context.
42
+ - Never fetch a full list when the user only needs a few fields (names, links, IDs).
43
+
44
+ ### Decision heuristic
45
+
46
+ - User mentions a specific tool or domain keyword → `grep` for it.
47
+ - User asks a broad question across all tools → list tools first, then act.
48
+ - Tool may return large payloads → use filter params + pipe to trim output.
49
+
27
50
  ## Examples
28
51
 
29
52
  ```bash
@@ -33,17 +56,14 @@ mcp-s-cli
33
56
  # With descriptions
34
57
  mcp-s-cli -d
35
58
 
36
- # Show server details
37
- mcp-s-cli info
38
-
39
59
  # Get tool schema
40
- mcp-s-cli info read_file
60
+ mcp-s-cli info my-tool
41
61
 
42
62
  # Call tool
43
- mcp-s-cli call read_file '{"path": "./README.md"}'
63
+ mcp-s-cli call my-tool '{"arg-name": "arg-value"}'
44
64
 
45
65
  # Pipe from stdin (no '-' needed!)
46
- cat args.json | mcp-s-cli call read_file
66
+ cat args.json | mcp-s-cli call my-tool
47
67
 
48
68
  # Search for tools
49
69
  mcp-s-cli grep "*file*"
@@ -68,24 +88,7 @@ mcp-s-cli call list_directory '{"path": "./src"}' \
68
88
  mcp-s-cli call get_file_contents '{"owner": "x", "repo": "y", "path": "z"}' > output.txt
69
89
  ```
70
90
 
71
- **Note:** `call` outputs raw text content directly (no jq needed for text extraction)
72
-
73
- ## Options
74
-
75
- | Flag | Purpose |
76
- | ----------- | -------------------- |
77
- | `-d` | Include descriptions |
78
- | `-c <path>` | Specify config file |
79
-
80
- ## Config
81
-
82
- The config file (`~/.config/mcp-s-cli/config.json`) uses a flat structure:
83
-
84
- ```json
85
- { "org": "myorg" }
86
- ```
87
-
88
- This derives the server URL as `https://myorg.mcp-s.com/mcp`. Alternatively, use `baseUrl` for a custom URL, or `userAccessKey` for stdio mode.
91
+ **Note:** `call` extracts text content from MCP responses and outputs it directly (no jq needed). Falls back to pretty-printed JSON when the response has no text parts.
89
92
 
90
93
  ## Common Errors
91
94
 
@@ -95,9 +98,14 @@ This derives the server URL as `https://myorg.mcp-s.com/mcp`. Alternatively, use
95
98
  | `mcp-s-cli call` | MISSING_ARGUMENT | Add tool name |
96
99
  | `mcp-s-cli call tool {bad}` | INVALID_JSON | Use valid JSON with quoted string keys |
97
100
 
101
+ ## Debugging
102
+
103
+ Set `MCP_S_CLI_DEBUG=1` to enable verbose debug output to stderr.
104
+
98
105
  ## Exit Codes
99
106
 
100
107
  - `0`: Success
101
108
  - `1`: Client error (bad args, missing config)
102
109
  - `2`: Server error (tool failed)
103
- - `3`: Network error
110
+ - `3`: Network error (connection failed)
111
+ - `4`: Auth error (authentication required)
package/dist/daemon.js CHANGED
@@ -64,7 +64,7 @@ async function saveAuthData() {
64
64
  mode: 384
65
65
  });
66
66
  } catch (err) {
67
- if (process.env.MCP_DEBUG) {
67
+ if (process.env.MCP_S_CLI_DEBUG) {
68
68
  console.error(
69
69
  `[mcp-s-cli] Failed to save auth data: ${err.message}`
70
70
  );
@@ -358,48 +358,32 @@ var DEFAULT_MAX_RETRIES = 3;
358
358
  var DEFAULT_RETRY_DELAY_MS = 1e3;
359
359
  var DEFAULT_DAEMON_TIMEOUT_SECONDS = 300;
360
360
  function debug(message) {
361
- if (process.env.MCP_DEBUG) {
361
+ if (process.env.MCP_S_CLI_DEBUG) {
362
362
  console.error(`[mcp-s-cli] ${message}`);
363
363
  }
364
364
  }
365
- function getTimeoutMs() {
366
- const envTimeout = process.env.MCP_TIMEOUT;
367
- if (envTimeout) {
368
- const seconds = Number.parseInt(envTimeout, 10);
369
- if (!Number.isNaN(seconds) && seconds > 0) {
370
- return seconds * 1e3;
371
- }
365
+ function getTimeoutMs(settings) {
366
+ if (settings?.timeout != null && settings.timeout > 0) {
367
+ return settings.timeout * 1e3;
372
368
  }
373
369
  return DEFAULT_TIMEOUT_MS;
374
370
  }
375
- function getMaxRetries() {
376
- const envRetries = process.env.MCP_MAX_RETRIES;
377
- if (envRetries) {
378
- const retries = Number.parseInt(envRetries, 10);
379
- if (!Number.isNaN(retries) && retries >= 0) {
380
- return retries;
381
- }
371
+ function getMaxRetries(settings) {
372
+ if (settings?.maxRetries != null && settings.maxRetries >= 0) {
373
+ return settings.maxRetries;
382
374
  }
383
375
  return DEFAULT_MAX_RETRIES;
384
376
  }
385
- function getRetryDelayMs() {
386
- const envDelay = process.env.MCP_RETRY_DELAY;
387
- if (envDelay) {
388
- const delay = Number.parseInt(envDelay, 10);
389
- if (!Number.isNaN(delay) && delay > 0) {
390
- return delay;
391
- }
377
+ function getRetryDelayMs(settings) {
378
+ if (settings?.retryDelay != null && settings.retryDelay > 0) {
379
+ return settings.retryDelay;
392
380
  }
393
381
  return DEFAULT_RETRY_DELAY_MS;
394
382
  }
395
383
  var DAEMON_SERVER_NAME = "mcp-s-cli";
396
- function getDaemonTimeoutMs() {
397
- const envTimeout = process.env.MCP_DAEMON_TIMEOUT;
398
- if (envTimeout) {
399
- const seconds = Number.parseInt(envTimeout, 10);
400
- if (!Number.isNaN(seconds) && seconds > 0) {
401
- return seconds * 1e3;
402
- }
384
+ function getDaemonTimeoutMs(settings) {
385
+ if (settings?.daemonTimeout != null && settings.daemonTimeout > 0) {
386
+ return settings.daemonTimeout * 1e3;
403
387
  }
404
388
  return DEFAULT_DAEMON_TIMEOUT_SECONDS * 1e3;
405
389
  }
@@ -430,13 +414,13 @@ import { join as join2 } from "path";
430
414
  import { fileURLToPath } from "url";
431
415
 
432
416
  // src/version.ts
433
- var VERSION = "0.0.9";
417
+ var VERSION = "0.0.10";
434
418
 
435
419
  // src/client.ts
436
- function getRetryConfig() {
437
- const totalBudgetMs = getTimeoutMs();
438
- const maxRetries = getMaxRetries();
439
- const baseDelayMs = getRetryDelayMs();
420
+ function getRetryConfig(settings) {
421
+ const totalBudgetMs = getTimeoutMs(settings);
422
+ const maxRetries = getMaxRetries(settings);
423
+ const baseDelayMs = getRetryDelayMs(settings);
440
424
  const retryBudgetMs = Math.max(0, totalBudgetMs - 5e3);
441
425
  return {
442
426
  maxRetries,
@@ -567,7 +551,7 @@ ${stderrOutput}`;
567
551
  }, `connect to ${serverName}`);
568
552
  }
569
553
  async function connectToHttpServer(serverName, config) {
570
- const oauthEnabled = process.env.MCP_NO_OAUTH !== "1";
554
+ const oauthEnabled = process.env.MCP_S_CLI_NO_OAUTH !== "1";
571
555
  return withRetry(async () => {
572
556
  const configuredAuth = config.headers?.Authorization || config.headers?.authorization;
573
557
  let headers = { ...config.headers };
@@ -717,18 +701,22 @@ async function listTools(client) {
717
701
  }));
718
702
  }, "list tools");
719
703
  }
720
- async function callTool(client, toolName, args) {
721
- return withRetry(async () => {
722
- const result = await client.callTool(
723
- {
724
- name: toolName,
725
- arguments: args
726
- },
727
- void 0,
728
- { timeout: getTimeoutMs() }
729
- );
730
- return result;
731
- }, `call tool ${toolName}`);
704
+ async function callTool(client, toolName, args, settings) {
705
+ return withRetry(
706
+ async () => {
707
+ const result = await client.callTool(
708
+ {
709
+ name: toolName,
710
+ arguments: args
711
+ },
712
+ void 0,
713
+ { timeout: getTimeoutMs(settings) }
714
+ );
715
+ return result;
716
+ },
717
+ `call tool ${toolName}`,
718
+ getRetryConfig(settings)
719
+ );
732
720
  }
733
721
 
734
722
  // src/daemon.ts
@@ -800,10 +788,10 @@ function killProcess(pid) {
800
788
  return false;
801
789
  }
802
790
  }
803
- async function runDaemon(serverName, config) {
791
+ async function runDaemon(serverName, config, settings) {
804
792
  const socketPath = getSocketPath();
805
793
  const configHash = getConfigHash(config);
806
- const timeoutMs = getDaemonTimeoutMs();
794
+ const timeoutMs = getDaemonTimeoutMs(settings);
807
795
  let idleTimer = null;
808
796
  let mcpClient = null;
809
797
  let server = null;
@@ -995,8 +983,11 @@ async function runDaemon(serverName, config) {
995
983
  if (process.argv[2] === "--daemon") {
996
984
  const serverName = process.argv[3];
997
985
  const configJson = process.argv[4];
986
+ const settingsJson = process.argv[5];
998
987
  if (!serverName || !configJson) {
999
- console.error("Usage: daemon.ts --daemon <serverName> <configJson>");
988
+ console.error(
989
+ "Usage: daemon.ts --daemon <serverName> <configJson> [settingsJson]"
990
+ );
1000
991
  process.exit(1);
1001
992
  }
1002
993
  let config;
@@ -1006,7 +997,16 @@ if (process.argv[2] === "--daemon") {
1006
997
  console.error("Invalid config JSON");
1007
998
  process.exit(1);
1008
999
  }
1009
- runDaemon(serverName, config).catch((error) => {
1000
+ let settings;
1001
+ if (settingsJson) {
1002
+ try {
1003
+ settings = JSON.parse(settingsJson);
1004
+ } catch {
1005
+ console.error("Invalid settings JSON");
1006
+ process.exit(1);
1007
+ }
1008
+ }
1009
+ runDaemon(serverName, config, settings).catch((error) => {
1010
1010
  console.error("Daemon failed:", error);
1011
1011
  process.exit(1);
1012
1012
  });
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ async function saveAuthData() {
60
60
  mode: 384
61
61
  });
62
62
  } catch (err) {
63
- if (process.env.MCP_DEBUG) {
63
+ if (process.env.MCP_S_CLI_DEBUG) {
64
64
  console.error(
65
65
  `[mcp-s-cli] Failed to save auth data: ${err.message}`
66
66
  );
@@ -583,51 +583,35 @@ var DEFAULT_MAX_RETRIES = 3;
583
583
  var DEFAULT_RETRY_DELAY_MS = 1e3;
584
584
  var DEFAULT_DAEMON_TIMEOUT_SECONDS = 300;
585
585
  function debug(message) {
586
- if (process.env.MCP_DEBUG) {
586
+ if (process.env.MCP_S_CLI_DEBUG) {
587
587
  console.error(`[mcp-s-cli] ${message}`);
588
588
  }
589
589
  }
590
- function getTimeoutMs() {
591
- const envTimeout = process.env.MCP_TIMEOUT;
592
- if (envTimeout) {
593
- const seconds = Number.parseInt(envTimeout, 10);
594
- if (!Number.isNaN(seconds) && seconds > 0) {
595
- return seconds * 1e3;
596
- }
590
+ function getTimeoutMs(settings) {
591
+ if (settings?.timeout != null && settings.timeout > 0) {
592
+ return settings.timeout * 1e3;
597
593
  }
598
594
  return DEFAULT_TIMEOUT_MS;
599
595
  }
600
- function getMaxRetries() {
601
- const envRetries = process.env.MCP_MAX_RETRIES;
602
- if (envRetries) {
603
- const retries = Number.parseInt(envRetries, 10);
604
- if (!Number.isNaN(retries) && retries >= 0) {
605
- return retries;
606
- }
596
+ function getMaxRetries(settings) {
597
+ if (settings?.maxRetries != null && settings.maxRetries >= 0) {
598
+ return settings.maxRetries;
607
599
  }
608
600
  return DEFAULT_MAX_RETRIES;
609
601
  }
610
- function getRetryDelayMs() {
611
- const envDelay = process.env.MCP_RETRY_DELAY;
612
- if (envDelay) {
613
- const delay = Number.parseInt(envDelay, 10);
614
- if (!Number.isNaN(delay) && delay > 0) {
615
- return delay;
616
- }
602
+ function getRetryDelayMs(settings) {
603
+ if (settings?.retryDelay != null && settings.retryDelay > 0) {
604
+ return settings.retryDelay;
617
605
  }
618
606
  return DEFAULT_RETRY_DELAY_MS;
619
607
  }
620
608
  var DAEMON_SERVER_NAME = "mcp-s-cli";
621
- function isDaemonEnabled() {
622
- return process.env.MCP_DAEMON !== "0";
609
+ function isDaemonEnabled(settings) {
610
+ return settings?.daemon !== false;
623
611
  }
624
- function getDaemonTimeoutMs() {
625
- const envTimeout = process.env.MCP_DAEMON_TIMEOUT;
626
- if (envTimeout) {
627
- const seconds = Number.parseInt(envTimeout, 10);
628
- if (!Number.isNaN(seconds) && seconds > 0) {
629
- return seconds * 1e3;
630
- }
612
+ function getDaemonTimeoutMs(settings) {
613
+ if (settings?.daemonTimeout != null && settings.daemonTimeout > 0) {
614
+ return settings.daemonTimeout * 1e3;
631
615
  }
632
616
  return DEFAULT_DAEMON_TIMEOUT_SECONDS * 1e3;
633
617
  }
@@ -650,7 +634,7 @@ function getConfigHash(config) {
650
634
  return createHash("sha256").update(str).digest("hex").slice(0, 16);
651
635
  }
652
636
  function isStrictEnvMode() {
653
- const value = process.env.MCP_STRICT_ENV?.toLowerCase();
637
+ const value = process.env.MCP_S_CLI_STRICT_ENV?.toLowerCase();
654
638
  return value !== "false" && value !== "0";
655
639
  }
656
640
  function substituteEnvVars(value) {
@@ -673,7 +657,7 @@ function substituteEnvVars(value) {
673
657
  type: "MISSING_ENV_VAR",
674
658
  message,
675
659
  details: "Referenced in config but not set in environment",
676
- suggestion: `Set the variable(s) before running: export ${missingVars[0]}="value" or set MCP_STRICT_ENV=false to use empty values`
660
+ suggestion: `Set the variable(s) before running: export ${missingVars[0]}="value" or set MCP_S_CLI_STRICT_ENV=false to use empty values`
677
661
  })
678
662
  );
679
663
  }
@@ -748,8 +732,8 @@ async function loadConfig(explicitPath) {
748
732
  let configPath;
749
733
  if (explicitPath) {
750
734
  configPath = resolve(explicitPath);
751
- } else if (process.env.MCP_CONFIG_PATH) {
752
- configPath = resolve(process.env.MCP_CONFIG_PATH);
735
+ } else if (process.env.MCP_S_CLI_CONFIG_PATH) {
736
+ configPath = resolve(process.env.MCP_S_CLI_CONFIG_PATH);
753
737
  }
754
738
  if (configPath) {
755
739
  if (!existsSync(configPath)) {
@@ -787,7 +771,8 @@ async function loadConfig(explicitPath) {
787
771
  "userAccessKey",
788
772
  "token",
789
773
  "allowedTools",
790
- "disabledTools"
774
+ "disabledTools",
775
+ "settings"
791
776
  ];
792
777
  const hasKnownField = knownFields.some((f) => f in raw);
793
778
  if (!hasKnownField) {
@@ -795,7 +780,7 @@ async function loadConfig(explicitPath) {
795
780
  }
796
781
  raw = substituteEnvVarsInObject(raw);
797
782
  const serverConfig = deriveServerConfig(raw);
798
- return { raw, serverConfig };
783
+ return { raw, serverConfig, settings: raw.settings ?? {} };
799
784
  }
800
785
 
801
786
  // src/daemon-client.ts
@@ -883,10 +868,10 @@ function killProcess(pid) {
883
868
  return false;
884
869
  }
885
870
  }
886
- async function runDaemon(serverName, config) {
871
+ async function runDaemon(serverName, config, settings) {
887
872
  const socketPath = getSocketPath();
888
873
  const configHash = getConfigHash(config);
889
- const timeoutMs = getDaemonTimeoutMs();
874
+ const timeoutMs = getDaemonTimeoutMs(settings);
890
875
  let idleTimer = null;
891
876
  let mcpClient = null;
892
877
  let server = null;
@@ -1078,8 +1063,11 @@ async function runDaemon(serverName, config) {
1078
1063
  if (process.argv[2] === "--daemon") {
1079
1064
  const serverName = process.argv[3];
1080
1065
  const configJson = process.argv[4];
1066
+ const settingsJson = process.argv[5];
1081
1067
  if (!serverName || !configJson) {
1082
- console.error("Usage: daemon.ts --daemon <serverName> <configJson>");
1068
+ console.error(
1069
+ "Usage: daemon.ts --daemon <serverName> <configJson> [settingsJson]"
1070
+ );
1083
1071
  process.exit(1);
1084
1072
  }
1085
1073
  let config;
@@ -1089,7 +1077,16 @@ if (process.argv[2] === "--daemon") {
1089
1077
  console.error("Invalid config JSON");
1090
1078
  process.exit(1);
1091
1079
  }
1092
- runDaemon(serverName, config).catch((error) => {
1080
+ let settings;
1081
+ if (settingsJson) {
1082
+ try {
1083
+ settings = JSON.parse(settingsJson);
1084
+ } catch {
1085
+ console.error("Invalid settings JSON");
1086
+ process.exit(1);
1087
+ }
1088
+ }
1089
+ runDaemon(serverName, config, settings).catch((error) => {
1093
1090
  console.error("Daemon failed:", error);
1094
1091
  process.exit(1);
1095
1092
  });
@@ -1175,17 +1172,18 @@ function isDaemonValid(serverName, config) {
1175
1172
  }
1176
1173
  return true;
1177
1174
  }
1178
- async function spawnDaemon(serverName, config) {
1175
+ async function spawnDaemon(serverName, config, settings) {
1179
1176
  debug(`[daemon-client] Spawning daemon for ${serverName}`);
1180
1177
  const __dirname = join2(fileURLToPath(import.meta.url), "..");
1181
1178
  const daemonScript = join2(__dirname, "daemon.js");
1182
1179
  const configJson = JSON.stringify(config);
1180
+ const settingsJson = JSON.stringify(settings ?? {});
1183
1181
  const proc = spawn(
1184
1182
  "node",
1185
- [daemonScript, "--daemon", serverName, configJson],
1183
+ [daemonScript, "--daemon", serverName, configJson, settingsJson],
1186
1184
  {
1187
1185
  stdio: "ignore",
1188
- env: { ...process.env, MCP_NO_OAUTH: "1" },
1186
+ env: { ...process.env, MCP_S_CLI_NO_OAUTH: "1" },
1189
1187
  detached: true
1190
1188
  }
1191
1189
  );
@@ -1208,10 +1206,10 @@ async function spawnDaemon(serverName, config) {
1208
1206
  debug(`[daemon-client] Daemon spawn timeout for ${serverName}`);
1209
1207
  return false;
1210
1208
  }
1211
- async function getDaemonConnection(serverName, config) {
1209
+ async function getDaemonConnection(serverName, config, settings) {
1212
1210
  const socketPath = getSocketPath();
1213
1211
  if (!isDaemonValid(serverName, config)) {
1214
- const spawned = await spawnDaemon(serverName, config);
1212
+ const spawned = await spawnDaemon(serverName, config, settings);
1215
1213
  if (!spawned) {
1216
1214
  debug(`[daemon-client] Failed to spawn daemon for ${serverName}`);
1217
1215
  return null;
@@ -1299,13 +1297,13 @@ async function cleanupOrphanedDaemons() {
1299
1297
  }
1300
1298
 
1301
1299
  // src/version.ts
1302
- var VERSION = "0.0.9";
1300
+ var VERSION = "0.0.10";
1303
1301
 
1304
1302
  // src/client.ts
1305
- function getRetryConfig() {
1306
- const totalBudgetMs = getTimeoutMs();
1307
- const maxRetries = getMaxRetries();
1308
- const baseDelayMs = getRetryDelayMs();
1303
+ function getRetryConfig(settings) {
1304
+ const totalBudgetMs = getTimeoutMs(settings);
1305
+ const maxRetries = getMaxRetries(settings);
1306
+ const baseDelayMs = getRetryDelayMs(settings);
1309
1307
  const retryBudgetMs = Math.max(0, totalBudgetMs - 5e3);
1310
1308
  return {
1311
1309
  maxRetries,
@@ -1443,7 +1441,7 @@ ${stderrOutput}`;
1443
1441
  }, `connect to ${serverName}`);
1444
1442
  }
1445
1443
  async function connectToHttpServer(serverName, config) {
1446
- const oauthEnabled = process.env.MCP_NO_OAUTH !== "1";
1444
+ const oauthEnabled = process.env.MCP_S_CLI_NO_OAUTH !== "1";
1447
1445
  return withRetry(async () => {
1448
1446
  const configuredAuth = config.headers?.Authorization || config.headers?.authorization;
1449
1447
  let headers = { ...config.headers };
@@ -1593,24 +1591,32 @@ async function listTools(client) {
1593
1591
  }));
1594
1592
  }, "list tools");
1595
1593
  }
1596
- async function callTool(client, toolName, args) {
1597
- return withRetry(async () => {
1598
- const result = await client.callTool(
1599
- {
1600
- name: toolName,
1601
- arguments: args
1602
- },
1603
- void 0,
1604
- { timeout: getTimeoutMs() }
1605
- );
1606
- return result;
1607
- }, `call tool ${toolName}`);
1594
+ async function callTool(client, toolName, args, settings) {
1595
+ return withRetry(
1596
+ async () => {
1597
+ const result = await client.callTool(
1598
+ {
1599
+ name: toolName,
1600
+ arguments: args
1601
+ },
1602
+ void 0,
1603
+ { timeout: getTimeoutMs(settings) }
1604
+ );
1605
+ return result;
1606
+ },
1607
+ `call tool ${toolName}`,
1608
+ getRetryConfig(settings)
1609
+ );
1608
1610
  }
1609
- async function getConnection(serverName, config) {
1611
+ async function getConnection(serverName, config, settings) {
1610
1612
  await cleanupOrphanedDaemons();
1611
- if (isDaemonEnabled()) {
1613
+ if (isDaemonEnabled(settings)) {
1612
1614
  try {
1613
- const daemonConn = await getDaemonConnection(serverName, config);
1615
+ const daemonConn = await getDaemonConnection(
1616
+ serverName,
1617
+ config,
1618
+ settings
1619
+ );
1614
1620
  if (daemonConn) {
1615
1621
  debug(`Using daemon connection for ${serverName}`);
1616
1622
  return {
@@ -1653,7 +1659,7 @@ async function getConnection(serverName, config) {
1653
1659
  if (!isToolAllowed(toolName, config)) {
1654
1660
  throw new Error(`Tool "${toolName}" is disabled by configuration`);
1655
1661
  }
1656
- return callTool(client, toolName, args);
1662
+ return callTool(client, toolName, args, settings);
1657
1663
  },
1658
1664
  async getInstructions() {
1659
1665
  return client.getInstructions();
@@ -1794,12 +1800,12 @@ function formatToolResult(result) {
1794
1800
  }
1795
1801
 
1796
1802
  // src/commands/call.ts
1797
- async function parseArgs(argsString) {
1803
+ async function parseArgs(argsString, settings) {
1798
1804
  let jsonString;
1799
1805
  if (argsString) {
1800
1806
  jsonString = argsString;
1801
1807
  } else if (!process.stdin.isTTY) {
1802
- const timeoutMs = getTimeoutMs();
1808
+ const timeoutMs = getTimeoutMs(settings);
1803
1809
  const chunks = [];
1804
1810
  let timeoutId;
1805
1811
  const readPromise = (async () => {
@@ -1841,11 +1847,11 @@ async function callCommand(options) {
1841
1847
  console.error(error.message);
1842
1848
  process.exit(1 /* CLIENT_ERROR */);
1843
1849
  }
1844
- const { serverConfig } = loaded;
1850
+ const { serverConfig, settings } = loaded;
1845
1851
  const serverLabel = "server";
1846
1852
  if (options.args !== void 0) {
1847
1853
  try {
1848
- await parseArgs(options.args);
1854
+ await parseArgs(options.args, settings);
1849
1855
  } catch (error) {
1850
1856
  console.error(error.message);
1851
1857
  process.exit(1 /* CLIENT_ERROR */);
@@ -1853,14 +1859,14 @@ async function callCommand(options) {
1853
1859
  }
1854
1860
  let args;
1855
1861
  try {
1856
- args = await parseArgs(options.args);
1862
+ args = await parseArgs(options.args, settings);
1857
1863
  } catch (error) {
1858
1864
  console.error(error.message);
1859
1865
  process.exit(1 /* CLIENT_ERROR */);
1860
1866
  }
1861
1867
  let connection;
1862
1868
  try {
1863
- connection = await getConnection(serverLabel, serverConfig);
1869
+ connection = await getConnection(serverLabel, serverConfig, settings);
1864
1870
  } catch (error) {
1865
1871
  console.error(
1866
1872
  formatCliError(
@@ -1970,7 +1976,7 @@ async function grepCommand(options) {
1970
1976
  console.error(error.message);
1971
1977
  process.exit(1 /* CLIENT_ERROR */);
1972
1978
  }
1973
- const { serverConfig } = loaded;
1979
+ const { serverConfig, settings } = loaded;
1974
1980
  const serverLabel = "server";
1975
1981
  const pattern = globToRegex(options.pattern);
1976
1982
  debug(`Searching for pattern "${options.pattern}"`);
@@ -1978,7 +1984,7 @@ async function grepCommand(options) {
1978
1984
  const allResults = [];
1979
1985
  let searchError;
1980
1986
  try {
1981
- connection = await getConnection(serverLabel, serverConfig);
1987
+ connection = await getConnection(serverLabel, serverConfig, settings);
1982
1988
  const tools = await connection.listTools();
1983
1989
  for (const tool of tools) {
1984
1990
  if (pattern.test(tool.name)) {
@@ -2016,11 +2022,11 @@ async function infoCommand(options) {
2016
2022
  console.error(error.message);
2017
2023
  process.exit(1 /* CLIENT_ERROR */);
2018
2024
  }
2019
- const { serverConfig } = loaded;
2025
+ const { serverConfig, settings } = loaded;
2020
2026
  const serverLabel = "server";
2021
2027
  let connection;
2022
2028
  try {
2023
- connection = await getConnection(serverLabel, serverConfig);
2029
+ connection = await getConnection(serverLabel, serverConfig, settings);
2024
2030
  } catch (error) {
2025
2031
  console.error(
2026
2032
  formatCliError(
@@ -2062,7 +2068,7 @@ async function infoCommand(options) {
2062
2068
  }
2063
2069
 
2064
2070
  // src/commands/init.ts
2065
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2071
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
2066
2072
  import { homedir as homedir4 } from "os";
2067
2073
  import { dirname as dirname4, join as join5 } from "path";
2068
2074
  import * as readline from "readline";
@@ -2480,6 +2486,15 @@ var GLOBAL_CONFIG_PATH2 = join5(
2480
2486
  "mcp-s-cli",
2481
2487
  "config.json"
2482
2488
  );
2489
+ function readExistingConfig() {
2490
+ try {
2491
+ if (existsSync6(GLOBAL_CONFIG_PATH2)) {
2492
+ return JSON.parse(readFileSync4(GLOBAL_CONFIG_PATH2, "utf-8"));
2493
+ }
2494
+ } catch {
2495
+ }
2496
+ return {};
2497
+ }
2483
2498
  function writeConfig(config) {
2484
2499
  mkdirSync4(dirname4(GLOBAL_CONFIG_PATH2), { recursive: true });
2485
2500
  writeFileSync4(
@@ -2491,7 +2506,9 @@ function writeConfig(config) {
2491
2506
  }
2492
2507
  async function initCommand(options) {
2493
2508
  const { baseUrl, org, mcp, toolkit, userAccessKey, token } = options;
2509
+ const existing = readExistingConfig();
2494
2510
  const config = {};
2511
+ if (existing.settings) config.settings = existing.settings;
2495
2512
  if (org) {
2496
2513
  config.org = org;
2497
2514
  } else if (baseUrl) {
@@ -2654,14 +2671,14 @@ async function listCommand(options) {
2654
2671
  console.error(error.message);
2655
2672
  process.exit(1 /* CLIENT_ERROR */);
2656
2673
  }
2657
- const { serverConfig } = loaded;
2674
+ const { serverConfig, settings } = loaded;
2658
2675
  const serverLabel = "server";
2659
2676
  let connection = null;
2660
2677
  let tools = [];
2661
2678
  let instructions;
2662
2679
  let errorMsg;
2663
2680
  try {
2664
- connection = await getConnection(serverLabel, serverConfig);
2681
+ connection = await getConnection(serverLabel, serverConfig, settings);
2665
2682
  tools = await connection.listTools();
2666
2683
  instructions = await connection.getInstructions();
2667
2684
  debug(`${serverLabel}: loaded ${tools.length} tools`);
@@ -2703,15 +2720,15 @@ async function logoutCommand(options) {
2703
2720
  }
2704
2721
 
2705
2722
  // src/commands/whoami.ts
2706
- import { existsSync as existsSync8, readFileSync as readFileSync4, statSync } from "fs";
2723
+ import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "fs";
2707
2724
  import { homedir as homedir5 } from "os";
2708
2725
  import { join as join6, resolve as resolve3 } from "path";
2709
2726
  function getResolvedConfigPath(explicitPath) {
2710
2727
  if (explicitPath) {
2711
2728
  return resolve3(explicitPath);
2712
2729
  }
2713
- if (process.env.MCP_CONFIG_PATH) {
2714
- return resolve3(process.env.MCP_CONFIG_PATH);
2730
+ if (process.env.MCP_S_CLI_CONFIG_PATH) {
2731
+ return resolve3(process.env.MCP_S_CLI_CONFIG_PATH);
2715
2732
  }
2716
2733
  const defaultPath = join6(homedir5(), ".config", "mcp-s-cli", "config.json");
2717
2734
  if (existsSync8(defaultPath)) {
@@ -2750,7 +2767,7 @@ async function whoamiCommand(options) {
2750
2767
  console.log(" (no tokens stored)");
2751
2768
  } else {
2752
2769
  try {
2753
- const raw2 = readFileSync4(authFilePath, "utf-8");
2770
+ const raw2 = readFileSync5(authFilePath, "utf-8");
2754
2771
  const data = JSON.parse(raw2);
2755
2772
  const tokenEntries = Object.entries(data.tokens ?? {});
2756
2773
  if (tokenEntries.length === 0) {
@@ -2762,7 +2779,7 @@ async function whoamiCommand(options) {
2762
2779
  }
2763
2780
  const historyPath = join6(homedir5(), ".config", "mcp-s-cli", "history.jsonl");
2764
2781
  if (existsSync8(historyPath)) {
2765
- const lines = readFileSync4(historyPath, "utf-8").split("\n").filter(Boolean);
2782
+ const lines = readFileSync5(historyPath, "utf-8").split("\n").filter(Boolean);
2766
2783
  const size = statSync(historyPath).size;
2767
2784
  const sizeStr = size >= 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)} MB` : size >= 1024 ? `${(size / 1024).toFixed(1)} KB` : `${size} B`;
2768
2785
  console.log("");
@@ -3067,19 +3084,19 @@ Examples:
3067
3084
  mcp-s-cli kill-daemon # Kill the running daemon process
3068
3085
 
3069
3086
  Environment Variables:
3070
- MCP_DAEMON=0 Disable connection caching (daemon mode, on by default)
3071
- MCP_DAEMON_TIMEOUT=N Set daemon idle timeout in seconds (default: 300)
3087
+ MCP_S_CLI_DEBUG=1 Enable debug output
3088
+ MCP_S_CLI_STRICT_ENV=false Warn (instead of error) on missing \${VAR} in config
3072
3089
 
3073
3090
  Config File:
3074
3091
  The CLI looks for config.json in:
3075
- 1. Path specified by MCP_CONFIG_PATH or -c/--config
3092
+ 1. Path specified by MCP_S_CLI_CONFIG_PATH or -c/--config
3076
3093
  2. ~/.config/mcp-s-cli/config.json
3077
3094
 
3078
3095
  'mcp-s-cli init' always writes to ~/.config/mcp-s-cli/config.json
3079
3096
  `);
3080
3097
  }
3081
- function appendHistory(argv) {
3082
- if (process.env.MCP_CLI_HISTORY !== "1") return;
3098
+ function appendHistory(argv, history) {
3099
+ if (!history) return;
3083
3100
  try {
3084
3101
  const dir = join7(homedir6(), ".config", "mcp-s-cli");
3085
3102
  mkdirSync5(dir, { recursive: true });
@@ -3095,8 +3112,15 @@ function appendHistory(argv) {
3095
3112
  }
3096
3113
  }
3097
3114
  async function main() {
3098
- appendHistory(process.argv.slice(2));
3099
- const args = parseArgs2(process.argv.slice(2));
3115
+ const argv = process.argv.slice(2);
3116
+ const args = parseArgs2(argv);
3117
+ let settings;
3118
+ try {
3119
+ const loaded = await loadConfig(args.configPath);
3120
+ settings = loaded.settings;
3121
+ } catch {
3122
+ }
3123
+ appendHistory(argv, settings?.history);
3100
3124
  switch (args.command) {
3101
3125
  case "help":
3102
3126
  printHelp();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mcp-s/cli",
3
- "version": "0.0.9",
4
- "description": "A lightweight CLI for interacting with MCP (Model Context Protocol) servers",
3
+ "version": "0.0.10",
4
+ "description": "A lightweight CLI for connecting AI agents to the Webrix MCP Gateway",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {