Metadata-Version: 2.4
Name: narrow-mcp
Version: 0.1.0
Summary: MCP server that narrows large low-density files (logs, CSV, JSON, HTML) to the verbatim spans relevant to an intent, verified against source.
Author: Manik Prakash
License-Expression: MIT
Project-URL: Repository, https://github.com/manik-prakash/narrow-mcp
Project-URL: Issues, https://github.com/manik-prakash/narrow-mcp/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp[cli]>=2.0.0
Requires-Dist: anthropic>=0.40.0
Requires-Dist: openai>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Dynamic: license-file

# narrow-mcp

<!-- mcp-name: io.github.manik-prakash/narrow-mcp -->

An MCP server that narrows large, low-density non-source files (logs, test
output, CSV, JSON, HTML) down to the verbatim spans relevant to a stated
intent -- verified against the original file, never a summary.

## Why

Coding agents burn context reading large low-density files. A 40k-token log
might hold a few hundred tokens of signal. This tool:

1. Runs deterministic narrowing first (grep-style search, structural
   parsing per file type, sampling) -- free, fast, zero LLM cost.
2. If that alone resolves the query with confidence (e.g. an exact CSV
   "null column" query), returns it directly. No LLM call at all.
3. Otherwise passes only the narrowed candidates (never the raw file) to a
   cheap, fast selector model that returns line ranges and a one-line
   reason -- never prose.
4. Re-reads the chosen line ranges from the **original file on disk** and
   returns that verbatim text. The selector's own words are never trusted
   or returned -- only its line-number coordinates, which get verified.
5. If the selector fails, times out, or returns something invalid, falls
   back to the deterministic candidate set rather than failing outright.

Non-goals: source code retrieval (use LSP/tree-sitter/ast-grep), prose
summarization, local/self-hosted models.

## Status

v1, single file per call. Four file types: log/build-output, CSV, JSON
(single document or JSONL), HTML.

## Development

```
pip install -e ".[dev]"
pytest
python eval/run_eval.py          # mocked selector, free
python eval/run_eval.py --live   # real selector call, needs an API key (see Configuration)
```

## Configuration

**You only need to set one API key.** The provider is auto-detected from
whichever key is present -- no separate provider/model config required:

| If you set...       | Provider used | Default model                                  |
|----------------------|----------------|-------------------------------------------------|
| `ANTHROPIC_API_KEY`   | `anthropic`    | `claude-haiku-4-5`                              |
| `OPENAI_API_KEY`      | `openai`       | `gpt-5-nano`                                    |
| `OPENROUTER_API_KEY`  | `openrouter`   | `openrouter/free` (see below)                   |
| *(none)*              | `anthropic`    | `claude-haiku-4-5` (calls just always fall back to the deterministic path) |

If more than one key is set, priority is Anthropic > OpenAI > OpenRouter.
Override anything explicitly with the env vars below.

### Using OpenRouter's free models

OpenRouter still requires its own API key even for $0-cost models -- set
`OPENROUTER_API_KEY` and you're done, no other config needed. It defaults to
**`openrouter/free`**, a meta-router that auto-picks among whichever
tool-calling-capable models are currently free, so it never goes stale the
way hardcoding one specific `:free` model name would.

To see the current free-model roster live (it rotates) and pick a specific
one instead of the meta-router:

```
narrow-mcp-list-free-models
```

Then set `NARROW_MCP_SELECTOR_MODEL=<id>` to whichever one you want.

OpenRouter's free tier is rate-limited (20 req/min; 50 req/day, or 1000/day
once the account has $10+ lifetime spend) -- fine for interactive use, worth
knowing about for batch runs.

### All environment variables

- `NARROW_MCP_SELECTOR_PROVIDER` -- `anthropic` | `openai` | `openrouter`.
  Overrides auto-detection.
- `NARROW_MCP_SELECTOR_MODEL` -- overrides the provider's default model.
- `NARROW_MCP_SELECTOR_API_KEY_ENV` -- overrides which env var holds the key.
- `NARROW_MCP_SELECTOR_TIMEOUT_S` (default `3.0`)
- `NARROW_MCP_MAX_CANDIDATE_CHARS`, `NARROW_MCP_MAX_CANDIDATES`,
  `NARROW_MCP_CONTEXT_LINES`, `NARROW_MCP_RIPGREP_PATH`,
  `NARROW_MCP_MAX_JSON_BYTES`

## Registering with Claude Code

```
claude mcp add narrow-mcp -- uvx narrow-mcp
```

Verify current `claude mcp add` flag syntax against `claude mcp add --help`
before relying on the above -- CLI flags change across releases.
