Skip to main content
Glama
swarnim-git3

mcp-model-router

by swarnim-git3
README.md
# MCP Model Router

An MCP server that routes each prompt to whichever Claude model best fits it.

The routing decision itself is **100% local** — plain Python, no LLM call. Only
one model call happens per request: the one that actually answers it. That
matters, because the obvious way to build this (ask a model to classify the
prompt first) doubles the number of calls and charges you for the classifier on
every single request.

## The tool

The server exposes one tool over MCP:

```python
smart_prompt(prompt: str) -> str
```

Any MCP-capable client can discover and call it. The response comes back with
the routing decision attached, so you can always audit the choice:

```
[routed to opus (claude-opus-5) — matched opus signals (score=2)]

<the answer>
```

## How routing works

`choose_model(prompt)` in `router.py` returns `(tier, reason)`. In order:

1. **Force tags.** A prompt starting with `!opus`, `!sonnet`, `!haiku` or
   `!fable` bypasses routing entirely.
2. **Signal scoring.** Four buckets are scored against the prompt:
   - code patterns (fenced blocks, `def`, stack traces, `.py`/`.js`/`.tsx`,
     regex, unit test) → coding tier
   - reasoning keywords (why, analyze, trade-off, architecture, root cause,
     pros and cons) → deepest tier
   - creative keywords (write a story, poem, screenplay, lyrics) → narrative tier
   - extraction keywords (extract, classify, translate, one sentence, tl;dr)
     → fastest tier
3. **Complexity escalation.** Question marks, bullet lines and numbered list
   items are summed; three or more structural signals adds a point to the
   deep-reasoning tier.
4. **Length fallback.** Only if nothing scored: ≤12 words → fastest tier,
   ≥150 words → deepest tier, otherwise the balanced generalist.

Keyword signals deliberately **outrank** the length heuristic. Without that
precedence, a short prompt like "write a short story" would let the word-count
bonus outvote the creative-keyword match it should lose to.

## Tuning it

Every decision is appended to `logs/routing_log.csv`:

| timestamp | tier | reason | prompt_excerpt |
| --- | --- | --- | --- |

The keyword lists in `router.py` are a starting point, not a final answer.
Watch the log fill up and edit them directly. After any change, re-run the
offline smoke test — it asserts expected routing on seven representative
prompts and costs nothing, because it never touches the API:

```bash
python test_router.py
```

## Setup

```bash
python -m venv .venv
.venv\Scripts\activate          # Windows
pip install -r requirements.txt

cp .env.example .env            # then put your real key in it
```

`.env` is gitignored — the key never leaves your machine.

Register the server with your MCP client via `.mcp.json`:

```json
{
  "mcpServers": {
    "model-router": {
      "command": "<path>\\.venv\\Scripts\\python.exe",
      "args": ["<path>\\server.py"]
    }
  }
}
```

Adjust both paths to wherever you cloned this.

## Files

| File | Role |
| --- | --- |
| `server.py` | MCP server, tool definition, model execution, CSV logging |
| `router.py` | The classifier: model registry, force tags, signal scoring, length fallback |
| `test_router.py` | Offline smoke test — no API calls, no cost |
| `.mcp.json` | Client registration |

## Requirements

`mcp` · `anthropic` · `python-dotenv`