Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .changeset/start-tool-filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
'@focus-mcp/cli': minor
---

Add tool hidden-list to `focus start` — hide specific tools from your AI client without uninstalling bricks.
Add tool visibility management to `focus start` — hide or pin tools without uninstalling bricks.

- `focus start --hide=<patterns>` hides matching tools at launch (comma-separated, glob `*` supported)
- `~/.focus/config.json` `tools.hidden` array for persistent per-session config; CLI arg overrides it
- `focus filter hide/show/list/clear` subcommand to manage the hidden list from the terminal
- `focus_filter` MCP tool lets agents manage the hidden list directly from within the AI client
- `focus_filter` itself is always visible regardless of the hidden list (avoids deadlocks)
- `focus start --hide=<patterns>` hides matching tools at launch; `--pin=<patterns>` marks tools as `alwaysLoad`
- `~/.focus/config.json` `tools.hidden` and `tools.alwaysLoad` arrays for persistent config; CLI args override
- `focus config tools hide/show/pin/unpin/list/clear` subcommand to manage visibility from the terminal
- `focus_config` MCP tool lets agents manage their own toolset visibility from within the AI client
- `focus_config` itself is always visible regardless of the hidden list (deadlock protection)
- 5 essential meta tools (`focus_list`, `focus_load`, `focus_search`, `focus_install`, `focus_config`) carry `_meta.anthropic/alwaysLoad: true` by default
20 changes: 20 additions & 0 deletions .changeset/symfony-cli-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@focus-mcp/cli': minor
---

Add `tools:` namespace commands (Symfony-style) + rename MCP tool `focus_config` → `focus_tools`.

New canonical command names:
- `focus tools:hide <pattern>` — hide tool (alias: `filter hide`)
- `focus tools:show <pattern>` — unhide tool (alias: `filter show`)
- `focus tools:pin <pattern>` — mark as alwaysLoad
- `focus tools:unpin <pattern>` — remove from alwaysLoad
- `focus tools:list` — show hidden + alwaysLoad lists (alias: `filter list`)
- `focus tools:clear` — reset both lists (alias: `filter clear`)

Also adds `catalog:` namespace aliases:
- `focus catalog:list`, `focus catalog:add`, `focus catalog:remove`

Old flat names (`filter hide`, `filter list`, etc.) remain as permanent aliases — no deprecation, no breaking change.

MCP tool rename: `focus_config` → `focus_tools` (actions: `hide`, `show`, `pin`, `unpin`, `list`, `clear`). `focus_tools` is immune to hidden lists.
50 changes: 32 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,45 +136,59 @@ focus start --hide="sym_get,ts_cleanup"
Patterns support a trailing `*` glob (`focus_*` matches `focus_install`, `focus_list`, etc.).
Exact names are also accepted.

> **Note:** `focus_filter` is always visible regardless of the hidden list, so you can always
> manage the hidden list from within your AI client.
> **Note:** `focus_tools` is always visible regardless of the hidden list, so you can always
> manage tool visibility from within your AI client.

### Persistent config: `~/.focus/config.json`

Add a `tools.hidden` list to hide tools across all sessions:
Add a `tools` section to persist filters across sessions:

```json
{
"tools": {
"hidden": ["sym_get", "focus_remove"]
"hidden": ["sym_get", "fo_delete"],
"alwaysLoad": ["ts_index"]
}
}
```

CLI `--hide` overrides the config file. If neither is set, all tools are exposed (default).
CLI flags override the config file. If neither is set, all tools are exposed (default).

### Manage the hidden list: `focus filter`
Add `--pin=<patterns>` to mark tools as always-loaded (surfaced as `_meta.anthropic/alwaysLoad: true` in MCP responses):

```bash
focus filter list # show current hidden list
focus filter hide sym_get # add sym_get to the hidden list
focus filter hide "focus_*" # hide an entire family (glob)
focus filter show sym_get # remove sym_get from the hidden list
focus filter clear # unhide everything
focus start --pin="ts_index,sym_find"
```

### Manage from the terminal: `focus tools:`

```bash
focus tools:list # show current hidden + alwaysLoad lists
focus tools:hide sym_get # add sym_get to the hidden list
focus tools:hide "focus_*" # hide an entire family (glob)
focus tools:show sym_get # remove sym_get from the hidden list
focus tools:pin ts_index # mark ts_index as alwaysLoad
focus tools:unpin ts_index # remove ts_index from alwaysLoad
focus tools:clear # reset both lists

# Legacy aliases (permanent, no deprecation):
focus filter list
focus filter hide sym_get
```

Changes are written to `~/.focus/config.json` and take effect on the next `focus start`.

