Skip to main content
Glama
mapix

filter-mcp-tools

by mapix
README.md
# filter-mcp-tools

[![npm](https://img.shields.io/npm/v/filter-mcp-tools.svg)](https://www.npmjs.com/package/filter-mcp-tools)
[![CI](https://github.com/mapix/filter-mcp-tools/actions/workflows/ci.yml/badge.svg)](https://github.com/mapix/filter-mcp-tools/actions/workflows/ci.yml)

`filter-mcp-tools` is a deterministic MCP tool-list relay. It connects to one
HTTP, legacy SSE, or stdio MCP server, lets an operator inspect the complete
catalog, and exposes a filtered stdio MCP server to Claude Code, Codex, or any
other local MCP client.

## Behavior

- Exact, glob, and regular-expression include/exclude rules match full tool names.
- Include rules are ORed. When any include rule exists, unmatched tools are removed.
- Exclude rules are ORed and always win over include rules.
- `tools/list` returns only retained tools; direct `tools/call` attempts for hidden
  tools are rejected before reaching the upstream server.
- A zero-tool result fails closed unless `--allow-empty` is explicit.
- Tool pagination and `notifications/tools/list_changed` are handled.
- Prompts, resources, subscriptions, and completion requests pass through unchanged
  when the upstream advertises them.

This is deterministic name filtering, not semantic retrieval. It is designed for
auditable allowlists and denylists.

## Recommended workflow

First inspect the upstream catalog without a filter:

```bash
npx -y filter-mcp-tools@latest list \
  --url https://mcp.example.com/mcp \
  --names-only
```

Then preview both sides of a proposed rule before exposing the relay:

```bash
npx -y filter-mcp-tools@latest list \
  --url https://mcp.example.com/mcp \
  --include-glob 'docs_*' \
  --exclude-regex '/delete|remove/i' \
  --show kept

npx -y filter-mcp-tools@latest list \
  --url https://mcp.example.com/mcp \
  --include-glob 'docs_*' \
  --exclude-regex '/delete|remove/i' \
  --show removed
```

Move the accepted rules into a JSON config and use the same file for `list` and
`proxy`. This keeps review and runtime behavior identical.

## Install and test

```bash
npm install --global filter-mcp-tools
filter-mcp-tools --version
```

To develop locally:

```bash
git clone https://github.com/mapix/filter-mcp-tools.git
cd filter-mcp-tools
npm ci
npm run check
```

One-off use does not require a global install:

```bash
npx -y filter-mcp-tools@latest --help
```

## Inspect before filtering

List every tool from an HTTP MCP endpoint:

```bash
filter-mcp-tools list \
  --url https://mcp.example.com/mcp \
  --header-env Authorization=MCP_AUTHORIZATION
```

Preview a filter and include the match explanation and complete MCP schemas:

```bash
filter-mcp-tools list \
  --url https://mcp.example.com/mcp \
  --header-env Authorization=MCP_AUTHORIZATION \
  --include-glob '*docs*' \
  --exclude-regex '/delete|remove/i' \
  --json
```

Useful output controls:

```bash
--show kept       # all, kept, or removed
--names-only      # one selected tool name per line
--json            # schemas plus includedBy/excludedBy explanations
```

## Run as a relay

```bash
filter-mcp-tools proxy \
  --url https://mcp.example.com/mcp \
  --header-env Authorization=MCP_AUTHORIZATION \
  --include-glob '*docs*' \
  --exclude-regex '/delete|remove/i'
```

The relay speaks MCP over stdout/stdin, so stdout is reserved for protocol frames.
Diagnostics go to stderr and never include resolved header values.

Claude Code example (pin the version in long-lived configuration):

```bash
claude mcp add -s user filtered-docs -- \
  npx -y filter-mcp-tools@0.1.0 proxy --config /absolute/path/filter.json
```

Codex uses the same stdio command:

```toml
[mcp_servers.filtered-docs]
command = "npx"
args = ["-y", "filter-mcp-tools@0.1.0", "proxy", "--config", "/absolute/path/filter.json"]
```

Clients with native per-server tool filtering can use that instead and avoid the
extra process.

## JSON configuration

Configuration avoids long client command lines and supports secret sources without
shell evaluation:

```json
{
  "upstream": {
    "transport": "http",
    "url": "https://mcp.example.com/mcp",
    "headers": {
      "Authorization": {
        "env": "MCP_TOKEN",
        "prefix": "Bearer "
      },
      "x-api-key": {
        "command": [
          "/usr/bin/security",
          "find-generic-password",
          "-s",
          "my-mcp-token",
          "-w"
        ]
      }
    }
  },
  "filter": {
    "include": ["docs_read"],
    "includeGlob": ["docs_*"],
    "includeRegex": ["/^wiki_(read|search)$/"],
    "exclude": [],
    "excludeGlob": ["*_admin"],
    "excludeRegex": ["/delete|remove/i"],
    "ignoreCase": false
  },
  "allowEmpty": false
}
```

Run it with `filter-mcp-tools list --config filter.json` to preview, then change
`list` to `proxy`. Command secret sources use `execFile`, never a shell; only use
configuration files you trust.

[`examples/filter.example.json`](examples/filter.example.json) is a copyable
starting point.

Literal header values support `${ENV_VAR}` interpolation. Avoid putting secrets in
CLI arguments because process listings and shell history can expose them.

Use `--transport sse` only for a legacy HTTP+SSE upstream. Streamable HTTP is the
default for `--url`; the relay exposed to the local client remains stdio in both
cases.

## Stdio upstream

```bash
filter-mcp-tools list \
  --transport stdio \
  --stdio-command node \
  --stdio-arg ./server.mjs \
  --include-regex '/^docs_/'
```

The JSON equivalents are `upstream.command`, `args`, `cwd`, and `env`.

## Scope and security

This relay is a client-side visibility and invocation boundary. A user who controls
the relay configuration can change its filters, so it does not replace authorization
on the upstream MCP server. Enforce real access through server-, key-, team-, or
identity-bound policy, then use this package to narrow the already-authorized set.

See [SECURITY.md](SECURITY.md) for vulnerability reporting.