@apify/mcpc 0.2.0-beta.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ### Fixed
10
+ ## [0.2.0] - 2026-03-24
11
+
12
+ ### Added
11
13
 
12
- - Fixed auth loss when reconnecting an unauthorized session via `mcpc connect` the `unauthorized` status was not cleared, causing all subsequent operations to fail with "Authentication required by server" even after successful reconnection
13
- - HTTP proxy support (`HTTP_PROXY`/`HTTPS_PROXY` env vars) now works for MCP server connections, OAuth token refresh, and x402 payment signing — previously the MCP SDK transport and OAuth calls bypassed the global proxy dispatcher
14
+ - New `mcpc grep <pattern>` command to search tools, resources, prompts, and instructions across all active sessions, with regex, type filters, and single-session search support
15
+ - New `tasks-list`, `tasks-get`, `tasks-cancel` commands for managing async tasks on the server
16
+ - `--task` flag for `tools-call` to opt-in to task execution with progress spinner; `--detach` to start a task and return the task ID immediately; press ESC during `--task` to detach on the fly
17
+ - `--insecure` global option to skip TLS certificate verification
18
+ - `--client-id` and `--client-secret` options for `mcpc login`, for servers that don't support dynamic client registration
19
+ - `--no-profile` option for `connect` to skip OAuth profile auto-detection
20
+ - `mcpc login` now falls back to accepting a pasted callback URL when the browser cannot be opened (e.g. headless servers, containers)
21
+ - `tools-list` now shows inline parameter signatures (e.g. `read_file(path: string, +4 optional)`) for quick scanning without `--full`
22
+ - `mcpc @session` now shows available tools list from bridge cache (no extra server call)
14
23
 
15
24
  ### Changed
16
25
 
@@ -27,47 +36,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
36
 
28
37
  Direct one-shot URL access (e.g. `mcpc mcp.apify.com tools-list`) is removed; create a session first with `mcpc connect`.
29
38
 
30
- - Renamed `--async` flag to `--task` in `tools-call` for consistency with MCP specification; `--detach` now implies `--task`
31
- - Revised session states: auth failures (401/403) now show as `unauthorized` (separate from `expired` which is for session ID expiry), with actionable login guidance; new `disconnected` state surfaces when bridge is alive but server has been unreachable for >2 minutes
39
+ - Revised session states: `unauthorized` (401/403), `disconnected` (bridge alive but server unreachable >2min), and `expired` (session ID rejected), each with actionable guidance
32
40
  - When `--profile` is not specified, only the `default` profile is used; non-default profiles require an explicit `--profile` flag
33
- - `@napi-rs/keyring` native addon is now loaded lazily: `mcpc` starts and works normally even when `libsecret` (Linux) or the addon itself is missing; a one-time warning is emitted and credentials fall back to `~/.mcpc/credentials.json` (mode 0600)
34
- - `x402 sign` now takes the PAYMENT-REQUIRED header as a positional argument instead of `-r` flag (e.g. `mcpc x402 sign <base64>`)
35
- - `tools-list` and `tools-get` now show `[task:optional]`, `[task:required]`, or `[task:forbidden]` instead of `[async]`
36
- - Tools cache now fetches all pages on startup and on `tools/list_changed` notifications; `tools-get` uses cached list first and only re-fetches if the tool is not found
37
- - `--header` / `-H` option is now specific to the `connect` command instead of being shown as a global option
38
-
39
- ### Added
40
-
41
- - New `tasks-list`, `tasks-get`, `tasks-cancel` commands for managing async tasks on the server
42
- - `--task` flag for `tools-call` to opt-in to task execution with a progress spinner showing elapsed time, server status messages, and progress notifications in human mode
43
- - `--detach` flag for `tools-call` to start a task and return the task ID immediately without waiting for completion (implies `--task`)
44
- - Press ESC during `--task` execution to detach on the fly — the task continues in the background and the task ID is printed
45
- - `--insecure` global option to skip TLS certificate verification, for MCP servers with self-signed certificates
46
- - `--client-id` and `--client-secret` options for `mcpc login` command, for servers that don't support dynamic client registration
47
- - `--no-profile` option for `connect` command to skip OAuth profile auto-detection and connect anonymously
48
- - `mcpc close @session`, `mcpc restart @session`, and `mcpc shell @session` command-first syntax as alternatives to `mcpc @session close/restart/shell`
49
- - `mcpc login` now falls back to accepting a pasted callback URL when the browser cannot be opened (e.g. headless servers, containers)
50
- - `tools-list` now shows inline parameter signatures (e.g. `read_file(path: string, +4 optional)`) for quick scanning without `--full`
51
- - `mcpc @session` now shows available tools list from bridge cache (no extra server call)
52
- - Task capability and `execution.taskSupport` displayed in `tools-get` and server info
53
- - x402 payments are now also sent via the MCP `_meta["x402/payment"]` field on `tools/call` requests, in addition to the existing HTTP header
41
+ - `@napi-rs/keyring` native addon is now loaded lazily; falls back to `~/.mcpc/credentials.json` when unavailable
42
+ - `--header` / `-H` option is now specific to the `connect` command instead of being a global option
43
+ - Tools cache now fetches all pages on startup and on `tools/list_changed` notifications
54
44
 
