@evantahler/mcpx 0.15.9 → 0.16.2

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.
@@ -25,11 +25,12 @@ This shows parameters, types, required fields, and the full JSON Schema.
25
25
  ## 3. Execute the tool
26
26
 
27
27
  ```bash
28
- mcpx exec <server> <tool> '<json args>'
28
+ mcpx exec <tool> '<json args>' # server auto-resolved if unambiguous
29
+ mcpx exec <server> <tool> '<json args>' # explicit server (required if tool name exists on multiple servers)
29
30
  mcpx exec <server> <tool> -f params.json
30
31
  ```
31
32
 
32
- Output is JSON when piped. Use `--json` to force JSON output in any context — prefer this when you need to parse results programmatically.
33
+ Output is JSON by default. Use `--json` to force JSON output in any context — prefer this when you need to parse results programmatically. Use `--format text` to extract just the text content (stripping the MCP protocol wrapper), or `--format markdown` for rich terminal rendering.
33
34
 
34
35
  ## Rules
35
36
 
@@ -38,6 +39,8 @@ Output is JSON when piped. Use `--json` to force JSON output in any context —
38
39
  - Use `mcpx search -k` for exact name matching
39
40
  - Pipe results through `jq` when you need to extract specific fields
40
41
  - Use `--json` when parsing output programmatically (automatic when piped, but explicit is safer)
42
+ - Use `--format text` to extract plain text from tool results (strips MCP protocol wrapper)
43
+ - Use `--format markdown` for rich terminal-rendered output with colors and formatting
41
44
  - Use `-v` for verbose debugging (HTTP details + JSON-RPC protocol messages) if an exec fails unexpectedly
42
45
  - Use `-l debug` to see all server log messages, or `-l error` for errors only
43
46
 
@@ -50,7 +53,10 @@ mcpx search "send a message"
50
53
  # See what parameters Slack_SendMessage needs
51
54
  mcpx info arcade Slack_SendMessage
52
55
 
53
- # Send a message
56
+ # Send a message (server optional if tool name is unique)
57
+ mcpx exec Slack_SendMessage '{"channel":"#general","message":"hello"}'
58
+
59
+ # Or explicitly specify the server
54
60
  mcpx exec arcade Slack_SendMessage '{"channel":"#general","message":"hello"}'
55
61
 
56
62
  # Chain commands — search repos and read the first result
@@ -116,7 +122,8 @@ mcpx deauth <server> # remove stored auth
116
122
  | `mcpx info <server>` | Server overview (version, capabilities, tools) |
117
123
  | `mcpx info <server> <tool>` | Show tool schema |
118
124
  | `mcpx exec <server>` | List tools for a server |
119
- | `mcpx exec <server> <tool> '<json>'` | Execute a tool |
125
+ | `mcpx exec <tool> '<json>'` | Execute tool (server auto-resolved) |
126
+ | `mcpx exec <server> <tool> '<json>'` | Execute tool (explicit server) |
120
127
  | `mcpx exec <server> <tool> -f file` | Execute with args from file |
121
128
  | `mcpx search "<query>"` | Search tools (keyword + semantic) |
122
129
  | `mcpx search -k "<pattern>"` | Keyword/glob search only |
@@ -149,15 +156,16 @@ mcpx deauth <server> # remove stored auth
149
156
 
150
157
  ## Global flags
151
158
 
152
- | Flag | Purpose |
153
- | ------------------------- | -------------------------------------------------------- |
154
- | `-j, --json` | Force JSON output (default when piped) |
155
- | `-v, --verbose` | Show HTTP details and JSON-RPC protocol messages |
156
- | `-d, --with-descriptions` | Include tool descriptions in list output |
157
- | `-c, --config <path>` | Specify config file location |
158
- | `-N, --no-interactive` | Decline server elicitation requests (for scripted usage) |
159
- | `-S, --show-secrets` | Show full auth tokens in verbose output (unmasked) |
160
- | `-l, --log-level <level>` | Minimum server log level to display (default: `warning`) |
159
+ | Flag | Purpose |
160
+ | --------------------------- | -------------------------------------------------------- |
161
+ | `-j, --json` | Force JSON output (default when piped) |
162
+ | `-F, --format <format>` | Output format: `json`, `text`, or `markdown` |
163
+ | `-v, --verbose` | Show HTTP details and JSON-RPC protocol messages |
164
+ | `-d, --with-descriptions` | Include tool descriptions in list output |
165
+ | `-c, --config <path>` | Specify config file location |
166
+ | `-N, --no-interactive` | Decline server elicitation requests (for scripted usage) |
167
+ | `-S, --show-secrets` | Show full auth tokens in verbose output (unmasked) |
168
+ | `-l, --log-level <level>` | Minimum server log level to display (default: `warning`) |
161
169
 
162
170
  ## `add` options
163
171
 
@@ -25,16 +25,22 @@ This shows parameters, types, required fields, and the full JSON Schema.
25
25
  ## 3. Execute the tool
26
26
 
27
27
  ```bash
28
- mcpx exec <server> <tool> '<json args>'
28
+ mcpx exec <tool> '<json args>' # server auto-resolved if unambiguous
29
+ mcpx exec <server> <tool> '<json args>' # explicit server (required if tool name exists on multiple servers)
29
30
  mcpx exec <server> <tool> -f params.json
30
31
  ```
31
32
 
33
+ Output is JSON by default. Use `--json` to force JSON output in any context — prefer this when you need to parse results programmatically. Use `--format text` to extract just the text content (stripping the MCP protocol wrapper), or `--format markdown` for rich terminal rendering.
34
+
32
35
  ## Rules
