|
| 1 | +# Tool Filtering Design — Arcane MCP Server |
| 2 | + |
| 3 | +**Date:** 2026-04-14 |
| 4 | +**Status:** Approved, ready for implementation planning |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +Arcane MCP Server registers all 180 tools (across 25 modules) unconditionally at startup. Every tool's schema and description is sent to the client on `tools/list`, which bloats the LLM's context window — even when the user only cares about a fraction of them. We need to let users trim the active tool set to what they actually use, and change that set on the fly without rebuilding or reconfiguring from scratch. |
| 9 | + |
| 10 | +**Goals:** |
| 11 | +- Reduce context bloat by letting users filter which tools the MCP server exposes. |
| 12 | +- Support three entry points: installer (first run), slash command (`/arcane:configure`), and an MCP prompt (`arcane_configure_tools`) for non-Claude-Code clients. |
| 13 | +- Reload changes hot when feasible; fall back to "reconnect MCP" otherwise. |
| 14 | +- Do not break existing installs on upgrade. |
| 15 | + |
| 16 | +**Non-goals:** |
| 17 | +- Per-environment tool filtering (e.g., "different tools for prod vs staging"). |
| 18 | +- Role-based access control on tools. |
| 19 | +- Server-side authorization of who can flip the config. |
| 20 | + |
| 21 | +## Architecture — "register-all, then filter" |
| 22 | + |
| 23 | +Register all 180 tools at startup as today, but capture the `RegisteredTool` handles returned by `server.registerTool()` into a central `ToolRegistry`. Immediately after registration, walk the registry and call `.disable()` on anything outside the resolved enabled-set. The SDK automatically filters disabled tools out of `tools/list` and emits `notifications/tools/list_changed` — so Claude Code only ever sees the enabled subset. |
| 24 | + |
| 25 | +**Why this over filtering-at-registration:** |
| 26 | +- Mechanical, minimal refactor of the 25 tool modules (each `registerXTools()` just captures return values). |
| 27 | +- Hot reload reduces to diff-and-call-`.enable()/.disable()`. SDK handles notifications. |
| 28 | +- 180 handler closures in memory is negligible. |
| 29 | +- Works identically in stdio + HTTP modes since both share `_registeredTools`. |
| 30 | + |
| 31 | +**SDK capabilities validated** (see `node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts:266-290` and `mcp.js:68-69, 618-646`): |
| 32 | +- `registerTool()` returns `{ enable(), disable(), update(), remove(), enabled }`. |
| 33 | +- `tools/list` handler filters on `tool.enabled`. |
| 34 | +- `.enable()/.disable()/.update()` auto-emits `sendToolListChanged()` if connected. |
| 35 | + |
| 36 | +## Config schema |
| 37 | + |
| 38 | +Extend `ArcaneConfig` in [src/config.ts](../../src/config.ts): |
| 39 | + |
| 40 | +```ts |
| 41 | +interface ToolsConfig { |
| 42 | + preset?: "commonly-used" | "read-only" | "minimal" | "deploy" | "full" | "custom"; |
| 43 | + modules?: string[]; // allowlist of module names |
| 44 | + enabled?: string[]; // per-tool overrides: additions |
| 45 | + disabled?: string[]; // per-tool overrides: subtractions |
| 46 | +} |
| 47 | + |
| 48 | +interface ArcaneConfig { |
| 49 | + // ...existing fields... |
| 50 | + tools?: ToolsConfig; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +**Resolution order** (applied in `resolveEnabledTools(config, registry)`): |
| 55 | +1. Start from `preset` expansion (or `full` if preset is absent and config is fresh; see "Upgrade behavior" below). |
| 56 | +2. If `modules` is set, intersect with the union of those modules' tools. |
| 57 | +3. Add anything in `enabled`. |
| 58 | +4. Subtract anything in `disabled`. |
| 59 | +5. Result = enabled-set. |
| 60 | + |
| 61 | +**Sources** (existing priority preserved): env vars > `~/.arcane/config.json` > defaults. |
| 62 | + |
| 63 | +New env vars: |
| 64 | +- `ARCANE_TOOL_PRESET` — e.g., `commonly-used` |
| 65 | +- `ARCANE_ENABLED_MODULES` — comma-separated module names |
| 66 | +- `ARCANE_ENABLED_TOOLS` — comma-separated tool names |
| 67 | +- `ARCANE_DISABLED_TOOLS` — comma-separated tool names |
| 68 | + |
| 69 | +## Presets |
| 70 | + |
| 71 | +Declarative data in a new [src/tools/presets.ts](../../src/tools/presets.ts). Editing presets = editing one file. |
| 72 | + |
| 73 | +| Preset | Contents | Rough tool count | Intended user | |
| 74 | +|---|---|---|---| |
| 75 | +| `commonly-used` | dashboard, container, image, project (stacks), volume, network, environment, system | ~60–70 | Default for new installs — "I want to manage Docker" | |
| 76 | +| `read-only` | every `*_list`, `*_get`, `*_inspect`, `*_stats` across all modules | ~60 | Status / observability assistants | |
| 77 | +| `minimal` | dashboard + container list/logs/stats only | ~10 | Smallest viable footprint | |
| 78 | +| `deploy` | project, gitops, template, registry, environment, build | ~40 | CI / deploy assistants | |
| 79 | +| `full` | all 180 | 180 | Today's behavior; also the upgrade fallback | |
| 80 | +| `custom` | implied when user has manually edited `modules`/`enabled`/`disabled` | variable | Power users | |
| 81 | + |
| 82 | +## Entry points |
| 83 | + |
| 84 | +### 1. Slash command `/arcane:configure` (Claude Code) |
| 85 | + |
| 86 | +Ships as `commands/configure.md` inside the plugin. Uses `AskUserQuestion` to walk the user through: |
| 87 | + |
| 88 | +1. **Preset picker** — single-select from the 6 presets. |
| 89 | +2. **If `custom` or "fine-tune"** — module picker, multiSelect across the 25 modules, pre-checked to current selection. |
| 90 | +3. **Per-tool step** — "Want to fine-tune individual tools? (yes/no)". If yes, iterate selected modules *one at a time*, showing a multiSelect of that module's tools so `AskUserQuestion` never exceeds ~10 options at a time. |
| 91 | +4. Write resolved config to `~/.arcane/config.json` (preserve existing non-`tools` keys). |
| 92 | +5. Touch config file mtime. If the hot-reload watcher picks it up, print "✓ tools refreshed". If the client doesn't observe `list_changed` within ~2s, print fallback: "Please run `/mcp` to reconnect the arcane server." |
| 93 | + |
| 94 | +### 2. Interactive installer (first run) |
| 95 | + |
| 96 | +Extend [install_arcane_skill-mcp.md](../../install_arcane_skill-mcp.md) with a "Step 4: Tool selection" section after env-var collection. Same logical flow as the slash command, extracted into a shared snippet referenced by both. Default-highlight `commonly-used`. New users never see the 180-tool firehose on day one. |
| 97 | + |
| 98 | +### 3. MCP prompt `arcane_configure_tools` |
| 99 | + |
| 100 | +Added to [src/prompts/index.ts](../../src/prompts/index.ts). For Claude Desktop / other MCP clients without slash commands. Renders as a structured prompt that instructs the client's LLM to walk the user through preset → module → tool selection via plain chat and then write `~/.arcane/config.json`. Less polished than `AskUserQuestion` but keeps feature parity off Claude Code. |
| 101 | + |
| 102 | +## Hot reload — hybrid |
| 103 | + |
| 104 | +After `registerAllTools()`, [src/server.ts](../../src/server.ts) starts a debounced `fs.watch()` on `~/.arcane/config.json`. On change: |
| 105 | + |
| 106 | +1. Re-run `loadConfig()`. |
| 107 | +2. Compute new enabled-set. |
| 108 | +3. Diff against the current enabled-set. |
| 109 | +4. Call `.enable()` / `.disable()` per tool — SDK emits `list_changed` automatically. |
| 110 | + |
| 111 | +**Fallback:** if the watcher can't attach (HTTP mode behind a proxy, env-var-only setup with no config file), we log a single warning at startup. The slash command detects this by checking a `hotReloadAvailable` flag on the registry and emits the "please reconnect" message instead of claiming success. |
| 112 | + |
| 113 | +Debounce ~250ms to absorb editor save chatter. If `loadConfig()` throws on a malformed file, log and keep the old set live — never disable everything on a parse error. |
| 114 | + |
| 115 | +## Upgrade behavior — one-time prompt |
| 116 | + |
| 117 | +When the server boots with a config that has **no** `tools` key (either no config file or an older one), we treat it as "unconfigured": |
| 118 | + |
| 119 | +1. Default preset is **`full`** (backwards-compatible — all 180 tools still enabled, existing workflows keep working). |
| 120 | +2. A one-time startup notice is logged (stderr, visible in Claude Code's MCP log). |
| 121 | +3. A static MCP resource `arcane://tools-config-notice` is registered, pointing the user to `/arcane:configure` with a short explanation of the context-bloat reason. |
| 122 | +4. Once the user runs `/arcane:configure` (or any config write introduces a `tools` key), the notice resource disappears on the next reload. |
| 123 | + |
| 124 | +Fresh installs via the interactive installer skip step 1's `full` default — they get `commonly-used` selected in the installer UI directly. |
| 125 | + |
| 126 | +## Files to change |
| 127 | + |
| 128 | +| File | Change | |
| 129 | +|---|---| |
| 130 | +| `src/tools/registry.ts` (new) | `ToolRegistry` class: `register(module, name, handle)`, `applyFilter(config)`, `diffAndApply(newConfig)`, `moduleOf(toolName)`, `hotReloadAvailable` flag. Holds `Map<string, { module, handle }>`. | |
| 131 | +| `src/tools/presets.ts` (new) | Declarative preset definitions + `resolveEnabled(config, allTools)` helper. | |
| 132 | +| `src/tools/*.ts` (all 25 modules) | Mechanical edit: each `registerXTools(server)` → `registerXTools(server, registry)`. Capture return values from `server.registerTool()` into `registry.register(...)`. Codemod-able. | |
| 133 | +| [src/tools/index.ts](../../src/tools/index.ts) | `registerAllTools(server, config)` creates registry, calls each module with it, applies filter. Returns registry for reuse. | |
| 134 | +| [src/config.ts](../../src/config.ts) | Add `tools` field + new env var parsing + preset name validation. Detect "unconfigured" state for upgrade notice. | |
| 135 | +| [src/server.ts](../../src/server.ts) | Start debounced config file watcher, wire `registry.diffAndApply()` on change. Export registry for HTTP session reuse. Register upgrade notice resource conditionally. | |
| 136 | +| [src/resources/index.ts](../../src/resources/index.ts) | New `arcane://tools-config-notice` resource, gated on "unconfigured" flag. | |
| 137 | +| [src/prompts/index.ts](../../src/prompts/index.ts) | New `arcane_configure_tools` prompt. | |
| 138 | +| `commands/configure.md` (new) | Plugin slash command `/arcane:configure`. | |
| 139 | +| [install_arcane_skill-mcp.md](../../install_arcane_skill-mcp.md) | Add "Step 4: Tool selection" section. | |
| 140 | +| [.claude-plugin/plugin.json](../../.claude-plugin/plugin.json) | Register the new command. | |
| 141 | +| `src/__tests__/tool-registry.test.ts` (new) | Unit tests: preset expansion, diff resolution, modules+enabled+disabled precedence, hot reload diff. | |
| 142 | +| `src/__tests__/presets.test.ts` (new) | Snapshot tests for each preset's resolved tool set. | |
| 143 | + |
| 144 | +## Error handling |
| 145 | + |
| 146 | +- **Unknown tool/module in config:** log warning, ignore (forward-compat — preset drift after downgrade shouldn't brick the server). |
| 147 | +- **Unknown preset:** fall back to `full`, log warning, surface in the upgrade notice resource. |
| 148 | +- **Config file parse error during hot reload:** keep current enabled-set live, log error. Never disable everything on a typo. |
| 149 | +- **Watcher attach failure:** log once at startup, set `hotReloadAvailable = false`, slash command shows the reconnect fallback message. |
| 150 | + |
| 151 | +## Verification / test plan |
| 152 | + |
| 153 | +- **Unit tests:** preset expansion, config resolution order (preset → modules → enabled → disabled), diff logic for hot reload. |
| 154 | +- **Integration test:** boot server with each preset, assert `tools/list` response size and exact tool names. |
| 155 | +- **Hot reload test:** boot server, modify config file, assert `list_changed` notification fired and `tools/list` reflects new set within 500ms. |
| 156 | +- **Upgrade test:** boot with old-shape config, assert notice resource present and default is `full`. Write new-shape config, assert notice resource removed on reload. |
| 157 | +- **Manual:** |
| 158 | + 1. Fresh install via installer → `commonly-used` preset → boot → Claude Code sees ~65 tools. |
| 159 | + 2. Run `/arcane:configure` → switch to `minimal` → tools list shrinks live. |
| 160 | + 3. Run `/arcane:configure` → custom module + per-tool fine-tune → specific tools toggle. |
| 161 | + 4. Simulate "upgrade" by removing `tools` key from config → restart → notice appears → run `/arcane:configure` → notice clears. |
| 162 | + 5. Install via Claude Desktop (no slash commands) → invoke `arcane_configure_tools` prompt → walk through conversation → config written → reconnect → filter applied. |
| 163 | + |
| 164 | +## Open for implementation phase |
| 165 | + |
| 166 | +- Exact tool list for `commonly-used` preset (curation pass across the 25 modules). |
| 167 | +- Whether to ship a VS Code snippet / codemod for the 25-module `registerTool()` return-value capture, or just do it by hand. |
| 168 | +- Whether `/arcane:configure` should show a diff ("3 tools added, 12 removed") before writing. |
0 commit comments