55
45
  ### Fixed
56
46
 
57
- - Explicit `--header "Authorization: Bearer ..."` is no longer silently ignored when a default OAuth profile exists for the same server; explicit CLI headers now take precedence over auto-detected profiles
58
- - Combining `--profile` with `--header "Authorization: ..."` now returns a clear error instead of silently stripping the header
59
- - Session restart now auto-detects the `default` OAuth profile created after the session was established, fixing the `login` then `restart` flow for unauthorized sessions
60
- - Sessions requiring authentication now correctly show as `expired` instead of `live` when the server rejects unauthenticated connections
61
- - Auth errors wrapped in `NetworkError` by bridge IPC are now detected on first health check, avoiding unnecessary bridge restart
47
+ - HTTP proxy support (`HTTP_PROXY`/`HTTPS_PROXY`) now works for MCP server connections, OAuth token refresh, and x402 payment signing
48
+ - Explicit `--header "Authorization: ..."` now takes precedence over auto-detected OAuth profiles
49
+ - Fixed auth loss when reconnecting an unauthorized session via `mcpc connect`
50
+ - Session restart now auto-detects the `default` OAuth profile created after the session was established
62
51
  - `--timeout` flag now correctly propagates to MCP requests via session bridge
63
52
  - `--task` and `--detach` tool calls now correctly send task creation parameters to the server
64
- - Bridge now forwards `logging/message` notifications from the MCP server to connected clients, so `logging-set-level` actually takes effect in interactive shell sessions
53
+ - Bridge now forwards `logging/message` notifications to connected clients
65
54
  - IPC buffer between CLI and bridge process is now capped at 10 MB, preventing unbounded memory growth
66
- - `parseServerArg()` now handles Windows drive-letter config paths and other ambiguous cases
67
- - Misplaced subcommand-specific flags (e.g. `--full`, `--proxy`) now produce clear "Unknown option" errors instead of confusing rejections
68
- - `logging-set-level` JSON output no longer includes a redundant `success` field
69
- - `logTarget` no longer prints a misleading prefix when a session doesn't exist
70
- - File lock retries now use randomized exponential backoff to reduce contention
55
+ - Fixed `mcpc help <command>` showing truncated usage line
71
56
 
72
57
  ## [0.1.10] - 2026-03-01
73
58
 
@@ -184,7 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
184
169
  - Interactive shell mode
185
170
  - JSON output mode for scripting
186
171
 
187
- [Unreleased]: https://github.com/apify/mcpc/compare/v0.1.10...HEAD
172
+ [Unreleased]: https://github.com/apify/mcpc/compare/v0.2.0...HEAD
173
+ [0.2.0]: https://github.com/apify/mcpc/compare/v0.1.10...v0.2.0
188
174
  [0.1.10]: https://github.com/apify/mcpc/compare/v0.1.9...v0.1.10
189
175
  [0.1.9]: https://github.com/apify/mcpc/compare/v0.1.8...v0.1.9
190
176
  [0.1.8]: https://github.com/apify/mcpc/compare/v0.1.7...v0.1.8
package/README.md CHANGED
@@ -13,7 +13,7 @@ After all, UNIX-compatible shell script is THE most universal coding language.
13
13
 
14
14
  - 🌎 **Compatible** - Works with any MCP server over Streamable HTTP or stdio.
15
15
  - 🔄 **Persistent sessions** - Keep multiple server connections alive simultaneously.
16
- - 🔧 **Strong MCP support** - Instructions, tools, resources, prompts, dynamic discovery.
16
+ - 🔧 **Strong MCP support** - Instructions, tools, resources, prompts, async tasks, dynamic discovery.
17
17
  - 🔌 **Code mode** - JSON output enables integration with CLI tools like `jq` and scripting.
18
18
  - 🤖 **AI sandboxing** - MCP proxy server to securely access authenticated sessions from AI-generated code.
19
19
  - 🔒 **Secure** - Full OAuth 2.1 support, OS keychain for credentials storage.
@@ -129,13 +129,15 @@ Commands:
129
129
  login <server> Interactively login to a server using OAuth and save profile
130
130
  logout <server> Delete an authentication profile for a server