33
36
 
34
37
  - Always search before executing — don't assume tool names exist
35
38
  - Always inspect the schema before executing — validate you have the right arguments
36
39
  - Use `mcpx search -k` for exact name matching
37
40
  - Pipe results through `jq` when you need to extract specific fields
41
+ - Use `--json` when parsing output programmatically (automatic when piped, but explicit is safer)
42
+ - Use `--format text` to extract plain text from tool results (strips MCP protocol wrapper)
43
+ - Use `--format markdown` for rich terminal-rendered output with colors and formatting
38
44
  - Use `-v` for verbose debugging (HTTP details + JSON-RPC protocol messages) if an exec fails unexpectedly
39
45
  - Use `-l debug` to see all server log messages, or `-l error` for errors only
40
46
 
@@ -47,7 +53,10 @@ mcpx search "send a message"
47
53
  # See what parameters Slack_SendMessage needs
48
54
  mcpx info arcade Slack_SendMessage
49
55
 
50
- # Send a message
56
+ # Send a message (server optional if tool name is unique)
57
+ mcpx exec Slack_SendMessage '{"channel":"#general","message":"hello"}'
58
+
59
+ # Or explicitly specify the server
51
60
  mcpx exec arcade Slack_SendMessage '{"channel":"#general","message":"hello"}'
52
61
 
53
62
  # Chain commands — search repos and read the first result
@@ -58,16 +67,22 @@ mcpx exec github search_repositories '{"query":"mcp"}' \
58
67
  # Read args from stdin
59
68
  echo '{"path":"./README.md"}' | mcpx exec filesystem read_file
60
69
 
61
- # Pipe from a file
62
- cat params.json | mcpx exec server tool
63
-
64
70
  # Read args from a file with --file flag
65
71
  mcpx exec filesystem read_file -f params.json
66
72
  ```
67
73
 
68
- ## 4. Long-running tools (Tasks)
74
+ ## Troubleshooting
75
+
76
+ - **"Not authenticated" / 401 error** → Run `mcpx auth <server>` to start the OAuth flow
77
+ - **Exec timeout** → Use `-v` to see where it stalls; set `MCP_TIMEOUT=<seconds>` to increase the timeout (default: 1800)
78
+ - **Search returns no results** → Try `mcpx search -k "*keyword*"` for glob matching, or `mcpx index` to rebuild the search index
79
+ - **Missing or stale tools** → Run `mcpx index` to rebuild; any command that connects to a server also auto-updates the index
80
+ - **Server won't connect** → Run `mcpx ping <server>` to check connectivity; use `-v` for protocol-level details
81
+ - **Auth token expired** → Run `mcpx auth <server> -r` to force a token refresh
82
+
83
+ ## Long-running tools (Tasks)
69
84
 
70
- Some tools support async execution via MCP Tasks. mcpx auto-detects this and uses task-augmented execution when available.
85
+ Some tools support async execution via MCP Tasks. mcpx auto-detects this.
71
86
 
72
87
  ```bash
73
88
  # Default: waits for the task to complete, showing progress
@@ -76,90 +91,123 @@ mcpx exec my-server long_running_tool '{"input": "data"}'
76
91
  # Return immediately with a task handle (for scripting/polling)
77
92
  mcpx exec my-server long_running_tool '{"input": "data"}' --no-wait
78
93
 
79
- # Check task status
94
+ # Check task status / retrieve result / cancel
80
95
  mcpx task get my-server <taskId>
81
-
82
- # Retrieve the result once complete
83
96
  mcpx task result my-server <taskId>
84
-
85
- # List all tasks on a server
86
- mcpx task list my-server
87
-
88
- # Cancel a running task
89
97
  mcpx task cancel my-server <taskId>
98
+ mcpx task list my-server
90
99
  ```
91
100
 
92
- For tools that don't support tasks, `exec` works exactly as before.
93
-
94
- ## 5. Elicitation (Server-Requested Input)
95
-
96
- Some servers request user input mid-operation (e.g., confirmations, auth flows). mcpx handles this automatically:
97
-
98
- ```bash
99
- # Interactive — prompts appear in the terminal
100
- mcpx exec my-server deploy_tool '{"target": "staging"}'
101
- # Server requests input: Confirm deployment
102
- # *Confirm [y/n]: y
103
-
104
- # Non-interactive — decline all elicitation (for scripts/CI)
105
- mcpx exec my-server deploy_tool '{"target": "staging"}' --no-interactive
101
+ ## Elicitation (Server-Requested Input)
106
102
 
107
- # JSON mode read/write elicitation as JSON via stdin/stdout
108
- echo '{"action":"accept","content":{"confirm":true}}' | \
109
- mcpx exec my-server deploy_tool '{"target": "staging"}' --json
110
- ```
103
+ Some servers request user input mid-operation. mcpx handles this automatically in interactive mode. Use `-N` / `--no-interactive` to decline all elicitation (for scripts/CI), or `--json` to handle elicitation programmatically via stdin/stdout.
111
104
 
112
105
  ## Authentication
113
106
 
114
- Some HTTP servers require OAuth. If you see an "Not authenticated" error:
115
-
116
107
  ```bash