### From your AI client: `focus_filter` MCP tool
### From your AI client: `focus_tools` MCP tool

The `focus_filter` MCP tool mirrors the CLI subcommand — your AI agent can manage the hidden
list directly:
The `focus_tools` MCP tool lets your AI agent manage tool visibility directly:

```
focus_filter action=hide pattern=sym_get
focus_filter action=show pattern=sym_get
focus_filter action=list
focus_filter action=clear
focus_tools action=hide pattern=sym_get
focus_tools action=show pattern=sym_get
focus_tools action=pin pattern=ts_index
focus_tools action=unpin pattern=ts_index
focus_tools action=list
focus_tools action=clear
```

Restart `focus start` (or reload your MCP client) to apply changes.
Expand Down
183 changes: 156 additions & 27 deletions src/bin/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ import { parseCenterJson, parseCenterLock } from '../center.ts';
import { addManyCommand } from '../commands/add.ts';
import { browseCommand } from '../commands/browse.ts';
import { catalogCommand } from '../commands/catalog.ts';
import {
configToolsClearCommand,
configToolsHideCommand,
configToolsListCommand,
configToolsPinCommand,
configToolsShowCommand,
configToolsUnpinCommand,
} from '../commands/config.ts';
import type { DoctorIO } from '../commands/doctor.ts';
import { doctorCommand, formatDoctorOutput } from '../commands/doctor.ts';
import {
filterClearCommand,
filterHideCommand,
filterListCommand,
filterShowCommand,
} from '../commands/filter.ts';
import { infoCommand } from '../commands/info.ts';
import { listCommand } from '../commands/list.ts';
import { reinstallCommand } from '../commands/reinstall.ts';
Expand All @@ -52,17 +54,26 @@ Commands:
reinstall <name> [...] Force-reinstall (preserves enabled state; use after doctor)
upgrade [name] [--all] Re-install brick(s) at the latest catalog version
search [query] Search bricks in the catalog
catalog Manage catalog sources (add|remove|list)
catalog [list|add|remove] Manage catalog sources (subcommand or catalog: namespace below)
catalog:list List catalog sources
catalog:add <url> <name> Add a catalog source
catalog:remove <url> Remove a catalog source
doctor [--json] [--fix] Audit local state and report actionable issues
--fix auto-remediate corrupted installs and missing deps
browse Interactive TUI to browse catalogs and bricks
start [--hide=<patterns>] Launch FocusMCP as a stdio MCP server (AI clients attach here)
start [options] Launch FocusMCP as a stdio MCP server (AI clients attach here)
--hide=<patterns> comma-separated patterns to hide (e.g. "sym_get,focus_*")
filter <action> [pattern] Manage the tool hidden-list (persisted in ~/.focus/config.json)
hide <pattern> hide a tool or glob (e.g. focus filter hide sym_get)
show <pattern> unhide a tool or glob
list show the current hidden list
clear unhide all tools
--pin=<patterns> comma-separated patterns to mark as alwaysLoad

Tool visibility (tools: namespace):
tools:hide <pattern> Hide a tool or glob (alias: filter hide)
tools:show <pattern> Unhide a tool or glob (alias: filter show)
tools:pin <pattern> Mark as alwaysLoad (_meta.anthropic/alwaysLoad: true)
tools:unpin <pattern> Remove from alwaysLoad list
tools:list Show hidden + alwaysLoad lists (alias: filter list)
tools:clear Reset both lists (alias: filter clear)
Legacy aliases: filter hide|show|list|clear (permanent, no deprecation)

help Print this help

Options:
Expand Down Expand Up @@ -319,40 +330,135 @@ async function runDoctor(rest: string[]): Promise<number> {
return result.errors > 0 ? 1 : 0;
}