131
131
  clean [resources...] Clean up mcpc data (sessions, profiles, logs, all)
132
+ grep <pattern> Search tools and instructions across all active sessions
132
133
  x402 [subcommand] [args...] Configure an x402 payment wallet (EXPERIMENTAL)
133
134
  help [command] [subcommand] Show help for a specific command
134
135
 
135
136
  MCP session commands (after connecting):
136
- <@session> Show MCP server info and capabilities
137
- <@session> tools-list List MCP tools
138
- <@session> tools-get <name>
137
+ <@session> Show MCP server info, capabilities, and tools
138
+ <@session> grep <pattern> Search tools, resources, or prompts
139
+ <@session> tools-list List all server tools
140
+ <@session> tools-get <name> Get tool details and schema
139
141
  <@session> tools-call <name> [arg:=val ... | <json> | <stdin]
140
142
  <@session> prompts-list
141
143
  <@session> prompts-get <name> [arg:=val ... | <json> | <stdin]
@@ -265,6 +267,45 @@ mcpc @apify shell
265
267
  Shell commands: `help`, `exit`/`quit`/Ctrl+D, Ctrl+C to cancel.
266
268
  Arrow keys navigate history (saved to `~/.mcpc/history`).
267
269
 
270
+ ### Grep (search across sessions)
271
+
272
+ `mcpc grep` searches tools, resources, and prompts across all active sessions or within a single session:
273
+
274
+ ```bash
275
+ # Search tools and server instructions in all active sessions
276
+ mcpc grep "search"
277
+
278
+ # Search within a single session
279
+ mcpc @apify grep "actor"
280
+
281
+ # Search resources and prompts instead of the default tools and instructions
282
+ mcpc grep "config" --resources --prompts
283
+
284
+ # Regex search
285
+ mcpc grep "search|find" -E
286
+
287
+ # Case-sensitive search (default is case-insensitive)
288
+ mcpc grep "Search" --case-sensitive
289
+
290
+ # Limit results
291
+ mcpc grep "e" -m 5
292
+
293
+ # JSON output for scripting
294
+ mcpc grep "actor" --json
295
+ ```
296
+
297
+ By default, `grep` searches only tools. Use `--resources` or `--prompts` to search those types
298
+ (combine with `--tools` to include tools too). Sessions that are crashed or unavailable are shown
299
+ with their status rather than silently skipped.
300
+
301
+ The `grep` command is useful for **dynamic tool discovery**,
302
+ also called [Tool search tool](https://www.anthropic.com/engineering/advanced-tool-use) by Anthropic
303
+ or [Dynamic context discovery](https://cursor.com/blog/dynamic-context-discovery) by Cursor.
304
+ Rather than loading all tools into AI agent's context, the agent can use `grep` to discover the right tool
305
+ for the job, and only load the relevant tools into the context when needed to reduce token usage and improve accuracy.
306
+
307
+ <!-- TODO: explain this more, show diagram -->
308
+
268
309
  ### JSON mode
269
310
 
270
311
  By default, `mcpc` prints output in Markdown-ish text format with colors, making it easy to read by both humans and AIs.
@@ -805,7 +846,7 @@ The bridge process manages the full MCP session lifecycle:
805
846
  | 🔔 [**Notifications**](#list-change-notifications) | ✅ Supported |
806
847
  | 📄 [**Pagination**](#pagination) | ✅ Supported |
807
848
  | 🏓 [**Ping**](#ping) | ✅ Supported |
808
- | ⏳ **Async tasks** | 🚧 Planned |
849
+ | ⏳ [**Async tasks**](#async-tasks) | Supported |
809
850
  | 📁 **Roots** | 🚧 Planned |
810
851
  | ❓ **Elicitation** | 🚧 Planned |
811
852
  | 🔤 **Completion** | 🚧 Planned |
@@ -957,6 +998,35 @@ mcpc @apify ping
957
998
  mcpc @apify ping --json
958
999
  ```
959
1000
 
1001
+ #### Async tasks
1002
+
1003
+ MCP servers can execute tools as [async tasks](https://modelcontextprotocol.io/specification/latest/server/utilities/tasks)
1004
+ that run in the background and report progress. `mcpc` supports the full task lifecycle:
1005
+
1006
+ ```bash
1007
+ # Call a tool as a task (waits for completion, shows progress spinner)
1008
+ mcpc @apify tools-call long-running-job input:="data" --task
1009
+
1010
+ # Start a task and return immediately with the task ID
1011
+ mcpc @apify tools-call long-running-job input:="data" --detach
1012
+
1013
+ # List active tasks
1014
+ mcpc @apify tasks-list
1015
+
1016
+ # Check task status
1017
+ mcpc @apify tasks-get <taskId>
1018
+
1019
+ # Cancel a running task
1020
+ mcpc @apify tasks-cancel <taskId>
1021
+ ```
1022
+
1023
+ With `--task`, the CLI shows a progress spinner with elapsed time, server status messages,
1024
+ and progress notifications. Press **ESC** during execution to detach and get the task ID
1025
+ for later retrieval. With `--detach`, the task starts and returns the task ID immediately.
1026
+
1027
+ `tools-list` and `tools-get` show task support annotations per tool:
1028
+ `[task:optional]`, `[task:required]`, or `[task:forbidden]`.
1029
+
960
1030
  ## Configuration
961
1031
 
962
1032
  You can configure `mcpc` using a config file, environment variables, or command-line flags.
@@ -1181,22 +1251,22 @@ See [CONTRIBUTING](./CONTRIBUTING.md) for development setup, architecture overvi
1181
1251
 
1182
1252
  <!-- Stars and activity as of March 2026. -->
1183
1253
 
1184
- | Tool | Lang | Stars | Active | Tools | Resources | Prompts | Code mode | Sessions | OAuth | Stdio | HTTP | Tool search | LLM |
1185
- | ----------------------------------------------------------------------- | ------ | ----: | ------ | ----- | --------- | ------- | --------- | -------- | ----- | ----- | ---- | ----------- | --- |
1186
- | **[apify/mcpc](https://github.com/apify/mcpc)** | TS | ~350 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
1187
- | [steipete/mcporter](https://github.com/steipete/mcporter) | TS | ~2.6k | ✅ | ✅ | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
1188
- | [IBM/mcp-cli](https://github.com/IBM/mcp-cli) | Python | ~1.9k | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
1189
- | [f/mcptools](https://github.com/f/mcptools) | Go | ~1.5k | ⚠️ | ✅ | ✅ | ✅ | ✅ | | | ✅ | ✅ | | — |
1190
- | [philschmid/mcp-cli](https://github.com/philschmid/mcp-cli) | TS | ~950 | | ✅ | | — | ✅ | | — | ✅ | ✅ | | — |
1191
- | [adhikasp/mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) | Python | ~670 | ⚠️ | ✅ | | | — | | — | ✅ | | | |
1192
- | [thellimist/clihub](https://github.com/thellimist/clihub) | Go | ~590 | | ✅ | | — | — | — | ✅ | | | ✅ | — |
1193
- | [wong2/mcp-cli](https://github.com/wong2/mcp-cli) | JS | ~420 | ⚠️ | ✅ | | | — | — | ✅ | | ✅ | | — |
1194
- | [knowsuchagency/mcp2cli](https://github.com/knowsuchagency/mcp2cli) | Python | ~170 | | ✅ | ✅ | ✅ | | | | ✅ | ✅ | | — |
1195
- | [mcpshim/mcpshim](https://github.com/mcpshim/mcpshim) | Go | ~46 | ✅ | ✅ | — | — | ✅ | ✅ | ✅ | — | ✅ | ✅ | — |
1196
- | [evantahler/mcpx](https://github.com/evantahler/mcpx) | TS | ~26 | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | — |
1197
- | [EstebanForge/mcp-cli-ent](https://github.com/EstebanForge/mcp-cli-ent) | Go | ~13 | ✅ | ✅ | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — |
1198
-
1199
- **Legend:** ✅ = supported, ⚠️ = stale (no commits in 3+ months), **LLM** = requires/uses an LLM.
1254
+ | Tool | Lang | Stars | Active | Tools | Resources | Prompts | Tasks | Code mode | Sessions | OAuth | Stdio | HTTP | Tool search | LLM |
1255
+ | ----------------------------------------------------------------------- | ------ | ----: | ------ | ----- | --------- | ------- | ----- | --------- | -------- | ----- | ----- | ---- | ----------- | --- |
1256
+ | **[apify/mcpc](https://github.com/apify/mcpc)** | TS | ~400 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
1257
+ | [steipete/mcporter](https://github.com/steipete/mcporter) | TS | ~3.2k | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
1258
+ | [IBM/mcp-cli](https://github.com/IBM/mcp-cli) | Python | ~1.9k | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
1259
+ | [knowsuchagency/mcp2cli](https://github.com/knowsuchagency/mcp2cli) | Python | ~1.7k | | ✅ | ✅ | ✅ | — | ✅ | | | ✅ | ✅ | | — |
1260
+ | [f/mcptools](https://github.com/f/mcptools) | Go | ~1.5k | ⚠️ | ✅ | | ✅ | | ✅ | | — | ✅ | ✅ | | — |
1261
+ | [philschmid/mcp-cli](https://github.com/philschmid/mcp-cli) | TS | ~1.0k | | ✅ | | | — | | ✅ | — | ✅ | | | |
1262
+ | [adhikasp/mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) | Python | ~670 | ⚠️ | ✅ | | ✅ | | — | — | — | ✅ | | | ✅ |
1263
+ | [thellimist/clihub](https://github.com/thellimist/clihub) | Go | ~640 | | ✅ | | | — | — | — | ✅ | | ✅ | | — |
1264
+ | [wong2/mcp-cli](https://github.com/wong2/mcp-cli) | JS | ~430 | ⚠️ | ✅ | ✅ | ✅ | | | | ✅ | — | ✅ | | — |
1265
+ | [mcpshim/mcpshim](https://github.com/mcpshim/mcpshim) | Go | ~53 | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | — | ✅ | ✅ | — |
1266
+ | [evantahler/mcpx](https://github.com/evantahler/mcpx) | TS | ~26 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | — |
1267
+ | [EstebanForge/mcp-cli-ent](https://github.com/EstebanForge/mcp-cli-ent) | Go | ~15 | ✅ | ✅ | — | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — |
1268
+
1269
+ **Legend:** ✅ = supported, ⚠️ = stale (no commits in 3+ months), **Tasks** = [async tasks](https://modelcontextprotocol.io/specification/latest/server/utilities/tasks), **LLM** = requires/uses an LLM.
1200
1270
 
1201
1271
  **Notes:**
1202
1272
 
@@ -0,0 +1,13 @@
1
+ import type { CommandOptions } from '../../lib/types.js';
2
+ export interface GrepOptions extends CommandOptions {
3
+ tools?: boolean | undefined;
4
+ resources?: boolean | undefined;
5
+ prompts?: boolean | undefined;
6
+ instructions?: boolean | undefined;
7
+ regex?: boolean | undefined;
8
+ caseSensitive?: boolean | undefined;
9
+ maxResults?: number | undefined;
10
+ }
11
+ export declare function grepSession(session: string, pattern: string, options: GrepOptions): Promise<number>;
12
+ export declare function grepAllSessions(pattern: string, options: GrepOptions): Promise<number>;
13
+ //# sourceMappingURL=grep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/grep.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA0B,cAAc,EAAe,MAAM,oBAAoB,CAAC;AAgB9F,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAoVD,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAyBD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAmL5F"}
@@ -0,0 +1,373 @@
1
+ import chalk from 'chalk';
2
+ import { ClientError } from '../../lib/errors.js';
3
+ import { isProcessAlive } from '../../lib/utils.js';
4
+ import { consolidateSessions } from '../../lib/sessions.js';
5
+ import { withSessionClient } from '../../lib/session-client.js';
6
+ import { withMcpClient } from '../helpers.js';
7
+ import { formatJson, formatToolParamsInline, formatToolAnnotations, grayBacktick, inBackticks, } from '../output.js';
8
+ import { getBridgeStatus, formatBridgeStatus } from './sessions.js';
9
+ function buildMatcher(pattern, options) {
10
+ if (options.regex) {
11
+ let re;
12
+ try {
13
+ re = new RegExp(pattern, options.caseSensitive ? '' : 'i');
14
+ }
15
+ catch (err) {
16
+ throw new ClientError(`Invalid regex pattern: ${pattern}\n${err instanceof Error ? err.message : String(err)}`);
17
+ }
18
+ return (text) => re.test(text);
19
+ }
20
+ if (options.caseSensitive) {
21
+ return (text) => text.includes(pattern);
22
+ }
23
+ const lowerPattern = pattern.toLowerCase();
24
+ return (text) => text.toLowerCase().includes(lowerPattern);
25
+ }
26
+ function getSearchTypes(options) {
27
+ const anyFilter = options.tools || options.resources || options.prompts || options.instructions;
28
+ return {
29
+ searchTools: anyFilter ? !!options.tools : true,
30
+ searchResources: !!options.resources,
31
+ searchPrompts: !!options.prompts,
32
+ searchInstructions: anyFilter ? !!options.instructions : true,
33
+ };
34
+ }
35
+ function buildToolSearchText(tool, sessionName) {
36
+ return `${sessionName}/${tool.name} ${JSON.stringify(tool, null, 2)}`;
37
+ }
38
+ function buildResourceSearchText(resource, sessionName) {
39
+ return `${sessionName}/${resource.uri} ${JSON.stringify(resource, null, 2)}`;
40
+ }
41
+ function buildPromptSearchText(prompt, sessionName) {
42
+ return `${sessionName}/${prompt.name} ${JSON.stringify(prompt, null, 2)}`;
43
+ }
44
+ function buildInstructionsSearchText(instructions, sessionName) {
45
+ return `${sessionName} ${instructions}`;
46
+ }
47
+ async function searchClient(client, matcher, options, sessionName = '') {
48
+ const { searchTools, searchResources, searchPrompts, searchInstructions } = getSearchTypes(options);
49
+ const serverDetails = await client.getServerDetails();
50
+ const capabilities = serverDetails.capabilities;
51
+ const canListTools = searchTools && !!capabilities?.tools;
52
+ const canListResources = searchResources && !!capabilities?.resources;
53
+ const canListPrompts = searchPrompts && !!capabilities?.prompts;
54
+ const [toolsResult, resourcesResult, promptsResult] = await Promise.all([
55
+ canListTools ? client.listAllTools() : null,
56
+ canListResources ? fetchAllResources(client) : null,
57
+ canListPrompts ? fetchAllPrompts(client) : null,
58
+ ]);
59
+ const matchedTools = (toolsResult?.tools ?? []).filter((t) => matcher(buildToolSearchText(t, sessionName)));
60
+ const matchedResources = (resourcesResult ?? []).filter((r) => matcher(buildResourceSearchText(r, sessionName)));
61
+ const matchedPrompts = (promptsResult ?? []).filter((p) => matcher(buildPromptSearchText(p, sessionName)));
62
+ const instructionsText = searchInstructions ? serverDetails.instructions : undefined;
63
+ const matchedInstructions = !!instructionsText && matcher(buildInstructionsSearchText(instructionsText, sessionName));
64
+ return {
65
+ tools: matchedTools,
66
+ resources: matchedResources,
67
+ prompts: matchedPrompts,
68
+ instructions: matchedInstructions,
69
+ };
70
+ }
71
+ async function fetchAllResources(client) {
72
+ const all = [];
73
+ let cursor;
74
+ do {
75
+ const result = await client.listResources(cursor);
76
+ all.push(...result.resources);
77
+ cursor = result.nextCursor;
78
+ } while (cursor);
79
+ return all;
80
+ }
81
+ async function fetchAllPrompts(client) {
82
+ const all = [];
83
+ let cursor;
84
+ do {
85
+ const result = await client.listPrompts(cursor);
86
+ all.push(...result.prompts);
87
+ cursor = result.nextCursor;
88
+ } while (cursor);
89
+ return all;
90
+ }
91
+ function countMatches(result) {
92
+ return (result.tools.length +
93
+ result.resources.length +
94
+ result.prompts.length +
95
+ (result.instructions ? 1 : 0));
96
+ }
97
+ function countMatchesByType(result) {
98
+ return {
99
+ tools: result.tools.length,
100
+ resources: result.resources.length,
101
+ prompts: result.prompts.length,
102
+ };
103
+ }
104
+ function sumMatchesByType(results) {
105
+ return results.reduce((acc, r) => ({
106
+ tools: acc.tools + r.tools.length,
107
+ resources: acc.resources + r.resources.length,
108
+ prompts: acc.prompts + r.prompts.length,
109
+ }), { tools: 0, resources: 0, prompts: 0 });
110
+ }
111
+ function truncateResult(result, limit) {
112
+ const total = countMatches(result);
113
+ if (total <= limit)
114
+ return { result, truncated: 0 };
115
+ let remaining = limit;
116
+ const instructions = result.instructions && remaining > 0 ? true : false;
117
+ if (instructions)
118
+ remaining--;
119
+ const tools = result.tools.slice(0, remaining);
120
+ remaining -= tools.length;
121
+ const resources = result.resources.slice(0, remaining);
122
+ remaining -= resources.length;
123
+ const prompts = result.prompts.slice(0, remaining);
124
+ return {
125
+ result: { tools, resources, prompts, instructions },
126
+ truncated: total - limit,
127
+ };
128
+ }
129
+ function formatToolLine(tool) {
130
+ const bullet = chalk.dim('*');
131
+ const params = formatToolParamsInline(tool.inputSchema);
132
+ const parts = [];
133
+ const annotationsStr = formatToolAnnotations(tool.annotations);
134
+ if (annotationsStr)
135
+ parts.push(annotationsStr);
136
+ const toolAny = tool;
137
+ const execution = toolAny.execution;
138
+ const taskSupport = execution?.taskSupport;
139
+ if (taskSupport)
140
+ parts.push(`task:${taskSupport}`);
141
+ const suffix = parts.length > 0 ? ` ${chalk.gray(`[${parts.join(', ')}]`)}` : '';
142
+ return `${bullet} ${grayBacktick()}${chalk.cyan(tool.name)}${params}${grayBacktick()}${suffix}`;
143
+ }
144
+ function formatResourceLine(resource) {
145
+ const bullet = chalk.dim('*');
146
+ return `${bullet} ${inBackticks(resource.uri)}`;
147
+ }
148
+ function formatPromptLine(prompt) {
149
+ const bullet = chalk.dim('*');
150
+ return `${bullet} ${inBackticks(prompt.name)}`;
151
+ }
152
+ function formatResultSection(label, items, formatLine, indent) {
153
+ if (items.length === 0)
154
+ return [];
155
+ const lines = [];
156
+ lines.push(`${indent}${chalk.bold(`${label} (${items.length}):`)}`);
157
+ for (const item of items) {
158
+ lines.push(`${indent} ${formatLine(item)}`);
159
+ }
160
+ return lines;
161
+ }
162
+ function formatGrepResultHuman(result, indent = '') {
163
+ const lines = [];
164
+ if (result.instructions) {
165
+ lines.push(`${indent}${chalk.bold('Instructions')}`);
166
+ }
167
+ lines.push(...formatResultSection('Tools', result.tools, formatToolLine, indent));
168
+ lines.push(...formatResultSection('Resources', result.resources, formatResourceLine, indent));
169
+ lines.push(...formatResultSection('Prompts', result.prompts, formatPromptLine, indent));
170
+ return lines;
171
+ }
172
+ export async function grepSession(session, pattern, options) {
173
+ const matcher = buildMatcher(pattern, options);
174
+ const sessionRef = session.startsWith('@') ? session : `@${session}`;
175
+ return await withMcpClient(session, options, async (client) => {
176
+ const fullResult = await searchClient(client, matcher, options, sessionRef);
177
+ const total = countMatches(fullResult);
178
+ const { result, truncated } = options.maxResults != null
179
+ ? truncateResult(fullResult, options.maxResults)
180
+ : { result: fullResult, truncated: 0 };
181
+ if (options.outputMode === 'json') {
182
+ const matchCounts = countMatchesByType(fullResult);
183
+ const jsonOutput = {
184
+ sessions: [
185
+ {
186
+ name: sessionRef,
187
+ status: 'live',
188
+ instructions: result.instructions,
189
+ tools: result.tools,
190
+ resources: result.resources,
191
+ prompts: result.prompts,
192
+ },
193
+ ],
194
+ totalMatches: {
195
+ ...matchCounts,
196
+ ...(truncated > 0 ? { truncated } : {}),
197
+ },
198
+ };
199
+ console.log(formatJson(jsonOutput));
200
+ }
201
+ else {
202
+ if (total === 0) {
203
+ console.log('No matches found.');
204
+ }
205
+ else {
206
+ const lines = formatGrepResultHuman(result);
207
+ lines.push('');
208
+ const suffix = truncated > 0 ? ` (showing ${countMatches(result)})` : '';
209
+ lines.push(chalk.dim(`${total} ${total === 1 ? 'match' : 'matches'}${suffix}.`));
210
+ console.log(lines.join('\n'));
211
+ }
212
+ }
213
+ return total > 0 ? 0 : 1;
214
+ });
215
+ }
216
+ function formatSkippedSession(skipped) {
217
+ const { dot, text } = formatBridgeStatus(skipped.status);
218
+ return `${chalk.cyan(skipped.name)} ${dot} ${text}`;
219
+ }
220
+ function isSessionQueryable(session) {
221
+ return !!session.pid && isProcessAlive(session.pid);
222
+ }
223
+ export async function grepAllSessions(pattern, options) {
224
+ const matcher = buildMatcher(pattern, options);
225
+ const { sessions } = await consolidateSessions(false);
226
+ const allSessionEntries = Object.values(sessions);
227
+ if (allSessionEntries.length === 0) {
228
+ if (options.outputMode === 'json') {
229
+ console.log(formatJson({ sessions: [], totalMatches: { tools: 0, resources: 0, prompts: 0 } }));
230
+ }
231
+ else {
232
+ console.log(chalk.bold('No active sessions.'));
233
+ console.log(chalk.dim(' \u21B3 run: mcpc connect mcp.example.com @test'));
234
+ }
235
+ return 1;
236
+ }
237
+ const toSessionRef = (name) => (name.startsWith('@') ? name : `@${name}`);
238
+ const queryableSessions = [];
239
+ const skippedSessions = [];
240
+ for (const session of allSessionEntries) {
241
+ if (isSessionQueryable(session)) {
242
+ queryableSessions.push(session);
243
+ }
244
+ else {
245
+ const status = getBridgeStatus(session);
246
+ skippedSessions.push({
247
+ name: toSessionRef(session.name),
248
+ status,
249
+ });
250
+ }
251
+ }
252
+ const settled = await Promise.allSettled(queryableSessions.map(async (session) => {
253
+ const sessionName = toSessionRef(session.name);
254
+ const result = await withSessionClient(sessionName, async (client) => {
255
+ return searchClient(client, matcher, options, sessionName);
256
+ });
257
+ return {
258
+ name: sessionName,
259
+ ...result,
260
+ };
261
+ }));
262
+ const results = [];
263
+ const resultsWithMatches = [];
264
+ const errors = [];
265
+ for (const [i, outcome] of settled.entries()) {
266
+ if (outcome.status === 'fulfilled') {
267
+ results.push(outcome.value);
268
+ if (countMatches(outcome.value) > 0) {
269
+ resultsWithMatches.push(outcome.value);
270
+ }
271
+ }
272
+ else {
273
+ const reason = outcome.reason;
274
+ errors.push({
275
+ name: toSessionRef(queryableSessions[i].name),
276
+ error: reason instanceof Error ? reason.message : String(reason),
277
+ });
278
+ }
279
+ }
280
+ const totalMatches = sumMatchesByType(results);
281
+ const totalMatchCount = results.reduce((sum, r) => sum + countMatches(r), 0);
282
+ let displayResultsWithMatches = resultsWithMatches;
283
+ let totalTruncated = 0;
284
+ if (options.maxResults != null) {
285
+ let remaining = options.maxResults;
286
+ displayResultsWithMatches = [];
287
+ for (const r of resultsWithMatches) {
288
+ if (remaining <= 0) {
289
+ totalTruncated += countMatches(r);
290
+ continue;
291
+ }
292
+ const { result: truncR, truncated } = truncateResult(r, remaining);
293
+ remaining -= countMatches(truncR);
294
+ totalTruncated += truncated;
295
+ displayResultsWithMatches.push({ ...r, ...truncR });
296
+ }
297
+ }
298
+ if (options.outputMode === 'json') {
299
+ const truncatedResultsByName = new Map(displayResultsWithMatches.map((r) => [r.name, r]));
300
+ const sessionEntries = [
301
+ ...results.map((r) => {
302
+ if (options.maxResults == null) {
303
+ return {
304
+ name: r.name,
305
+ status: 'live',
306
+ instructions: r.instructions,
307
+ tools: r.tools,
308
+ resources: r.resources,
309
+ prompts: r.prompts,
310
+ };
311
+ }
312
+ const display = truncatedResultsByName.get(r.name);
313
+ return {
314
+ name: r.name,
315
+ status: 'live',
316
+ instructions: display?.instructions ?? false,
317
+ tools: display?.tools ?? [],
318
+ resources: display?.resources ?? [],
319
+ prompts: display?.prompts ?? [],
320
+ };
321
+ }),
322
+ ...errors.map((e) => ({
323
+ name: e.name,
324
+ status: 'error',
325
+ error: e.error,
326
+ })),
327
+ ...skippedSessions.map((s) => ({
328
+ name: s.name,
329
+ status: s.status,
330
+ })),
331
+ ];
332
+ const jsonOutput = {
333
+ sessions: sessionEntries,
334
+ totalMatches: {
335
+ ...totalMatches,
336
+ ...(totalTruncated > 0 ? { truncated: totalTruncated } : {}),
337
+ },
338
+ };
339
+ console.log(formatJson(jsonOutput));
340
+ }
341
+ else {
342
+ const lines = [];
343
+ for (const skipped of skippedSessions) {
344
+ lines.push(formatSkippedSession(skipped));
345
+ }
346
+ for (const r of displayResultsWithMatches) {
347
+ lines.push(chalk.cyan(r.name));
348
+ lines.push(...formatGrepResultHuman(r, ' '));
349
+ lines.push('');
350
+ }
351
+ for (const err of errors) {
352
+ lines.push(chalk.yellow(`Warning: ${err.name} \u2014 ${err.error}`));
353
+ }
354
+ if (totalMatchCount === 0 && skippedSessions.length === 0) {
355
+ console.log('No matches found.');
356
+ }
357
+ else {
358
+ if (totalMatchCount > 0) {
359
+ const sessionCount = resultsWithMatches.length;
360
+ const showing = totalMatchCount - totalTruncated;
361
+ const suffix = totalTruncated > 0 ? ` (showing ${showing})` : '';
362
+ lines.push(chalk.dim(`${totalMatchCount} ${totalMatchCount === 1 ? 'match' : 'matches'} across ${sessionCount} ${sessionCount === 1 ? 'session' : 'sessions'}${suffix}.`));
363
+ }
364
+ else if (lines.length > 0) {
365
+ lines.push('');
366
+ lines.push('No matches found.');
367
+ }
368
+ console.log(lines.join('\n'));
369
+ }
370
+ }
371
+ return totalMatchCount > 0 ? 0 : 1;
372
+ }
373
+ //# sourceMappingURL=grep.js.map