117
- mcpx auth <server> # authenticate via browser
118
- mcpx auth <server> -s # check token status and TTL
119
- mcpx auth <server> -r # force token refresh
120
- mcpx deauth <server> # remove stored auth
108
+ mcpx auth <server> # authenticate via browser
109
+ mcpx auth <server> -s # check token status and TTL
110
+ mcpx auth <server> -r # force token refresh
111
+ mcpx auth <server> --no-index # authenticate without rebuilding search index
112
+ mcpx deauth <server> # remove stored auth
121
113
  ```
122
114
 
123
115
  ## Available commands
124
116
 
125
117
  | Command | Purpose |
126
118
  | -------------------------------------- | --------------------------------- |
127
- | `mcpx` | List all servers and tools |
128
- | `mcpx servers` | List servers (name, type, detail) |
129
- | `mcpx -d` | List with descriptions |
130
- | `mcpx info <server>` | Server overview (version, capabilities, tools) |
131
- | `mcpx info <server> <tool>` | Show tool schema |
132
- | `mcpx exec <server>` | List tools for a server |
133
- | `mcpx exec <server> <tool> '<json>'` | Execute a tool |
134
- | `mcpx exec <server> <tool> -f file` | Execute with args from file |
135
- | `mcpx search "<query>"` | Search tools (keyword + semantic) |
136
- | `mcpx search -k "<pattern>"` | Keyword/glob search only |
137
- | `mcpx search -q "<query>"` | Semantic search only |
138
- | `mcpx search -n <number> "<query>"` | Limit number of results (default: 10) |
139
- | `mcpx index` | Build/rebuild search index |
140
- | `mcpx index -i` | Show index status |
141
- | `mcpx auth <server>` | Authenticate with OAuth |
142
- | `mcpx auth <server> -s` | Check token status and TTL |
119
+ | `mcpx` | List all servers and tools |
120
+ | `mcpx servers` | List servers (name, type, detail) |
121
+ | `mcpx -d` | List with descriptions |
122
+ | `mcpx info <server>` | Server overview (version, capabilities, tools) |
123
+ | `mcpx info <server> <tool>` | Show tool schema |
124
+ | `mcpx exec <server>` | List tools for a server |
125
+ | `mcpx exec <tool> '<json>'` | Execute tool (server auto-resolved) |
126
+ | `mcpx exec <server> <tool> '<json>'` | Execute tool (explicit server) |
127
+ | `mcpx exec <server> <tool> -f file` | Execute with args from file |
128
+ | `mcpx search "<query>"` | Search tools (keyword + semantic) |
129
+ | `mcpx search -k "<pattern>"` | Keyword/glob search only |
130
+ | `mcpx search -q "<query>"` | Semantic search only |
131
+ | `mcpx search -n <number> "<query>"` | Limit number of results (default: 10) |
132
+ | `mcpx index` | Build/rebuild search index |
133
+ | `mcpx index -i` | Show index status |
134
+ | `mcpx auth <server>` | Authenticate with OAuth |
135
+ | `mcpx auth <server> -s` | Check token status and TTL |
143
136
  | `mcpx auth <server> -r` | Force token refresh |
144
- | `mcpx deauth <server>` | Remove stored authentication |
145
- | `mcpx ping` | Check connectivity to all servers |
146
- | `mcpx ping <server> [server2...]` | Check specific server(s) |
147
- | `mcpx add <name> --command <cmd>` | Add a stdio MCP server |
148
- | `mcpx add <name> --url <url>` | Add an HTTP MCP server |
149
- | `mcpx add <name> --url <url> --transport sse` | Add a legacy SSE server |
150
- | `mcpx remove <name>` | Remove an MCP server |
151
- | `mcpx skill install --claude` | Install mcpx skill for Claude |
152
- | `mcpx skill install --cursor` | Install mcpx rule for Cursor |
153
- | `mcpx resource` | List all resources across servers |
154
- | `mcpx resource <server>` | List resources for a server |
155
- | `mcpx resource <server> <uri>` | Read a specific resource |
156
- | `mcpx prompt` | List all prompts across servers |
157
- | `mcpx prompt <server>` | List prompts for a server |
158
- | `mcpx prompt <server> <name> '<json>'` | Get a specific prompt |
159
- | `mcpx exec <server> <tool> --no-wait` | Execute as async task, return handle |
160
- | `mcpx exec <server> <tool> --ttl <ms>` | Set task TTL (default: 60000) |
161
- | `mcpx -N exec <server> <tool> ...` | Decline elicitation (non-interactive) |
162
- | `mcpx task list <server>` | List tasks on a server |
163
- | `mcpx task get <server> <taskId>` | Get task status |
164
- | `mcpx task result <server> <taskId>` | Retrieve completed task result |
165
- | `mcpx task cancel <server> <taskId>` | Cancel a running task |
137
+ | `mcpx auth <server> --no-index` | Authenticate without rebuilding index |
138
+ | `mcpx deauth <server>` | Remove stored authentication |
139
+ | `mcpx ping` | Check connectivity to all servers |
140
+ | `mcpx ping <server> [server2...]` | Check specific server(s) |
141
+ | `mcpx add <name> --command <cmd>` | Add a stdio MCP server |
142
+ | `mcpx add <name> --url <url>` | Add an HTTP MCP server |
143
+ | `mcpx remove <name>` | Remove an MCP server |
144
+ | `mcpx skill install --claude` | Install mcpx skill for Claude |
145
+ | `mcpx skill install --cursor` | Install mcpx rule for Cursor |
146
+ | `mcpx resource` | List all resources across servers |
147
+ | `mcpx resource <server>` | List resources for a server |
148
+ | `mcpx resource <server> <uri>` | Read a specific resource |
149
+ | `mcpx prompt` | List all prompts across servers |
150
+ | `mcpx prompt <server>` | List prompts for a server |
151
+ | `mcpx prompt <server> <name> '<json>'` | Get a specific prompt |
152
+ | `mcpx task list <server>` | List tasks on a server |
153
+ | `mcpx task get <server> <taskId>` | Get task status |
154
+ | `mcpx task result <server> <taskId>` | Retrieve completed task result |
155
+ | `mcpx task cancel <server> <taskId>` | Cancel a running task |
156
+
157
+ ## Global flags
158
+
159
+ | Flag | Purpose |
160
+ | --------------------------- | -------------------------------------------------------- |
161
+ | `-j, --json` | Force JSON output (default when piped) |
162
+ | `-F, --format <format>` | Output format: `json`, `text`, or `markdown` |
163
+ | `-v, --verbose` | Show HTTP details and JSON-RPC protocol messages |
164
+ | `-d, --with-descriptions` | Include tool descriptions in list output |
165
+ | `-c, --config <path>` | Specify config file location |
166
+ | `-N, --no-interactive` | Decline server elicitation requests (for scripted usage) |
167
+ | `-S, --show-secrets` | Show full auth tokens in verbose output (unmasked) |
168
+ | `-l, --log-level <level>` | Minimum server log level to display (default: `warning`) |
169
+
170
+ ## `add` options
171
+
172
+ | Flag | Purpose |
173
+ | -------------------------- | -------------------------------------- |
174
+ | `--command <cmd>` | Command to run (stdio server) |
175
+ | `--args <a1,a2,...>` | Comma-separated arguments |
176
+ | `--env <KEY=VAL,...>` | Comma-separated environment variables |
177
+ | `--cwd <dir>` | Working directory for the command |
178
+ | `--url <url>` | Server URL (HTTP server) |
179
+ | `--header <Key:Value>` | HTTP header (repeatable) |
180
+ | `--transport <type>` | Transport: `sse` or `streamable-http` |
181
+ | `--allowed-tools <t1,t2>` | Comma-separated allowed tool patterns |
182
+ | `--disabled-tools <t1,t2>` | Comma-separated disabled tool patterns |
183
+ | `-f, --force` | Overwrite if server already exists |
184
+ | `--no-auth` | Skip automatic OAuth after adding |
185
+ | `--no-index` | Skip rebuilding the search index |
186
+
187
+ ## `remove` options
188
+
189
+ | Flag | Purpose |
190
+ | ------------- | ------------------------------------------------ |
191
+ | `--keep-auth` | Don't remove stored auth credentials |
192
+ | `--dry-run` | Show what would be removed without changing files |
193
+
194
+ ## `skill install` options
195
+
196
+ | Flag | Purpose |
197
+ | ----------- | ------------------------------------------ |
198
+ | `--claude` | Install skill for Claude Code |
199
+ | `--cursor` | Install rule for Cursor |
200
+ | `--global` | Install to global location (`~/`) |
201
+ | `--project` | Install to project location (default) |
202
+ | `-f, --force` | Overwrite if file already exists |
203
+
204
+ ## Environment variables
205
+
206
+ | Variable | Purpose | Default |
207
+ | ----------------- | --------------------------- | ---------- |
208
+ | `MCP_CONFIG_PATH` | Config directory path | `~/.mcpx/` |
209
+ | `MCP_TIMEOUT` | Request timeout (seconds) | `1800` |
210
+ | `MCP_CONCURRENCY` | Parallel server connections | `5` |
211
+ | `MCP_MAX_RETRIES` | Retry attempts | `3` |
212
+ | `MCP_STRICT_ENV` | Error on missing `${VAR}` | `true` |
213
+ | `MCP_DEBUG` | Enable debug output | `false` |
package/README.md CHANGED
@@ -42,6 +42,9 @@ mcpx info github search_repositories
42
42
  # Execute a tool
43
43
  mcpx exec github search_repositories '{"query": "mcp server"}'
44
44
 
45
+ # Execute a tool without specifying the server (auto-resolved)
46
+ mcpx exec search_repositories '{"query": "mcp server"}'
47
+
45
48
  # Search tools — combines keyword and semantic matching
46
49
  mcpx search "post a ticket to linear"
47
50
 
@@ -70,6 +73,7 @@ mcpx search -n 5 "manage pull requests"
70
73
  | `mcpx index` | Build/rebuild the search index |
71
74
  | `mcpx index -i` | Show index status |
72
75
  | `mcpx exec <server> <tool> [json]` | Validate inputs locally, then execute tool |
76
+ | `mcpx exec <tool> [json]` | Execute tool (server auto-resolved if unambiguous) |
73
77
  | `mcpx exec <server> <tool> -f file` | Read tool args from a JSON file |
74
78
  | `mcpx exec <server>` | List available tools for a server |
75
79
  | `mcpx auth <server>` | Authenticate with an HTTP MCP server (OAuth) |
@@ -89,8 +93,8 @@ mcpx search -n 5 "manage pull requests"
89
93
  | `mcpx prompt` | List all prompts across all servers |
90
94
  | `mcpx prompt <server>` | List prompts for a server |
91
95
  | `mcpx prompt <server> <name> [json]` | Get a specific prompt |
92
- | `mcpx exec <server> <tool> --no-wait` | Execute as async task, return task handle immediately |
93
- | `mcpx exec <server> <tool> --ttl <ms>` | Set task TTL in milliseconds (default: 60000) |
96
+ | `mcpx exec [server] <tool> --no-wait` | Execute as async task, return task handle immediately |
97
+ | `mcpx exec [server] <tool> --ttl <ms>` | Set task TTL in milliseconds (default: 60000) |
94
98
  | `mcpx task list <server>` | List tasks on a server |
95
99
  | `mcpx task get <server> <taskId>` | Get task status |
96
100
  | `mcpx task result <server> <taskId>` | Retrieve completed task result |
@@ -107,6 +111,7 @@ mcpx search -n 5 "manage pull requests"
107
111
  | `-v, --verbose` | Show HTTP details and JSON-RPC protocol messages |
108
112
  | `-S, --show-secrets` | Show full auth tokens in verbose output (unmasked) |
109
113
  | `-j, --json` | Force JSON output (default when piped) |
114
+ | `-F, --format <format>` | Output format: `json`, `text`, or `markdown` |
110
115
  | `-N, --no-interactive` | Decline server elicitation requests (for scripted usage) |
111
116
  | `-l, --log-level <level>` | Minimum server log level to display (default: `warning`) |
112
117
 
@@ -495,7 +500,36 @@ mcpx info github | jq '.tools[].name'
495
500
  mcpx info github --json
496
501
  ```
