@evantahler/mcpcli 0.3.4 → 0.4.0
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 +23 -23
- package/package.json +1 -1
- package/skills/mcpcli.md +11 -11
- package/src/cli.ts +2 -2
- package/src/client/http.ts +2 -1
- package/src/client/oauth.ts +5 -6
- package/src/commands/auth.ts +3 -3
- package/src/commands/{call.ts → exec.ts} +4 -4
- package/src/commands/index.ts +4 -6
- package/src/commands/info.ts +2 -2
- package/src/commands/list.ts +2 -2
- package/src/commands/search.ts +4 -7
- package/src/context.ts +3 -0
- package/src/output/logger.ts +114 -0
- package/src/search/indexer.ts +2 -1
- package/src/output/spinner.ts +0 -39
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ mcpcli info github
|
|
|
34
34
|
# Inspect a specific tool
|
|
35
35
|
mcpcli info github search_repositories
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
mcpcli
|
|
37
|
+
# Execute a tool
|
|
38
|
+
mcpcli exec github search_repositories '{"query": "mcp server"}'
|
|
39
39
|
|
|
40
40
|
# Search tools — combines keyword and semantic matching
|
|
41
41
|
mcpcli search "post a ticket to linear"
|
|
@@ -59,8 +59,8 @@ mcpcli search -q "manage pull requests"
|
|
|
59
59
|
| `mcpcli search -q <query>` | Semantic search only |
|
|
60
60
|
| `mcpcli index` | Build/rebuild the search index |
|
|
61
61
|
| `mcpcli index -i` | Show index status |
|
|
62
|
-
| `mcpcli
|
|
63
|
-
| `mcpcli
|
|
62
|
+
| `mcpcli exec <server> <tool> [json]` | Validate inputs locally, then execute tool |
|
|
63
|
+
| `mcpcli exec <server>` | List available tools for a server |
|
|
64
64
|
| `mcpcli auth <server>` | Authenticate with an HTTP MCP server (OAuth) |
|
|
65
65
|
| `mcpcli auth <server> -s` | Check auth status and token TTL |
|
|
66
66
|
| `mcpcli auth <server> -r` | Force token refresh |
|
|
@@ -193,7 +193,7 @@ Stores OAuth tokens for HTTP MCP servers. You don't edit this directly — manag
|
|
|
193
193
|
}
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
Tokens are automatically refreshed when expired (if a refresh token is available). Any command that connects to a server (`
|
|
196
|
+
Tokens are automatically refreshed when expired (if a refresh token is available). Any command that connects to a server (`exec`, `info`, `search`, listing) will refresh tokens transparently. `mcpcli auth <server> --status` shows current token state and TTL.
|
|
197
197
|
|
|
198
198
|
### `search.json` — Semantic Search Index (managed automatically)
|
|
199
199
|
|
|
@@ -308,7 +308,7 @@ The index updates incrementally — only new or changed tools are re-indexed. Th
|
|
|
308
308
|
|
|
309
309
|
```bash
|
|
310
310
|
# See full HTTP traffic
|
|
311
|
-
mcpcli -v
|
|
311
|
+
mcpcli -v exec arcade Gmail_WhoAmI
|
|
312
312
|
|
|
313
313
|
# > POST https://api.arcade.dev/mcp/evan-coding
|
|
314
314
|
# > authorization: Bearer eyJhbGci...
|
|
@@ -329,7 +329,7 @@ mcpcli -v call arcade Gmail_WhoAmI
|
|
|
329
329
|
# { "content": [ ... ] }
|
|
330
330
|
|
|
331
331
|
# Debug on stderr, clean JSON on stdout
|
|
332
|
-
mcpcli -v
|
|
332
|
+
mcpcli -v exec arcade Gmail_WhoAmI | jq .
|
|
333
333
|
|
|
334
334
|
# Show full auth tokens (unmasked)
|
|
335
335
|
mcpcli -v -S call arcade Gmail_WhoAmI
|
|
@@ -339,19 +339,19 @@ The `>` / `<` convention matches curl — `>` for request, `<` for response. The
|
|
|
339
339
|
|
|
340
340
|
## Input Validation
|
|
341
341
|
|
|
342
|
-
`mcpcli
|
|
342
|
+
`mcpcli exec` validates tool arguments locally before sending them to the server. MCP tools advertise a JSON Schema for their inputs — mcpcli uses this to catch errors fast, without a round-trip.
|
|
343
343
|
|
|
344
344
|
```bash
|
|
345
345
|
# Missing required field — caught locally
|
|
346
|
-
mcpcli
|
|
346
|
+
mcpcli exec github create_issue '{"title": "bug"}'
|
|
347
347
|
# => error: missing required field "repo" (github/create_issue)
|
|
348
348
|
|
|
349
349
|
# Wrong type — caught locally
|
|
350
|
-
mcpcli
|
|
350
|
+
mcpcli exec github create_issue '{"repo": "foo", "title": 123}'
|
|
351
351
|
# => error: "title" must be a string, got number (github/create_issue)
|
|
352
352
|
|
|
353
353
|
# Valid — sent to server
|
|
354
|
-
mcpcli
|
|
354
|
+
mcpcli exec github create_issue '{"repo": "foo", "title": "bug"}'
|
|
355
355
|
# => { ... }
|
|
356
356
|
```
|
|
357
357
|
|
|
@@ -362,7 +362,7 @@ Validation covers:
|
|
|
362
362
|
- **Enum values** — rejects values not in the allowed set
|
|
363
363
|
- **Nested objects** — validates recursively
|
|
364
364
|
|
|
365
|
-
If a tool's `inputSchema` is unavailable (some servers don't provide one),
|
|
365
|
+
If a tool's `inputSchema` is unavailable (some servers don't provide one), execution proceeds without local validation.
|
|
366
366
|
|
|
367
367
|
## Shell Output & Piping
|
|
368
368
|
|
|
@@ -379,26 +379,26 @@ mcpcli info github | jq '.tools[].name'
|
|
|
379
379
|
mcpcli info github --json
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
-
Tool
|
|
382
|
+
Tool results are always JSON, designed for chaining:
|
|
383
383
|
|
|
384
384
|
```bash
|
|
385
385
|
# Search repos and read the first result
|
|
386
|
-
mcpcli
|
|
386
|
+
mcpcli exec github search_repositories '{"query":"mcp"}' \
|
|
387
387
|
| jq -r '.content[0].text | fromjson | .items[0].full_name' \
|
|
388
|
-
| xargs -I {} mcpcli
|
|
388
|
+
| xargs -I {} mcpcli exec github get_file_contents '{"owner":"{}","path":"README.md"}'
|
|
389
389
|
|
|
390
390
|
# Conditional execution
|
|
391
|
-
mcpcli
|
|
391
|
+
mcpcli exec filesystem list_directory '{"path":"."}' \
|
|
392
392
|
| jq -e '.content[0].text | contains("package.json")' \
|
|
393
|
-
&& mcpcli
|
|
393
|
+
&& mcpcli exec filesystem read_file '{"path":"./package.json"}'
|
|
394
394
|
```
|
|
395
395
|
|
|
396
396
|
Stdin works for tool arguments:
|
|
397
397
|
|
|
398
398
|
```bash
|
|
399
|
-
echo '{"path":"./README.md"}' | mcpcli
|
|
399
|
+
echo '{"path":"./README.md"}' | mcpcli exec filesystem read_file
|
|
400
400
|
|
|
401
|
-
cat params.json | mcpcli
|
|
401
|
+
cat params.json | mcpcli exec server tool
|
|
402
402
|
```
|
|
403
403
|
|
|
404
404
|
## Agent Integration
|
|
@@ -416,7 +416,7 @@ Then in any Claude Code session, the agent can use `/mcpcli` or the skill trigge
|
|
|
416
416
|
|
|
417
417
|
1. **Search first** — `mcpcli search "<intent>"` to find relevant tools
|
|
418
418
|
2. **Inspect** — `mcpcli info <server> <tool>` to get the schema before calling
|
|
419
|
-
3. **
|
|
419
|
+
3. **Execute** — `mcpcli exec <server> <tool> '<json>'` to execute
|
|
420
420
|
|
|
421
421
|
This keeps tool schemas out of the system prompt entirely. The agent discovers what it needs on-demand, saving tokens and context window space.
|
|
422
422
|
|
|
@@ -432,10 +432,10 @@ To discover tools:
|
|
|
432
432
|
mcpcli search -k "<pattern>" # keyword/glob only
|
|
433
433
|
mcpcli info <server> <tool> # tool schema
|
|
434
434
|
|
|
435
|
-
To
|
|
436
|
-
mcpcli
|
|
435
|
+
To execute tools:
|
|
436
|
+
mcpcli exec <server> <tool> '<json args>'
|
|
437
437
|
|
|
438
|
-
Always search before
|
|
438
|
+
Always search before executing — don't assume tool names.
|
|
439
439
|
```
|
|
440
440
|
|
|
441
441
|
## Development
|
package/package.json
CHANGED
package/skills/mcpcli.md
CHANGED
|
@@ -22,19 +22,19 @@ mcpcli info <server> <tool>
|
|
|
22
22
|
|
|
23
23
|
This shows parameters, types, required fields, and the full JSON Schema.
|
|
24
24
|
|
|
25
|
-
## 3.
|
|
25
|
+
## 3. Execute the tool
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
mcpcli
|
|
28
|
+
mcpcli exec <server> <tool> '<json args>'
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Rules
|
|
32
32
|
|
|
33
|
-
- Always search before
|
|
34
|
-
- Always inspect the schema before
|
|
33
|
+
- Always search before executing — don't assume tool names exist
|
|
34
|
+
- Always inspect the schema before executing — validate you have the right arguments
|
|
35
35
|
- Use `mcpcli search -k` for exact name matching
|
|
36
36
|
- Pipe results through `jq` when you need to extract specific fields
|
|
37
|
-
- Use `-v` for verbose HTTP debugging if
|
|
37
|
+
- Use `-v` for verbose HTTP debugging if an exec fails unexpectedly
|
|
38
38
|
|
|
39
39
|
## Examples
|
|
40
40
|
|
|
@@ -46,15 +46,15 @@ mcpcli search "send a message"
|
|
|
46
46
|
mcpcli info arcade Slack_SendMessage
|
|
47
47
|
|
|
48
48
|
# Send a message
|
|
49
|
-
mcpcli
|
|
49
|
+
mcpcli exec arcade Slack_SendMessage '{"channel":"#general","message":"hello"}'
|
|
50
50
|
|
|
51
51
|
# Chain commands — search repos and read the first result
|
|
52
|
-
mcpcli
|
|
52
|
+
mcpcli exec github search_repositories '{"query":"mcp"}' \
|
|
53
53
|
| jq -r '.content[0].text | fromjson | .items[0].full_name' \
|
|
54
|
-
| xargs -I {} mcpcli
|
|
54
|
+
| xargs -I {} mcpcli exec github get_file_contents '{"owner":"{}","path":"README.md"}'
|
|
55
55
|
|
|
56
56
|
# Read args from stdin
|
|
57
|
-
echo '{"path":"./README.md"}' | mcpcli
|
|
57
|
+
echo '{"path":"./README.md"}' | mcpcli exec filesystem read_file
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Authentication
|
|
@@ -76,8 +76,8 @@ mcpcli deauth <server> # remove stored auth
|
|
|
76
76
|
| `mcpcli -d` | List with descriptions |
|
|
77
77
|
| `mcpcli info <server>` | Show tools for a server |
|
|
78
78
|
| `mcpcli info <server> <tool>` | Show tool schema |
|
|
79
|
-
| `mcpcli
|
|
80
|
-
| `mcpcli
|
|
79
|
+
| `mcpcli exec <server>` | List tools for a server |
|
|
80
|
+
| `mcpcli exec <server> <tool> '<json>'` | Execute a tool |
|
|
81
81
|
| `mcpcli search "<query>"` | Search tools (keyword + semantic) |
|
|
82
82
|
| `mcpcli search -k "<pattern>"` | Keyword/glob search only |
|
|
83
83
|
| `mcpcli search -q "<query>"` | Semantic search only |
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { program } from "commander";
|
|
|
4
4
|
import { registerListCommand } from "./commands/list.ts";
|
|
5
5
|
import { registerInfoCommand } from "./commands/info.ts";
|
|
6
6
|
import { registerSearchCommand } from "./commands/search.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { registerExecCommand } from "./commands/exec.ts";
|
|
8
8
|
import { registerAuthCommand, registerDeauthCommand } from "./commands/auth.ts";
|
|
9
9
|
import { registerIndexCommand } from "./commands/index.ts";
|
|
10
10
|
import { registerAddCommand } from "./commands/add.ts";
|
|
@@ -27,7 +27,7 @@ program
|
|
|
27
27
|
registerListCommand(program);
|
|
28
28
|
registerInfoCommand(program);
|
|
29
29
|
registerSearchCommand(program);
|
|
30
|
-
|
|
30
|
+
registerExecCommand(program);
|
|
31
31
|
registerAuthCommand(program);
|
|
32
32
|
registerDeauthCommand(program);
|
|
33
33
|
registerIndexCommand(program);
|
package/src/client/http.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
2
2
|
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
3
|
import { dim } from "ansis";
|
|
4
4
|
import type { HttpServerConfig } from "../config/schemas.ts";
|
|
5
|
+
import { logger } from "../output/logger.ts";
|
|
5
6
|
|
|
6
7
|
type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
|
|
7
8
|
|
|
@@ -51,7 +52,7 @@ function createDebugFetch(showSecrets: boolean): FetchLike {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function log(line: string) {
|
|
54
|
-
|
|
55
|
+
logger.writeRaw(line + "\n");
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function logHeaders(
|
package/src/client/oauth.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
import type { AuthFile } from "../config/schemas.ts";
|
|
14
14
|
import { saveAuth } from "../config/loader.ts";
|
|
15
15
|
import type { FormatOptions } from "../output/formatter.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { logger } from "../output/logger.ts";
|
|
17
17
|
|
|
18
18
|
export class McpOAuthProvider implements OAuthClientProvider {
|
|
19
19
|
private serverName: string;
|
|
@@ -83,10 +83,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
83
83
|
async redirectToAuthorization(url: URL): Promise<void> {
|
|
84
84
|
const urlStr = url.toString();
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
const { dim } = await import("ansis");
|
|
88
|
-
process.stderr.write(`${dim(urlStr)}\n`);
|
|
89
|
-
}
|
|
86
|
+
logger.info(urlStr);
|
|
90
87
|
|
|
91
88
|
const cmd =
|
|
92
89
|
process.platform === "darwin"
|
|
@@ -195,6 +192,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
195
192
|
});
|
|
196
193
|
|
|
197
194
|
await this.saveTokens(tokens);
|
|
195
|
+
|
|
196
|
+
logger.info(`Token refreshed for "${this.serverName}"`);
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
|
|
@@ -269,7 +268,7 @@ export async function tryOAuthIfSupported(
|
|
|
269
268
|
if (!oauthSupported) return false;
|
|
270
269
|
|
|
271
270
|
const provider = new McpOAuthProvider({ serverName, configDir, auth });
|
|
272
|
-
const spinner = startSpinner(`Authenticating with "${serverName}"…`, formatOptions);
|
|
271
|
+
const spinner = logger.startSpinner(`Authenticating with "${serverName}"…`, formatOptions);
|
|
273
272
|
try {
|
|
274
273
|
await runOAuthFlow(serverUrl, provider);
|
|
275
274
|
spinner.success(`Authenticated with "${serverName}"`);
|
package/src/commands/auth.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getContext } from "../context.ts";
|
|
|
3
3
|
import { isHttpServer } from "../config/schemas.ts";
|
|
4
4
|
import { saveAuth } from "../config/loader.ts";
|
|
5
5
|
import { McpOAuthProvider, runOAuthFlow } from "../client/oauth.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { logger } from "../output/logger.ts";
|
|
7
7
|
import { runIndex } from "./index.ts";
|
|
8
8
|
|
|
9
9
|
export function registerAuthCommand(program: Command) {
|
|
@@ -41,7 +41,7 @@ export function registerAuthCommand(program: Command) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (options.refresh) {
|
|
44
|
-
const spinner = startSpinner(`Refreshing token for "${server}"…`, formatOptions);
|
|
44
|
+
const spinner = logger.startSpinner(`Refreshing token for "${server}"…`, formatOptions);
|
|
45
45
|
try {
|
|
46
46
|
await provider.refreshIfNeeded(serverConfig.url);
|
|
47
47
|
spinner.success(`Token refreshed for "${server}"`);
|
|
@@ -53,7 +53,7 @@ export function registerAuthCommand(program: Command) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Default: full OAuth flow
|
|
56
|
-
const spinner = startSpinner(`Authenticating with "${server}"…`, formatOptions);
|
|
56
|
+
const spinner = logger.startSpinner(`Authenticating with "${server}"…`, formatOptions);
|
|
57
57
|
try {
|
|
58
58
|
await runOAuthFlow(serverConfig.url, provider);
|
|
59
59
|
spinner.success(`Authenticated with "${server}"`);
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
formatServerTools,
|
|
7
7
|
formatValidationErrors,
|
|
8
8
|
} from "../output/formatter.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { logger } from "../output/logger.ts";
|
|
10
10
|
import { validateToolInput } from "../validation/schema.ts";
|
|
11
11
|
|
|
12
|
-
export function
|
|
12
|
+
export function registerExecCommand(program: Command) {
|
|
13
13
|
program
|
|
14
|
-
.command("
|
|
14
|
+
.command("exec <server> [tool] [args]")
|
|
15
15
|
.description("execute a tool (omit tool name to list available tools)")
|
|
16
16
|
.action(async (server: string, tool: string | undefined, argsStr: string | undefined) => {
|
|
17
17
|
const { manager, formatOptions } = await getContext(program);
|
|
@@ -52,7 +52,7 @@ export function registerCallCommand(program: Command) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const spinner = startSpinner(`
|
|
55
|
+
const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
|
|
56
56
|
const result = await manager.callTool(server, tool, args);
|
|
57
57
|
spinner.stop();
|
|
58
58
|
console.log(formatCallResult(result, formatOptions));
|
package/src/commands/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { yellow } from "ansis";
|
|
3
3
|
import { getContext } from "../context.ts";
|
|
4
4
|
import { buildSearchIndex } from "../search/indexer.ts";
|
|
5
5
|
import { getStaleServers } from "../search/staleness.ts";
|
|
6
6
|
import { saveSearchIndex } from "../config/loader.ts";
|
|
7
7
|
import { formatError } from "../output/formatter.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { logger } from "../output/logger.ts";
|
|
9
9
|
|
|
10
10
|
/** Run the search index build. Reusable from other commands (e.g. add). */
|
|
11
11
|
export async function runIndex(program: Command): Promise<void> {
|
|
12
12
|
const { config, manager, formatOptions } = await getContext(program);
|
|
13
|
-
const spinner = startSpinner("Connecting to servers...", formatOptions);
|
|
13
|
+
const spinner = logger.startSpinner("Connecting to servers...", formatOptions);
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
const start = performance.now();
|
|
@@ -22,9 +22,7 @@ export async function runIndex(program: Command): Promise<void> {
|
|
|
22
22
|
await saveSearchIndex(config.configDir, index);
|
|
23
23
|
spinner.success(`Indexed ${index.tools.length} tools in ${elapsed}s`);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
process.stderr.write(dim(`Saved to ${config.configDir}/search.json\n`));
|
|
27
|
-
}
|
|
25
|
+
logger.info(`Saved to ${config.configDir}/search.json`);
|
|
28
26
|
} catch (err) {
|
|
29
27
|
spinner.error("Indexing failed");
|
|
30
28
|
console.error(formatError(String(err), formatOptions));
|
package/src/commands/info.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getContext } from "../context.ts";
|
|
3
3
|
import { formatServerTools, formatToolSchema, formatError } from "../output/formatter.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { logger } from "../output/logger.ts";
|
|
5
5
|
|
|
6
6
|
export function registerInfoCommand(program: Command) {
|
|
7
7
|
program
|
|
@@ -10,7 +10,7 @@ export function registerInfoCommand(program: Command) {
|
|
|
10
10
|
.action(async (server: string, tool: string | undefined) => {
|
|
11
11
|
const { manager, formatOptions } = await getContext(program);
|
|
12
12
|
const target = tool ? `${server}/${tool}` : server;
|
|
13
|
-
const spinner = startSpinner(`Connecting to ${target}...`, formatOptions);
|
|
13
|
+
const spinner = logger.startSpinner(`Connecting to ${target}...`, formatOptions);
|
|
14
14
|
try {
|
|
15
15
|
if (tool) {
|
|
16
16
|
const toolSchema = await manager.getToolSchema(server, tool);
|
package/src/commands/list.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getContext } from "../context.ts";
|
|
3
3
|
import { formatToolList, formatError } from "../output/formatter.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { logger } from "../output/logger.ts";
|
|
5
5
|
|
|
6
6
|
export function registerListCommand(program: Command) {
|
|
7
7
|
program.action(async () => {
|
|
8
8
|
const { manager, formatOptions } = await getContext(program);
|
|
9
|
-
const spinner = startSpinner("Connecting to servers...", formatOptions);
|
|
9
|
+
const spinner = logger.startSpinner("Connecting to servers...", formatOptions);
|
|
10
10
|
try {
|
|
11
11
|
const { tools, errors } = await manager.getAllTools();
|
|
12
12
|
spinner.stop();
|
package/src/commands/search.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { yellow } from "ansis";
|
|
3
2
|
import { getContext } from "../context.ts";
|
|
4
3
|
import { search } from "../search/index.ts";
|
|
5
4
|
import { getStaleServers } from "../search/staleness.ts";
|
|
6
5
|
import { formatError, formatSearchResults } from "../output/formatter.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { logger } from "../output/logger.ts";
|
|
8
7
|
|
|
9
8
|
export function registerSearchCommand(program: Command) {
|
|
10
9
|
program
|
|
@@ -23,14 +22,12 @@ export function registerSearchCommand(program: Command) {
|
|
|
23
22
|
|
|
24
23
|
const stale = getStaleServers(config.searchIndex, config.servers);
|
|
25
24
|
if (stale.length > 0) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`Warning: index has tools for removed servers: ${stale.join(", ")}. Run: mcpcli index\n`,
|
|
29
|
-
),
|
|
25
|
+
logger.warn(
|
|
26
|
+
`Warning: index has tools for removed servers: ${stale.join(", ")}. Run: mcpcli index`,
|
|
30
27
|
);
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
const spinner = startSpinner("Searching...", formatOptions);
|
|
30
|
+
const spinner = logger.startSpinner("Searching...", formatOptions);
|
|
34
31
|
|
|
35
32
|
try {
|
|
36
33
|
const results = await search(query, config.searchIndex, {
|
package/src/context.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
import type { FormatOptions } from "./output/formatter.ts";
|
|
6
|
+
import { logger } from "./output/logger.ts";
|
|
6
7
|
|
|
7
8
|
export interface AppContext {
|
|
8
9
|
config: Config;
|
|
@@ -46,5 +47,7 @@ export async function getContext(program: Command): Promise<AppContext> {
|
|
|
46
47
|
showSecrets,
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
logger.configure(formatOptions);
|
|
51
|
+
|
|
49
52
|
return { config, manager, formatOptions };
|
|
50
53
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createSpinner } from "nanospinner";
|
|
2
|
+
import { dim, yellow, red } from "ansis";
|
|
3
|
+
import type { FormatOptions } from "./formatter.ts";
|
|
4
|
+
|
|
5
|
+
export interface Spinner {
|
|
6
|
+
update(text: string): void;
|
|
7
|
+
success(text?: string): void;
|
|
8
|
+
error(text?: string): void;
|
|
9
|
+
stop(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Logger {
|
|
13
|
+
private static instance: Logger;
|
|
14
|
+
private activeSpinner: ReturnType<typeof createSpinner> | null = null;
|
|
15
|
+
private formatOptions: FormatOptions = {};
|
|
16
|
+
|
|
17
|
+
private constructor() {}
|
|
18
|
+
|
|
19
|
+
static getInstance(): Logger {
|
|
20
|
+
if (!Logger.instance) {
|
|
21
|
+
Logger.instance = new Logger();
|
|
22
|
+
}
|
|
23
|
+
return Logger.instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Set format options (called once during context setup) */
|
|
27
|
+
configure(options: FormatOptions): void {
|
|
28
|
+
this.formatOptions = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether interactive output is suppressed (JSON mode or non-TTY stderr) */
|
|
32
|
+
private isSilent(): boolean {
|
|
33
|
+
return !!this.formatOptions.json || !(process.stderr.isTTY ?? false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Write a line to stderr, pausing any active spinner around the write */
|
|
37
|
+
private writeStderr(msg: string): void {
|
|
38
|
+
if (this.activeSpinner) {
|
|
39
|
+
this.activeSpinner.clear();
|
|
40
|
+
process.stderr.write(msg + "\n");
|
|
41
|
+
this.activeSpinner.render();
|
|
42
|
+
} else {
|
|
43
|
+
process.stderr.write(msg + "\n");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Info-level message (dim text on stderr). Suppressed in JSON/non-TTY mode. */
|
|
48
|
+
info(msg: string): void {
|
|
49
|
+
if (this.isSilent()) return;
|
|
50
|
+
this.writeStderr(dim(msg));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Warning message (yellow text on stderr). Suppressed in JSON/non-TTY mode. */
|
|
54
|
+
warn(msg: string): void {
|
|
55
|
+
if (this.isSilent()) return;
|
|
56
|
+
this.writeStderr(yellow(msg));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Error message (red text on stderr). Always writes. */
|
|
60
|
+
error(msg: string): void {
|
|
61
|
+
this.writeStderr(red(msg));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Debug/verbose message (dim text on stderr). Only when verbose is enabled. */
|
|
65
|
+
debug(msg: string): void {
|
|
66
|
+
if (!this.formatOptions.verbose || this.isSilent()) return;
|
|
67
|
+
this.writeStderr(dim(msg));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Write a raw string to stderr. Spinner-aware but no formatting or newline added. */
|
|
71
|
+
writeRaw(msg: string): void {
|
|
72
|
+
if (this.activeSpinner) {
|
|
73
|
+
this.activeSpinner.clear();
|
|
74
|
+
process.stderr.write(msg);
|
|
75
|
+
this.activeSpinner.render();
|
|
76
|
+
} else {
|
|
77
|
+
process.stderr.write(msg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Start a spinner. Returns the Spinner interface. */
|
|
82
|
+
startSpinner(text: string, options?: FormatOptions): Spinner {
|
|
83
|
+
const opts = options ?? this.formatOptions;
|
|
84
|
+
|
|
85
|
+
// No spinner in JSON/piped mode
|
|
86
|
+
if (opts.json || !(process.stderr.isTTY ?? false)) {
|
|
87
|
+
return { update() {}, success() {}, error() {}, stop() {} };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const spinner = createSpinner(text, { stream: process.stderr }).start();
|
|
91
|
+
this.activeSpinner = spinner;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
update: (text: string) => {
|
|
95
|
+
spinner.update({ text });
|
|
96
|
+
},
|
|
97
|
+
success: (text?: string) => {
|
|
98
|
+
spinner.success({ text });
|
|
99
|
+
this.activeSpinner = null;
|
|
100
|
+
},
|
|
101
|
+
error: (text?: string) => {
|
|
102
|
+
spinner.error({ text });
|
|
103
|
+
this.activeSpinner = null;
|
|
104
|
+
},
|
|
105
|
+
stop: () => {
|
|
106
|
+
spinner.stop();
|
|
107
|
+
this.activeSpinner = null;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The singleton logger instance */
|
|
114
|
+
export const logger = Logger.getInstance();
|
package/src/search/indexer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ServerManager, ToolWithServer } from "../client/manager.ts";
|
|
2
2
|
import type { SearchIndex, IndexedTool } from "../config/schemas.ts";
|
|
3
3
|
import { generateEmbedding } from "./semantic.ts";
|
|
4
|
+
import { logger } from "../output/logger.ts";
|
|
4
5
|
|
|
5
6
|
/** Extract keywords from a tool name by splitting on separators and camelCase */
|
|
6
7
|
export function extractKeywords(name: string): string[] {
|
|
@@ -70,7 +71,7 @@ export async function buildSearchIndex(
|
|
|
70
71
|
|
|
71
72
|
if (errors.length > 0) {
|
|
72
73
|
for (const err of errors) {
|
|
73
|
-
|
|
74
|
+
logger.warn(`${err.server}: ${err.message}`);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
package/src/output/spinner.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { createSpinner } from "nanospinner";
|
|
2
|
-
import type { FormatOptions } from "./formatter.ts";
|
|
3
|
-
|
|
4
|
-
export interface Spinner {
|
|
5
|
-
update(text: string): void;
|
|
6
|
-
success(text?: string): void;
|
|
7
|
-
error(text?: string): void;
|
|
8
|
-
stop(): void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Create a spinner that only renders in interactive mode */
|
|
12
|
-
export function startSpinner(text: string, options: FormatOptions): Spinner {
|
|
13
|
-
// No spinner in JSON/piped mode
|
|
14
|
-
if (options.json || !(process.stderr.isTTY ?? false)) {
|
|
15
|
-
return {
|
|
16
|
-
update() {},
|
|
17
|
-
success() {},
|
|
18
|
-
error() {},
|
|
19
|
-
stop() {},
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const spinner = createSpinner(text, { stream: process.stderr }).start();
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
update(text: string) {
|
|
27
|
-
spinner.update({ text });
|
|
28
|
-
},
|
|
29
|
-
success(text?: string) {
|
|
30
|
-
spinner.success({ text });
|
|
31
|
-
},
|
|
32
|
-
error(text?: string) {
|
|
33
|
-
spinner.error({ text });
|
|
34
|
-
},
|
|
35
|
-
stop() {
|
|
36
|
-
spinner.stop();
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|