@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.
- package/.claude/skills/mcpx.md +21 -13
- package/.cursor/rules/mcpx.mdc +125 -77
- package/README.md +40 -5
- package/package.json +1 -1
- package/src/cli.ts +9 -1
- package/src/commands/exec.ts +92 -8
- package/src/context.ts +9 -1
- package/src/output/format-output.ts +12 -2
- package/src/output/formatter.ts +200 -3
package/.claude/skills/mcpx.md
CHANGED
|
@@ -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 <
|
|
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
|
|
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 <
|
|
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
|
|
153
|
-
|
|
|
154
|
-
| `-j, --json`
|
|
155
|
-
| `-
|
|
156
|
-
| `-
|
|
157
|
-
| `-
|
|
158
|
-
| `-
|
|
159
|
-
| `-
|
|
160
|
-
| `-
|
|
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
|
|
package/.cursor/rules/mcpx.mdc
CHANGED
|
@@ -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 <
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
118
|
-
mcpx auth <server> -s
|
|
119
|
-
mcpx auth <server> -r
|
|
120
|
-
mcpx
|
|
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`
|
|
128
|
-
| `mcpx servers`
|
|
129
|
-
| `mcpx -d`
|
|
130
|
-
| `mcpx info <server>`
|
|
131
|
-
| `mcpx info <server> <tool>`
|
|
132
|
-
| `mcpx exec <server>`
|
|
133
|
-
| `mcpx exec <
|
|
134
|
-
| `mcpx exec <server> <tool>
|
|
135
|
-
| `mcpx
|
|
136
|
-
| `mcpx search
|
|
137
|
-
| `mcpx search -
|
|
138
|
-
| `mcpx search -
|
|
139
|
-
| `mcpx
|
|
140
|
-
| `mcpx index
|
|
141
|
-
| `mcpx
|
|
142
|
-
| `mcpx auth <server
|
|
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
|
|
145
|
-
| `mcpx
|
|
146
|
-
| `mcpx ping
|
|
147
|
-
| `mcpx
|
|
148
|
-
| `mcpx add <name> --
|
|
149
|
-
| `mcpx add <name> --url <url
|
|
150
|
-
| `mcpx remove <name>`
|
|
151
|
-
| `mcpx skill install --claude`
|
|
152
|
-
| `mcpx skill install --cursor`
|
|
153
|
-
| `mcpx resource`
|
|
154
|
-
| `mcpx resource <server>`
|
|
155
|
-
| `mcpx resource <server> <uri>`
|
|
156
|
-
| `mcpx prompt`
|
|
157
|
-
| `mcpx prompt <server>`
|
|
158
|
-
| `mcpx prompt <server> <name> '<json>'` | Get a specific prompt
|
|
159
|
-
| `mcpx
|
|
160
|
-
| `mcpx
|
|
161
|
-
| `mcpx
|
|
162
|
-
| `mcpx task
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
93
|
-
| `mcpx exec
|
|
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
|
-
|
|
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>'`
|
|
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 <
|
|
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
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 (
|
|
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
|
}
|
package/src/commands/exec.ts
CHANGED
|
@@ -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 <
|
|
18
|
-
.description("execute a tool (
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
}
|
package/src/output/formatter.ts
CHANGED
|
@@ -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,
|
|
350
|
-
|
|
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 */
|