497
502
 
498
- Tool results are always JSON, designed for chaining:
503
+ ### Output Formats (`--format`)
504
+
505
+ Tool results (`exec`, `task result`) support three output formats via the global `--format` / `-F` flag:
506
+
507
+ | Format | Description |
508
+ | ---------- | ----------------------------------------------------------------------- |
509
+ | `json` | Full MCP protocol response as JSON (default) |
510
+ | `text` | Extract text from content blocks, strip protocol wrapper |
511
+ | `markdown` | Extract text and render with rich terminal formatting (colors, borders) |
512
+
513
+ ```bash
514
+ # Default JSON output — full MCP response with content array
515
+ mcpx exec github search_repositories '{"query":"mcp"}'
516
+
517
+ # Text — just the content, no protocol wrapper
518
+ mcpx exec github search_repositories '{"query":"mcp"}' --format text
519
+
520
+ # Markdown — rich terminal rendering with colors and formatting
521
+ mcpx exec github search_repositories '{"query":"mcp"}' -F markdown
522
+ ```
523
+
524
+ The `text` format extracts text from MCP content blocks and strips the protocol wrapper. If the text contains JSON, it's pretty-printed. Non-text content (images, resources) gets descriptive placeholders.
525
+
526
+ The `markdown` format extracts text the same way, then renders it through Bun's built-in markdown parser with ANSI styling — headings, bold/italic, code blocks with borders, colored links, and bullet lists.
527
+
528
+ For other commands (`list`, `info`, `search`), `--format json` forces JSON output and `--format text`/`--format markdown` use the existing human-friendly formatting.
529
+
530
+ ### Chaining tool results
531
+
532
+ Tool results are JSON by default, designed for chaining:
499
533
 
