mcp-compress-router 1.0.2 → 1.1.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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # MCP Compressing Router
2
2
 
3
+ [![CI](https://github.com/ameshkov/mcp-compress-router/actions/workflows/ci.yml/badge.svg)](https://github.com/ameshkov/mcp-compress-router/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/mcp-compress-router)](https://www.npmjs.com/package/mcp-compress-router)
5
+ [![GitHub release](https://img.shields.io/github/v/release/ameshkov/mcp-compress-router)](https://github.com/ameshkov/mcp-compress-router/releases)
6
+
3
7
  <p align="center">
4
8
  Compress all connected MCP into a single router MCP and save up to 99% on
5
9
  tokens.
@@ -10,6 +14,28 @@
10
14
  alt="MCP Compress Router" width="600"/>
11
15
  </p>
12
16
 
17
+ ## Table of Contents
18
+
19
+ - [The Problem](#the-problem)
20
+ - [The Solution](#the-solution)
21
+ - [Prerequisites](#prerequisites)
22
+ - [Quick Start](#quick-start)
23
+ - [Configuration](#configuration)
24
+ - [Config File Location](#config-file-location)
25
+ - [Adding Downstream Servers](#adding-downstream-servers)
26
+ - [Per-Server Enable/Disable](#per-server-enabledisable)
27
+ - [Per-Server Tool Selection](#per-server-tool-selection)
28
+ - [Inspecting Tools](#inspecting-tools)
29
+ - [OAuth](#oauth)
30
+ - [Custom Headers](#custom-headers)
31
+ - [Secrets and Variable Expansion](#secrets-and-variable-expansion)
32
+ - [Connecting Coding Agents](#connecting-coding-agents)
33
+ - [Opencode](#opencode)
34
+ - [Claude Code](#claude-code)
35
+ - [Codex](#codex)
36
+ - [GitHub Copilot](#github-copilot)
37
+ - [How It Works](#how-it-works)
38
+
13
39
  ## The Problem
14
40
 
15
41
  When you have multiple MCPs every request to the LLM will include ALL their
@@ -49,3 +75,428 @@ an average coding session will be about **$0.032175** so we saved about
49
75
 
50
76
  This is just a basic example with just 3 MCP servers, the more MCP servers you
51
77
  have, the more you save.
78
+
79
+ ## Prerequisites
80
+
81
+ - **Node.js 24 or later** — the router runs on Node.js and is launched
82
+ via `npx`, so no separate install step is needed.
83
+ - **A coding agent that supports stdio MCP servers** — this covers
84
+ virtually every modern coding agent (opencode, Claude Code, Codex,
85
+ GitHub Copilot, Cursor, etc.). The router exposes itself as a single
86
+ stdio MCP server, so any agent that can spawn a local MCP process
87
+ works.
88
+
89
+ ## Quick Start
90
+
91
+ The router is published on npm as
92
+ [`mcp-compress-router`](https://www.npmjs.com/package/mcp-compress-router).
93
+ You do not need to install it — just run it with `npx`:
94
+
95
+ ```bash
96
+ npx mcp-compress-router add github -- npx -y @modelcontextprotocol/server-github
97
+ ```
98
+
99
+ This registers a downstream MCP server named `github` and writes it to
100
+ your [config file](#config-file-location). Repeat for every MCP server you
101
+ want to compress.
102
+
103
+ Then point your [coding agent](#connecting-coding-agents) at the router:
104
+
105
+ ```bash
106
+ npx mcp-compress-router
107
+ ```
108
+
109
+ When started without a subcommand, the router runs the MCP server over
110
+ stdio and exposes exactly two tools (`get_tool_schema`,
111
+ `invoke_tool`) to the agent.
112
+
113
+ ## Configuration
114
+
115
+ The router reads its configuration from a single JSON(C) file that lists
116
+ every downstream MCP server to compress. You can edit this file by hand
117
+ or use the `add` / `remove` / `get` / `list` CLI commands.
118
+
119
+ ### Config File Location
120
+
121
+ By default, the config file lives in a platform-specific directory
122
+ (`mcp.jsonc` is preferred over `mcp.json` when both exist):
123
+
124
+ - **Windows:** `%APPDATA%\mcp-compress-router\`
125
+ - **macOS:** `~/Library/Application Support/mcp-compress-router/`
126
+ - **Linux:** `~/.local/share/mcp-compress-router/`
127
+
128
+ You can override this with:
129
+
130
+ - The `-c, --config <path>` flag on any command, or
131
+ - The `MCP_COMPRESS_ROUTER_HOME` environment variable (points to a
132
+ directory containing the config file).
133
+
134
+ If the file does not exist when a management command runs, it is created
135
+ automatically with an empty `{ "mcpServers": {} }` body.
136
+
137
+ A `.env` file in the **same directory** is loaded automatically at
138
+ startup, so you can keep secrets out of the config (see
139
+ [Secrets and Variable Expansion](#secrets-and-variable-expansion)).
140
+
141
+ > **Note on `-c` and credential storage:** when you override the config
142
+ > path with `-c /some/dir/mcp.json`, both `credentials.json` (OAuth
143
+ > tokens) and `mcp.json` live in `/some/dir/` — i.e. next to the config
144
+ > file you specified. The `.env` file, however, is loaded from the
145
+ > [configuration directory](#config-file-location) resolved by
146
+ > `MCP_COMPRESS_ROUTER_HOME` or the platform default, *not* from beside
147
+ > the explicit `-c` path. To co-locate `.env` with a custom config, set
148
+ > `MCP_COMPRESS_ROUTER_HOME` to the same directory.
149
+
150
+ ### Adding Downstream Servers
151
+
152
+ Use the `add` command to register a downstream MCP server.
153
+
154
+ A good description helps the LLM route requests to the correct server.
155
+ When several servers are compressed behind the router, the model sees
156
+ each server's name, its description, and a list of tool names in the
157
+ `get_tool_schema` catalog. A clear description (e.g. *"GitHub API tools
158
+ for issues, PRs, and repos"*) steers the model toward the right server
159
+ far better than a bare name.
160
+
161
+ **stdio server** (a local process):
162
+
163
+ ```bash
164
+ npx mcp-compress-router add github --description "GitHub API tools" \
165
+ -- npx -y @modelcontextprotocol/server-github
166
+
167
+ # With environment variables
168
+ npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
169
+ --description "GitHub API tools" \
170
+ -- npx -y @modelcontextprotocol/server-github
171
+ ```
172
+
173
+ **HTTP server** (a remote endpoint; transport auto-detected from the
174
+ URL):
175
+
176
+ ```bash
177
+ npx mcp-compress-router add my-http https://localhost:3100/mcp
178
+
179
+ # With a custom header
180
+ npx mcp-compress-router add my-http \
181
+ --header "Authorization: Bearer mytoken" \
182
+ https://localhost:3100/mcp
183
+ ```
184
+
185
+ This produces a config file that looks like:
186
+
187
+ ```jsonc
188
+ {
189
+ "mcpServers": {
190
+ "github": {
191
+ "type": "stdio",
192
+ "command": "npx",
193
+ "args": ["-y", "@modelcontextprotocol/server-github"],
194
+ "env": { "GITHUB_PERSONAL_TOKEN": "ghp_xxx" },
195
+ "description": "GitHub API tools"
196
+ },
197
+ "my-http": {
198
+ "type": "http",
199
+ "url": "https://localhost:3100/mcp",
200
+ "headers": { "Authorization": "Bearer mytoken" }
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ Both `.json` and `.jsonc` (JSON with comments and trailing commas) are
207
+ supported. CLI commands write plain `.json`; hand-edited files may use
208
+ `.jsonc`.
209
+
210
+ Other management commands:
211
+
212
+ ```bash
213
+ npx mcp-compress-router list # list all servers + auth status
214
+ npx mcp-compress-router get my-http # show one server's config
215
+ npx mcp-compress-router remove my-http # remove a server
216
+ ```
217
+
218
+ ### Per-Server Enable/Disable
219
+
220
+ Every server entry accepts an optional `enabled` boolean. When set to
221
+ `false`, the router skips that server entirely at startup — no process
222
+ spawn, no network connection, no discovery — and it is absent from the
223
+ `get_tool_schema` catalog. All configuration is preserved so the server
224
+ can be turned back on instantly. Omitting `enabled` (the default) means
225
+ enabled, keeping `mcp.json` clean and fully backward compatible.
226
+
227
+ Toggle it from the CLI without touching the rest of the config:
228
+
229
+ ```bash
230
+ npx mcp-compress-router disable github # writes "enabled": false
231
+ npx mcp-compress-router enable github # removes the field
232
+ ```
233
+
234
+ You can also set it at creation time:
235
+
236
+ ```bash
237
+ npx mcp-compress-router add archive --disabled -- npx -y server-archive
238
+ ```
239
+
240
+ ### Per-Server Tool Selection
241
+
242
+ Two optional fields control which of a server's advertised tools are
243
+ exposed to the LLM. Both are arrays of glob patterns
244
+ ([picomatch](https://github.com/micromatch/picomatch) syntax: `*`, `?`,
245
+ `{a,b}`, `[abc]`) matched against bare tool names:
246
+
247
+ - **`allowedTools`** — when present, only matching tools are exposed.
248
+ An empty array (`[]`) exposes *no* tools (handy for staging a server
249
+ while you build the list).
250
+ - **`disabledTools`** — removes matching tools from whatever would
251
+ otherwise be exposed. The denylist wins: a tool matching both lists
252
+ is blocked.
253
+
254
+ Filtered tools are hidden from the catalog *and* hard-rejected by
255
+ `invoke_tool`, so even an LLM that guesses a filtered name cannot
256
+ reach the downstream server.
257
+
258
+ ```jsonc
259
+ "dangerous": {
260
+ "type": "stdio",
261
+ "command": "npx",
262
+ "args": ["-y", "@some/mcp-server"],
263
+ "allowedTools": ["list_issues", "get_pull_request"],
264
+ "disabledTools": ["*_delete"]
265
+ }
266
+ ```
267
+
268
+ A pattern that matches no real tool is not an error — the router logs a
269
+ warning (visible with `-v`) and continues. A malformed pattern is a
270
+ hard error at startup. Set filters at creation time with repeatable
271
+ flags:
272
+
273
+ ```bash
274
+ npx mcp-compress-router add github \
275
+ --allowed-tools list_issues \
276
+ --allowed-tools get_pull_request \
277
+ -- npx -y server-github
278
+ ```
279
+
280
+ ### Inspecting Tools
281
+
282
+ To see exactly which tools a server advertises — and which are
283
+ `[exposed]` or `[filtered]` under your current selection — connect to
284
+ it live without starting the full router:
285
+
286
+ ```bash
287
+ npx mcp-compress-router tools github
288
+ ```
289
+
290
+ This works regardless of the server's `enabled` state (inspecting a
291
+ disabled server is the primary way to build its allowlist). For HTTP
292
+ servers, stored OAuth credentials and `oauth` overrides are reused. If
293
+ the server cannot be reached or is missing required auth, the command
294
+ exits non-zero with a clear error and prints no partial list.
295
+
296
+ ### OAuth
297
+
298
+ HTTP servers that require OAuth are supported. When you `add` an HTTP
299
+ server, the router probes it for OAuth metadata and starts the login
300
+ flow automatically if OAuth is advertised. You can also trigger it
301
+ manually:
302
+
303
+ ```bash
304
+ npx mcp-compress-router login my-http
305
+ ```
306
+
307
+ This opens your browser to complete the authorization-code flow. Tokens
308
+ are stored in a separate `credentials.json` in the same directory as
309
+ `mcp.json` (with `0600` permissions on Unix), so you can safely
310
+ share or version-control `mcp.json` without exposing tokens. Add
311
+ `credentials.json` to your `.gitignore`.
312
+
313
+ By default the router uses
314
+ [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591).
315
+ If your server requires a pre-registered client, add an `oauth` block to
316
+ the server entry (in `mcp.json`):
317
+
318
+ ```jsonc
319
+ "my-http": {
320
+ "type": "http",
321
+ "url": "https://example.com/mcp",
322
+ "oauth": {
323
+ "clientId": "${MY_CLIENT_ID}",
324
+ "clientSecret": "${MY_CLIENT_SECRET}",
325
+ "scope": "read write"
326
+ }
327
+ }
328
+ ```
329
+
330
+ Only `clientId` is required; `clientSecret` and `scope` are optional.
331
+
332
+ Other OAuth commands:
333
+
334
+ ```bash
335
+ npx mcp-compress-router logout my-http # remove stored credentials
336
+ ```
337
+
338
+ For headless or CI environments, override the browser with the
339
+ `MCP_COMPRESS_ROUTER_BROWSER` environment variable. The authorization
340
+ URL is appended as a single final argument (no shell):
341
+
342
+ ```bash
343
+ MCP_COMPRESS_ROUTER_BROWSER="node /path/to/headless-browser.js" \
344
+ npx mcp-compress-router login my-http
345
+ ```
346
+
347
+ The default login timeout is 120 seconds; override it with
348
+ `MCP_COMPRESS_ROUTER_LOGIN_TIMEOUT_MS`.
349
+
350
+ ### Custom Headers
351
+
352
+ For HTTP servers that authenticate with a static API key or bearer
353
+ token instead of OAuth, use the `headers` field. You can set it via the
354
+ CLI or directly in `mcp.json`:
355
+
356
+ ```bash
357
+ npx mcp-compress-router add my-http \
358
+ --header "Authorization: Bearer mytoken" \
359
+ --header "X-Custom: value" \
360
+ https://example.com/mcp
361
+ ```
362
+
363
+ ```jsonc
364
+ "my-http": {
365
+ "type": "http",
366
+ "url": "https://example.com/mcp",
367
+ "headers": {
368
+ "Authorization": "Bearer ${MY_SERVER_TOKEN}",
369
+ "X-Custom": "value"
370
+ }
371
+ }
372
+ ```
373
+
374
+ Header values support
375
+ [variable expansion](#secrets-and-variable-expansion), so you can keep
376
+ the actual token out of the config file.
377
+
378
+ ### Secrets and Variable Expansion
379
+
380
+ Every string field in a server entry (`command`, `args`, `env`,
381
+ `headers`, `url`, `oauth.*`) is expanded against the process
382
+ environment at load time. Two syntaxes are supported:
383
+
384
+ | Syntax | Behavior |
385
+ | --- | --- |
386
+ | `${VAR}` | Replaced with the value of `VAR`. Throws if unset. |
387
+ | `${VAR:-default}` | Replaced with `VAR` when set and non-empty, otherwise `default`. |
388
+
389
+ Put your secrets in a `.env` file next to `mcp.json`:
390
+
391
+ ```bash
392
+ # <config directory>/.env
393
+ GITHUB_PERSONAL_TOKEN=ghp_abc123
394
+ MY_SERVER_TOKEN=secret-token
395
+ ```
396
+
397
+ Shell environment variables always take precedence over `.env` values.
398
+
399
+ ## Connecting Coding Agents
400
+
401
+ Once your downstream servers are configured, connect your agent to the
402
+ router the same way you would connect any other MCP server — by
403
+ pointing it at `npx mcp-compress-router`. The examples below assume the
404
+ default [config location](#config-file-location); pass `-c <path>` if
405
+ you use a custom one.
406
+
407
+ ### Opencode
408
+
409
+ Add the router to your `opencode.json` under `mcp`:
410
+
411
+ ```json
412
+ {
413
+ "$schema": "https://opencode.ai/config.json",
414
+ "mcp": {
415
+ "compress-router": {
416
+ "type": "local",
417
+ "command": ["npx", "-y", "mcp-compress-router"],
418
+ "enabled": true
419
+ }
420
+ }
421
+ }
422
+ ```
423
+
424
+ ### Claude Code
425
+
426
+ Add this to a project-level `.mcp.json` in your workspace root, or to
427
+ your user-level config (applies to every project):
428
+
429
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
430
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
431
+ - **Linux:** `~/.config/Claude/claude_desktop_config.json`
432
+
433
+ ```json
434
+ {
435
+ "mcpServers": {
436
+ "compress-router": {
437
+ "command": "npx",
438
+ "args": ["-y", "mcp-compress-router"]
439
+ }
440
+ }
441
+ }
442
+ ```
443
+
444
+ ### Codex
445
+
446
+ Add a `[mcp_servers.compress-router]` table to your Codex config (note
447
+ the snake_case key). The config path is `~/.codex/config.toml` on
448
+ macOS/Linux, or `%USERPROFILE%\.codex\config.toml` on Windows; you can
449
+ also scope it to a single project via `.codex/config.toml` in trusted
450
+ projects.
451
+
452
+ ```toml
453
+ [mcp_servers.compress-router]
454
+ command = "npx"
455
+ args = ["-y", "mcp-compress-router"]
456
+ enabled = true
457
+ ```
458
+
459
+ ### GitHub Copilot
460
+
461
+ Add this to `.vscode/mcp.json` in your workspace (project-level, applies
462
+ only to that workspace), or to your **user-level** MCP settings which
463
+ apply across every workspace: open the Command Palette →
464
+ `Preferences: Open User Settings (JSON)` and add the same `servers`
465
+ block under the `mcp` key. Project-level and user-level entries are
466
+ merged, with project-level taking precedence.
467
+
468
+ ```json
469
+ {
470
+ "servers": {
471
+ "compress-router": {
472
+ "command": "npx",
473
+ "args": ["-y", "mcp-compress-router"]
474
+ }
475
+ }
476
+ }
477
+ ```
478
+
479
+ ## How It Works
480
+
481
+ Once connected, the agent sees exactly **two tools**:
482
+
483
+ - **`get_tool_schema(server, tools)`** — Retrieves the JSON parameter
484
+ schema for one or more tools on a downstream MCP server. The tool's
485
+ description includes a compact listing of all servers and their
486
+ available tool names.
487
+ - **`invoke_tool(server, tool, arguments)`** — Forwards a tool call to
488
+ the downstream MCP server and returns the result.
489
+
490
+ The typical workflow:
491
+
492
+ 1. The agent reads the compact catalog from the `get_tool_schema`
493
+ description and identifies which tools it needs.
494
+ 2. It calls `get_tool_schema` to learn the exact parameters.
495
+ 3. It calls `invoke_tool` to execute a tool, validated against the
496
+ cached schema.
497
+
498
+ This replaces thousands of tokens of tool listings with a compact ~900
499
+ token catalog, regardless of how many downstream servers you have.
500
+
501
+ For the full configuration and environment variable reference, see
502
+ [configuration.md](docs/configuration.md).
@@ -1,22 +1,35 @@
1
1
  import { ensureConfigDir, readConfigFile, writeConfigFile, readCredentials, writeCredentials, } from './config-io.js';
2
+ import { validateGlobPattern } from '../utils/index.js';
2
3
  /**
3
- * Handles the `add <name> <commandOrUrl> [args...]` subcommand.
4
+ * Validates every glob pattern in an optional tool list, throwing with
5
+ * the field name and offending pattern on the first invalid entry.
4
6
  *
5
- * - If commandOrUrl starts with http:// or https://, auto-detects as HTTP.
6
- * - Otherwise treats it as a stdio command.
7
- * - Writes the entry to the mcpServers object and saves the config file.
7
+ * @param field - "allowedTools" or "disabledTools" (for the message).
8
+ * @param patterns - Patterns to validate; undefined/empty is a no-op.
9
+ * @throws If any pattern is rejected by picomatch.
10
+ */
11
+ function validateToolListPatterns(field, patterns) {
12
+ if (!patterns || patterns.length === 0)
13
+ return;
14
+ for (const pattern of patterns) {
15
+ try {
16
+ validateGlobPattern(pattern);
17
+ }
18
+ catch (err) {
19
+ const reason = err instanceof Error ? err.message : String(err);
20
+ throw new Error(`Invalid "${field}" pattern "${pattern}": ${reason}`);
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * Builds the raw server entry from parsed CLI options, including
26
+ * transport auto-detection, env/headers, description, and the optional
27
+ * enable/filter fields.
8
28
  *
9
- * @param configPath - Absolute path to the mcp.json file.
10
29
  * @param opts - Parsed CLI options.
11
- * @returns Human-readable confirmation message.
12
- * @throws If the server name already exists.
30
+ * @returns The constructed raw server entry and its resolved transport type.
13
31
  */
14
- export async function handleAdd(configPath, opts) {
15
- await ensureConfigDir(configPath);
16
- const servers = await readConfigFile(configPath);
17
- if (opts.name in servers) {
18
- throw new Error(`Server "${opts.name}" already exists. Use "remove ${opts.name}" first to replace it.`);
19
- }
32
+ function buildServerEntry(opts) {
20
33
  // Auto-detect HTTP from URL pattern
21
34
  const isUrl = opts.commandOrUrl.startsWith('http://') || opts.commandOrUrl.startsWith('https://');
22
35
  const type = isUrl ? 'http' : opts.transport;
@@ -36,6 +49,44 @@ export async function handleAdd(configPath, opts) {
36
49
  entry.env = opts.env;
37
50
  }
38
51
  }
52
+ if (opts.description) {
53
+ entry.description = opts.description;
54
+ }
55
+ if (opts.disabled) {
56
+ entry.enabled = false;
57
+ }
58
+ if (opts.allowedTools && opts.allowedTools.length > 0) {
59
+ entry.allowedTools = opts.allowedTools;
60
+ }
61
+ if (opts.disabledTools && opts.disabledTools.length > 0) {
62
+ entry.disabledTools = opts.disabledTools;
63
+ }
64
+ return { entry, type };
65
+ }
66
+ /**
67
+ * Handles the `add <name> <commandOrUrl> [args...]` subcommand.
68
+ *
69
+ * - If commandOrUrl starts with http:// or https://, auto-detects as HTTP.
70
+ * - Otherwise treats it as a stdio command.
71
+ * - Writes the entry to the mcpServers object and saves the config file.
72
+ *
73
+ * @param configPath - Absolute path to the mcp.json file.
74
+ * @param opts - Parsed CLI options.
75
+ * @returns Human-readable confirmation message.
76
+ * @throws If the server name already exists.
77
+ */
78
+ export async function handleAdd(configPath, opts) {
79
+ if (opts.enabled && opts.disabled) {
80
+ throw new Error('Cannot specify both --enabled and --disabled.');
81
+ }
82
+ validateToolListPatterns('allowedTools', opts.allowedTools);
83
+ validateToolListPatterns('disabledTools', opts.disabledTools);
84
+ await ensureConfigDir(configPath);
85
+ const servers = await readConfigFile(configPath);
86
+ if (opts.name in servers) {
87
+ throw new Error(`Server "${opts.name}" already exists. Use "remove ${opts.name}" first to replace it.`);
88
+ }
89
+ const { entry, type } = buildServerEntry(opts);
39
90
  servers[opts.name] = entry;
40
91
  await writeConfigFile(configPath, servers);
41
92
  let result = `Added server "${opts.name}" (${type}).`;
@@ -0,0 +1,29 @@
1
+ import { ensureConfigDir, readConfigFile, writeConfigFile } from './config-io.js';
2
+ /**
3
+ * Handles the `disable <name>` subcommand: sets `enabled: false` on a
4
+ * server entry, preserving every other field. Idempotent — if the server
5
+ * is already disabled, reports it and makes no change. Pure config edit;
6
+ * no network access, probe, or login.
7
+ *
8
+ * @param configPath - Absolute path to the mcp.json file.
9
+ * @param name - Server name to disable.
10
+ * @returns Human-readable confirmation message.
11
+ * @throws If the server name is not found in mcp.json.
12
+ */
13
+ export async function handleDisable(configPath, name) {
14
+ await ensureConfigDir(configPath);
15
+ const servers = await readConfigFile(configPath);
16
+ if (!(name in servers)) {
17
+ const available = Object.keys(servers);
18
+ const hint = available.length > 0
19
+ ? ` Available servers: ${available.join(', ')}`
20
+ : ' No servers configured.';
21
+ throw new Error(`Server "${name}" not found.${hint}`);
22
+ }
23
+ if (servers[name].enabled === false) {
24
+ return `Server "${name}" is already disabled.`;
25
+ }
26
+ servers[name].enabled = false;
27
+ await writeConfigFile(configPath, servers);
28
+ return `Disabled server "${name}".`;
29
+ }
@@ -0,0 +1,35 @@
1
+ import { ensureConfigDir, readConfigFile, writeConfigFile } from './config-io.js';
2
+ /**
3
+ * Handles the `enable <name>` subcommand: removes the `enabled` field
4
+ * from a server entry so it defaults to enabled, preserving every other
5
+ * field. Idempotent — if the server is already enabled (field absent or
6
+ * `true`), reports it and makes no change (a stray `true` is normalized
7
+ * away by deletion). Pure config edit; no network access, probe, or login.
8
+ *
9
+ * @param configPath - Absolute path to the mcp.json file.
10
+ * @param name - Server name to enable.
11
+ * @returns Human-readable confirmation message.
12
+ * @throws If the server name is not found in mcp.json.
13
+ */
14
+ export async function handleEnable(configPath, name) {
15
+ await ensureConfigDir(configPath);
16
+ const servers = await readConfigFile(configPath);
17
+ if (!(name in servers)) {
18
+ const available = Object.keys(servers);
19
+ const hint = available.length > 0
20
+ ? ` Available servers: ${available.join(', ')}`
21
+ : ' No servers configured.';
22
+ throw new Error(`Server "${name}" not found.${hint}`);
23
+ }
24
+ if (servers[name].enabled === undefined || servers[name].enabled === true) {
25
+ // Normalize a stray explicit `true` away so the file stays clean.
26
+ if ('enabled' in servers[name]) {
27
+ delete servers[name].enabled;
28
+ await writeConfigFile(configPath, servers);
29
+ }
30
+ return `Server "${name}" is already enabled.`;
31
+ }
32
+ delete servers[name].enabled;
33
+ await writeConfigFile(configPath, servers);
34
+ return `Enabled server "${name}".`;
35
+ }
@@ -4,3 +4,6 @@ export { handleGet } from './get-command.js';
4
4
  export { handleList } from './list-command.js';
5
5
  export { handleLogin } from './login-command.js';
6
6
  export { handleLogout } from './logout-command.js';
7
+ export { handleEnable } from './enable-command.js';
8
+ export { handleDisable } from './disable-command.js';
9
+ export { handleTools } from './tools-command.js';