async function runFilter(rest: string[]): Promise<number> {
const sub = rest[0];
/**
* `runTools` — shared handler for `focus tools:<action>` (Symfony canonical)
* and `focus filter <action>` (legacy alias, permanent).
*
* `rest` is [action, pattern?].
*/
async function runTools(rest: string[]): Promise<number> {
const action = rest[0];
const pattern = rest[1];

if (sub === 'hide') {
if (action === 'hide') {
if (!pattern) {
process.stderr.write('error: `focus tools:hide <pattern>` requires a pattern.\n');
return 1;
}
process.stdout.write(`${await configToolsHideCommand(pattern)}\n`);
return 0;
}

if (action === 'show') {
if (!pattern) {
process.stderr.write('error: `focus tools:show <pattern>` requires a pattern.\n');
return 1;
}
process.stdout.write(`${await configToolsShowCommand(pattern)}\n`);
return 0;
}

if (action === 'pin') {
if (!pattern) {
process.stderr.write('error: `focus filter hide <pattern>` requires a pattern.\n');
process.stderr.write('error: `focus tools:pin <pattern>` requires a pattern.\n');
return 1;
}
process.stdout.write(`${await filterHideCommand(pattern)}\n`);
process.stdout.write(`${await configToolsPinCommand(pattern)}\n`);
return 0;
}

if (sub === 'show') {
if (action === 'unpin') {
if (!pattern) {
process.stderr.write('error: `focus filter show <pattern>` requires a pattern.\n');
process.stderr.write('error: `focus tools:unpin <pattern>` requires a pattern.\n');
return 1;
}
process.stdout.write(`${await configToolsUnpinCommand(pattern)}\n`);
return 0;
}

if (action === 'list' || action === undefined) {
process.stdout.write(`${await configToolsListCommand()}\n`);
return 0;
}

if (action === 'clear') {
process.stdout.write(`${await configToolsClearCommand()}\n`);
return 0;
}

process.stderr.write(
`error: unknown tools action "${action}". Use: hide, show, pin, unpin, list, clear\n`,
);
return 1;
}

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multi-branch CLI dispatch for config tools subcommands
async function runConfig(rest: string[]): Promise<number> {
// Expect: ["tools", <action>, <pattern?>]
if (rest[0] !== 'tools') {
process.stderr.write(
`error: unknown config subcommand "${rest[0] ?? ''}". Use: focus config tools <action>\n`,
);
return 1;
}

const action = rest[1];
const pattern = rest[2];

if (action === 'hide') {
if (!pattern) {
process.stderr.write(
'error: `focus config tools hide <pattern>` requires a pattern.\n',
);
return 1;
}
process.stdout.write(`${await configToolsHideCommand(pattern)}\n`);
return 0;
}

if (action === 'show') {
if (!pattern) {
process.stderr.write(
'error: `focus config tools show <pattern>` requires a pattern.\n',
);
return 1;
}
process.stdout.write(`${await configToolsShowCommand(pattern)}\n`);
return 0;
}

if (action === 'pin') {
if (!pattern) {
process.stderr.write('error: `focus config tools pin <pattern>` requires a pattern.\n');
return 1;
}
process.stdout.write(`${await configToolsPinCommand(pattern)}\n`);
return 0;
}

if (action === 'unpin') {
if (!pattern) {
process.stderr.write(
'error: `focus config tools unpin <pattern>` requires a pattern.\n',
);
return 1;
}
process.stdout.write(`${await filterShowCommand(pattern)}\n`);
process.stdout.write(`${await configToolsUnpinCommand(pattern)}\n`);
return 0;
}

if (sub === 'list' || sub === undefined) {
process.stdout.write(`${await filterListCommand()}\n`);
if (action === 'list' || action === undefined) {
process.stdout.write(`${await configToolsListCommand()}\n`);
return 0;
}

if (sub === 'clear') {
process.stdout.write(`${await filterClearCommand()}\n`);
if (action === 'clear') {
process.stdout.write(`${await configToolsClearCommand()}\n`);
return 0;
}

process.stderr.write(
`error: unknown filter subcommand "${sub}". Use: hide, show, list, clear\n`,
`error: unknown action "${action}". Use: hide, show, pin, unpin, list, clear\n`,
);
return 1;
}
Expand Down Expand Up @@ -404,10 +510,33 @@ async function main(argv: string[]): Promise<number> {
return runSearch(rest);
case 'catalog':
return runCatalog(rest);
// catalog: namespace — Symfony-style aliases (permanent)
case 'catalog:list':
return runCatalog(['list', ...rest]);
case 'catalog:add':
return runCatalog(['add', ...rest]);
case 'catalog:remove':
return runCatalog(['remove', ...rest]);
case 'doctor':
return runDoctor(rest);
case 'config':
return runConfig(rest);
// tools: namespace — canonical Symfony-style commands
case 'tools:hide':
return runTools(['hide', ...rest]);
case 'tools:show':
return runTools(['show', ...rest]);
case 'tools:pin':
return runTools(['pin', ...rest]);
case 'tools:unpin':
return runTools(['unpin', ...rest]);
case 'tools:list':
return runTools(['list', ...rest]);
case 'tools:clear':
return runTools(['clear', ...rest]);
// filter <action> [pattern] — legacy alias for tools: (permanent, no deprecation)
case 'filter':
return runFilter(rest);
return runTools(rest);
case 'browse':
await browseCommand();
return 0;
Expand Down
Loading
Loading