500
534
  ```bash
501
535
  # Search repos and read the first result
@@ -549,7 +583,7 @@ Then in any Claude Code session, the agent can use `/mcpx` or the skill triggers
549
583
 
550
584
  1. **Search first** — `mcpx search "<intent>"` to find relevant tools
551
585
  2. **Inspect** — `mcpx info <server> <tool>` to get the schema before calling
552
- 3. **Execute** — `mcpx exec <server> <tool> '<json>'` to execute
586
+ 3. **Execute** — `mcpx exec <tool> '<json>'` to execute (or `mcpx exec <server> <tool> '<json>'` if the tool name is ambiguous)
553
587
 
554
588
  This keeps tool schemas out of the system prompt entirely. The agent discovers what it needs on-demand, saving tokens and context window space.
555
589
 
@@ -584,7 +618,8 @@ To discover tools:
584
618
  mcpx info <server> <tool> # tool schema
585
619
 
586
620
  To execute tools:
587
- mcpx exec <server> <tool> '<json args>'
621
+ mcpx exec <tool> '<json args>' # server auto-resolved
622
+ mcpx exec <server> <tool> '<json args>' # explicit server
588
623
  mcpx exec <server> <tool> -f params.json
589
624
 
590
625
  Always search before executing — don't assume tool names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evantahler/mcpx",
3
- "version": "0.15.9",
3
+ "version": "0.16.2",
4
4
  "description": "A command-line interface for MCP servers. curl for MCP.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ program
25
25
  .option("-c, --config <path>", "config directory path")
26
26
  .option("-d, --with-descriptions", "include tool descriptions in output")
27
27
  .option("-j, --json", "force JSON output")
28
+ .option("-F, --format <format>", "output format (json, text, markdown)")
28
29
  .option("-v, --verbose", "show HTTP details and JSON-RPC protocol messages")
29
30
  .option("-S, --show-secrets", "show full auth tokens in verbose output")
30
31
  .option("-N, --no-interactive", "decline server elicitation requests")
@@ -56,7 +57,14 @@ const cliArgs = process.argv.slice(2);
56
57
  let firstCommand: string | undefined;
57
58
  for (let i = 0; i < cliArgs.length; i++) {
58
59
  const a = cliArgs[i];
59
- if (a === "-c" || a === "--config" || a === "-l" || a === "--log-level") {
60
+ if (
61
+ a === "-c" ||
62
+ a === "--config" ||
63
+ a === "-l" ||
64
+ a === "--log-level" ||
65
+ a === "-F" ||
66
+ a === "--format"
67
+ ) {
60
68
  i++; // skip the option's value argument
61
69
  continue;
62
70
  }
@@ -11,27 +11,108 @@ import { logger } from "../output/logger.ts";
11
11
  import { validateToolInput } from "../validation/schema.ts";
12
12
  import { parseJsonArgs, readStdin } from "../lib/input.ts";
13
13
  import { DEFAULTS } from "../constants.ts";
14
+ import type { ServerManager } from "../client/manager.ts";
15
+
16
+ type ResolvedArgs =
17
+ | { mode: "list-tools"; server: string }
18
+ | { mode: "call-tool"; server: string; tool: string; argsStr: string | undefined };
19
+
20
+ /**
21
+ * Resolve the positional args into either list-tools or call-tool mode.
22
+ * Supports both `exec <server> <tool> [args]` and `exec <tool> [args]`.
23
+ */
24
+ async function resolveExecArgs(
25
+ manager: ServerManager,
26
+ first: string,
27
+ second: string | undefined,
28
+ third: string | undefined,
29
+ ): Promise<ResolvedArgs> {
30
+ const serverNames = manager.getServerNames();
31
+ const isServer = serverNames.includes(first);
32
+
33
+ if (isServer) {
34
+ // Traditional form: exec <server> [tool] [args]
35
+ if (!second) {
36
+ return { mode: "list-tools", server: first };
37
+ }
38
+
39
+ // Validate the tool exists on the specified server
40
+ const serverTools = await manager.listTools(first);
41
+ const toolExists = serverTools.some((t) => t.name === second);
42
+
43
+ if (!toolExists) {
44
+ const { tools } = await manager.getAllTools();
45
+ const matches = tools.filter((t) => t.tool.name === second);
46
+
47
+ if (matches.length === 1) {
48
+ throw new Error(
49
+ `Tool "${second}" not found on server "${first}". Did you mean:\n mcpx exec ${matches[0]!.server} ${second}`,
50
+ );
51
+ } else if (matches.length > 1) {
52
+ const servers = matches.map((m) => m.server).join(", ");
53
+ throw new Error(
54
+ `Tool "${second}" not found on server "${first}". Found on: ${servers}\nUsage: mcpx exec <server> ${second} [args]`,
55
+ );
56
+ } else {
57
+ throw new Error(
58
+ `Tool "${second}" not found on server "${first}". Run "mcpx search ${second}" to find similar tools.`,
59
+ );
60
+ }
61
+ }
62
+
63
+ return { mode: "call-tool", server: first, tool: second, argsStr: third };
64
+ }
65
+
66
+ // Not a server name — treat first as a tool name
67
+ const toolName = first;
68
+ const { tools } = await manager.getAllTools();
69
+ const matches = tools.filter((t) => t.tool.name === toolName);
70
+
71
+ if (matches.length === 0) {
72
+ throw new Error(
73
+ `Unknown server or tool "${first}". Run "mcpx search ${first}" to find similar tools.`,
74
+ );
75
+ }
76
+
77
+ if (matches.length > 1) {
78
+ const servers = matches.map((m) => m.server).join(", ");
79
+ throw new Error(
80
+ `Ambiguous tool "${toolName}" — found on multiple servers: ${servers}\nSpecify the server: mcpx exec <server> ${toolName} [args]`,
81
+ );
82
+ }
83
+
84
+ return { mode: "call-tool", server: matches[0]!.server, tool: toolName, argsStr: second };
85
+ }
14
86
 
15
87
  export function registerExecCommand(program: Command) {
16
88
  program
17
- .command("exec <server> [tool] [args]")
18
- .description("execute a tool (omit tool name to list available tools)")
89
+ .command("exec <first> [second] [third]")
90
+ .description("execute a tool (server is optional if tool name is unambiguous)")
19
91
  .option("-f, --file <path>", "read JSON args from a file")
20
92
  .option("--no-wait", "return task handle immediately without waiting for completion")
21
93
  .option("--ttl <ms>", "task TTL in milliseconds", String(DEFAULTS.TASK_TTL_MS))
22
94
  .action(
23
95
  async (
24
- server: string,
25
- tool: string | undefined,
26
- argsStr: string | undefined,
96
+ first: string,
97
+ second: string | undefined,
98
+ third: string | undefined,
27
99
  options: { file?: string; wait: boolean; ttl: string },
28
100
  ) => {
29
101
  const { manager, formatOptions } = await getContext(program);
30
102
 
31
- if (!tool) {
103
+ let resolved: ResolvedArgs;
104
+ try {
105
+ resolved = await resolveExecArgs(manager, first, second, third);
106
+ } catch (err) {
107
+ console.error(formatError(String(err), formatOptions));
108
+ await manager.close();
109
+ process.exit(1);
110
+ }
111
+
112
+ if (resolved.mode === "list-tools") {
32
113
  try {
33
- const tools = await manager.listTools(server);
34
- console.log(formatServerTools(server, tools, formatOptions));
114
+ const tools = await manager.listTools(resolved.server);
115
+ console.log(formatServerTools(resolved.server, tools, formatOptions));
35
116
  } catch (err) {
36
117
  console.error(formatError(String(err), formatOptions));
37
118
  process.exit(1);
@@ -40,6 +121,9 @@ export function registerExecCommand(program: Command) {
40
121
  }
41
122
  return;
42
123
  }
124
+
125
+ const { server, tool, argsStr } = resolved;
126
+
43
127
  try {
44
128
  // Error if both --file and positional arg provided
45
129
  if (options.file && argsStr) {
package/src/context.ts CHANGED
@@ -2,7 +2,7 @@ import type { Command } from "commander";
2
2
  import { loadConfig, type LoadConfigOptions } from "./config/loader.ts";
3
3
  import { ServerManager } from "./client/manager.ts";
4
4
  import type { Config } from "./config/schemas.ts";
5
- import type { FormatOptions } from "./output/formatter.ts";
5
+ import { type FormatOptions, type OutputFormat, VALID_FORMATS } from "./output/formatter.ts";
6
6
  import { logger } from "./output/logger.ts";
7
7
  import { ENV, DEFAULTS } from "./constants.ts";
8
8
 
@@ -35,6 +35,13 @@ export async function getContext(program: Command): Promise<AppContext> {
35
35
  // Commander's --no-interactive sets opts.interactive = false (default true)
36
36
  const noInteractive = opts.interactive === false;
37
37
 
38
+ const formatFlag = opts.format as string | undefined;
39
+ if (formatFlag && !VALID_FORMATS.includes(formatFlag as OutputFormat)) {
40
+ console.error(`error: Invalid format "${formatFlag}". Use: ${VALID_FORMATS.join(", ")}`);
41
+ process.exit(1);
42
+ }
43
+ const format = formatFlag as OutputFormat | undefined;
44
+
38
45
  const manager = new ServerManager({
39
46
  servers: config.servers,
40
47
  configDir: config.configDir,
@@ -55,6 +62,7 @@ export async function getContext(program: Command): Promise<AppContext> {
55
62
  verbose,
56
63
  showSecrets,
57
64
  logLevel,
65
+ format,
58
66
  };
59
67
 
60
68
  logger.configure(formatOptions);
@@ -3,14 +3,24 @@ import { isInteractive } from "./formatter.ts";
3
3
 
4
4
  /**
5
5
  * Format output with automatic JSON/interactive branching.
6
- * In non-interactive mode, returns JSON.stringify of jsonData.
7
- * In interactive mode, calls interactiveFn() for formatted output.
6
+ * When --format is explicitly set, it takes precedence:
7
+ * json JSON.stringify of jsonData
8
+ * text or markdown → interactiveFn() (already well-formatted for non-exec commands)
9
+ * Otherwise falls back to the existing auto-detection:
10
+ * non-interactive → JSON, interactive → formatted text.
8
11
  */
9
12
  export function formatOutput(
10
13
  jsonData: unknown,
11
14
  interactiveFn: () => string,
12
15
  options: FormatOptions,
13
16
  ): string {
17
+ if (options.format) {
18
+ if (options.format === "json") {
19
+ return JSON.stringify(jsonData, null, 2);
20
+ }
21
+ // text and markdown use the interactive formatter for non-exec commands
22
+ return interactiveFn();
23
+ }
14
24
  if (!isInteractive(options)) {
15
25
  return JSON.stringify(jsonData, null, 2);
16
26
  }
@@ -6,12 +6,17 @@ import type { SearchResult } from "../search/index.ts";
6
6
  import { formatOutput } from "./format-output.ts";
7
7
  import { formatTable } from "./format-table.ts";
8
8
 
9
+ export const VALID_FORMATS = ["json", "text", "markdown"] as const;
10
+
11
+ export type OutputFormat = (typeof VALID_FORMATS)[number];
12
+
9
13
  export interface FormatOptions {
10
14
  json?: boolean;
11
15
  withDescriptions?: boolean;
12
16
  verbose?: boolean;
13
17
  showSecrets?: boolean;
14
18
  logLevel?: string;
19
+ format?: OutputFormat;
15
20
  }
16
21
 
17
22
  export interface UnifiedItem {
@@ -345,9 +350,201 @@ function exampleValue(name: string, prop: Record<string, unknown>): unknown {
345
350
  }
346
351
  }
347
352
 
348
- /** Format a tool call result */
349
- export function formatCallResult(result: unknown, _options: FormatOptions): string {
350
- return JSON.stringify(parseNestedJson(result), null, 2);
353
+ /** Format a tool call result, dispatching on the --format option */
354
+ export function formatCallResult(result: unknown, options: FormatOptions): string {
355
+ const format = options.format ?? "json";
356
+
357
+ switch (format) {
358
+ case "text":
359
+ return formatCallResultAsText(result);
360
+ case "markdown":
361
+ return formatCallResultAsMarkdown(result);
362
+ case "json":
363
+ default:
364
+ return JSON.stringify(parseNestedJson(result), null, 2);
365
+ }
366
+ }
367
+
368
+ /** Extract human-readable text from an MCP tool call result */
369
+ function formatCallResultAsText(result: unknown): string {
370
+ const r = result as {
371
+ content?: Array<{
372
+ type: string;
373
+ text?: string;
374
+ data?: string;
375
+ mimeType?: string;
376
+ uri?: string;
377
+ }>;
378
+ isError?: boolean;
379
+ };
380
+
381
+ if (!r.content || !Array.isArray(r.content) || r.content.length === 0) {
382
+ return JSON.stringify(result, null, 2);
383
+ }
384
+
385
+ const parts: string[] = [];
386
+
387
+ for (const block of r.content) {
388
+ switch (block.type) {
389
+ case "text":
390
+ if (block.text !== undefined) {
391
+ try {
392
+ const parsed = JSON.parse(block.text);
393
+ parts.push(JSON.stringify(parsed, null, 2));
394
+ } catch {
395
+ parts.push(block.text);
396
+ }
397
+ }
398
+ break;
399
+ case "image":
400
+ parts.push(
401
+ `[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
402
+ );
403
+ break;
404
+ case "resource":
405
+ parts.push(`[resource: ${block.uri ?? "unknown"}]`);
406
+ break;
407
+ default:
408
+ parts.push(`[${block.type}]`);
409
+ break;
410
+ }
411
+ }
412
+
413
+ let output = parts.join("\n");
414
+ if (r.isError) {
415
+ output = `error: ${output}`;
416
+ }
417
+ return output;
418
+ }
419
+
420
+ /** Render an MCP tool call result as styled markdown for terminal output */
421
+ function formatCallResultAsMarkdown(result: unknown): string {
422
+ const r = result as {
423
+ content?: Array<{
424
+ type: string;
425
+ text?: string;
426
+ data?: string;
427
+ mimeType?: string;
428
+ uri?: string;
429
+ }>;
430
+ isError?: boolean;
431
+ };
432
+
433
+ if (!r.content || !Array.isArray(r.content) || r.content.length === 0) {
434
+ return renderMarkdownToAnsi(jsonToMarkdown(result));
435
+ }
436
+
437
+ const parts: string[] = [];
438
+
439
+ for (const block of r.content) {
440
+ switch (block.type) {
441
+ case "text":
442
+ if (block.text !== undefined) {
443
+ try {
444
+ const parsed = JSON.parse(block.text);
445
+ parts.push(jsonToMarkdown(parsed));
446
+ } catch {
447
+ // Plain text / already markdown — pass through as-is
448
+ parts.push(block.text);
449
+ }
450
+ }
451
+ break;
452
+ case "image":
453
+ parts.push(
454
+ `[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
455
+ );
456
+ break;
457
+ case "resource":
458
+ parts.push(`[resource: ${block.uri ?? "unknown"}]`);
459
+ break;
460
+ default:
461
+ parts.push(`[${block.type}]`);
462
+ break;
463
+ }
464
+ }
465
+
466
+ let output = parts.join("\n\n");
467
+ if (r.isError) {
468
+ output = `**error:** ${output}`;
469
+ }
470
+ return renderMarkdownToAnsi(output);
471
+ }
472
+
473
+ /** Convert a key name like "display_name" to "Display Name" */
474
+ function humanizeKey(key: string): string {
475
+ return key
476
+ .replace(/[_-]/g, " ")
477
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
478
+ .replace(/\b\w/g, (c) => c.toUpperCase());
479
+ }
480
+
481
+ /** Check if a value is a plain primitive (string, number, boolean, null) */
482
+ function isPrimitive(value: unknown): value is string | number | boolean | null {
483
+ return value === null || typeof value !== "object";
484
+ }
485
+
486
+ /**
487
+ * Convert a JSON value into a readable markdown document.
488
+ * Object keys become headings at their nesting depth (depth 1 = #, depth 2 = ##, etc.).
489
+ * Arrays of primitives become bullet lists. Arrays of objects get numbered sub-sections.
490
+ * Headings are capped at depth 6 (######); deeper nesting uses **bold** labels instead.
491
+ */
492
+ export function jsonToMarkdown(value: unknown, depth: number = 1): string {
493
+ if (isPrimitive(value)) {
494
+ return String(value ?? "null");
495
+ }
496
+
497
+ if (Array.isArray(value)) {
498
+ // Array of all primitives → bullet list
499
+ if (value.every(isPrimitive)) {
500
+ return value.map((v) => `- ${String(v ?? "null")}`).join("\n");
501
+ }
502
+ // Array of objects → numbered sub-sections
503
+ return value
504
+ .map((item, i) => {
505
+ if (isPrimitive(item)) {
506
+ return `- ${String(item ?? "null")}`;
507
+ }
508
+ const label = depth <= 6 ? `${"#".repeat(depth)} ${i + 1}` : `**${i + 1}**`;
509
+ return `${label}\n\n${jsonToMarkdown(item, depth + 1)}`;
510
+ })
511
+ .join("\n\n");
512
+ }
513
+
514
+ // Object → each key becomes a heading
515
+ const entries = Object.entries(value as Record<string, unknown>);
516
+ const lines: string[] = [];
517
+
518
+ for (const [key, val] of entries) {
519
+ const heading = humanizeKey(key);
520
+
521
+ if (isPrimitive(val)) {
522
+ if (depth <= 6) {
523
+ lines.push(`${"#".repeat(depth)} ${heading}\n\n${String(val ?? "null")}`);
524
+ } else {
525
+ lines.push(`**${heading}:** ${String(val ?? "null")}`);
526
+ }
527
+ } else if (Array.isArray(val) && val.every(isPrimitive)) {
528
+ // Array of primitives: heading then bullet list
529
+ const list = val.map((v) => `- ${String(v ?? "null")}`).join("\n");
530
+ if (depth <= 6) {
531
+ lines.push(`${"#".repeat(depth)} ${heading}\n\n${list}`);
532
+ } else {
533
+ lines.push(`**${heading}:**\n${list}`);
534
+ }
535
+ } else {
536
+ // Nested object or array of objects
537
+ const label = depth <= 6 ? `${"#".repeat(depth)} ${heading}` : `**${heading}**`;
538
+ lines.push(`${label}\n\n${jsonToMarkdown(val, depth + 1)}`);
539
+ }
540
+ }
541
+
542
+ return lines.join("\n\n");
543
+ }
544
+
545
+ /** Render a markdown string to ANSI-styled terminal output using Bun's built-in renderer */
546
+ export function renderMarkdownToAnsi(input: string): string {
547
+ return Bun.markdown.ansi(input);
351
548
  }
352
549
 
353
550
  /** Recursively parse JSON strings inside MCP content blocks */