hyperroute-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperroute_mcp-0.1.0/LICENSE +21 -0
- hyperroute_mcp-0.1.0/PKG-INFO +224 -0
- hyperroute_mcp-0.1.0/README.md +195 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/__init__.py +13 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/__main__.py +13 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/client.py +188 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/config.py +58 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/native.py +161 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/server.py +622 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp/tokenstore.py +58 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/PKG-INFO +224 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/SOURCES.txt +19 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/dependency_links.txt +1 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/entry_points.txt +2 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/requires.txt +7 -0
- hyperroute_mcp-0.1.0/hyperroute_mcp.egg-info/top_level.txt +1 -0
- hyperroute_mcp-0.1.0/pyproject.toml +57 -0
- hyperroute_mcp-0.1.0/setup.cfg +4 -0
- hyperroute_mcp-0.1.0/tests/test_client.py +104 -0
- hyperroute_mcp-0.1.0/tests/test_native.py +97 -0
- hyperroute_mcp-0.1.0/tests/test_server.py +188 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HyperRouteAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperroute-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for HyperRoute — routes each task to the best external tool and runs it for your coordinator agent.
|
|
5
|
+
Author: HyperRouteAI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://hyperroute.io
|
|
8
|
+
Project-URL: Repository, https://github.com/HyperRouteAI/hyperroute-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/HyperRouteAI/hyperroute-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,hyperroute,agents,tool-routing,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp>=1.2.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# hyperroute-mcp
|
|
31
|
+
|
|
32
|
+
The official [Model Context Protocol](https://modelcontextprotocol.io) server for
|
|
33
|
+
**[HyperRoute](https://hyperroute.io)**.
|
|
34
|
+
|
|
35
|
+
HyperRoute is a router for AI agents. Give it a task and it picks the best external tool for
|
|
36
|
+
*that* task — measured, not advertised — then runs the tool for you with your own key held
|
|
37
|
+
server-side, and learns from how it went. This MCP server is how a coordinator agent (Claude
|
|
38
|
+
Code, Codex, Goose, Cursor, LangGraph, …) drives it:
|
|
39
|
+
|
|
40
|
+
> recommend → onboard a key → execute the tool server-side → report the outcome
|
|
41
|
+
|
|
42
|
+
It talks to the router only over its public HTTP API and holds no product logic of its own.
|
|
43
|
+
|
|
44
|
+
## Why route at all
|
|
45
|
+
|
|
46
|
+
An agent with 100 tools bolted on has a context problem and a quality problem. HyperRoute
|
|
47
|
+
replaces both with one verb: your agent learns `recommend`, and HyperRoute decides which of
|
|
48
|
+
hundreds of tools actually answers this task, whether you can already do it better yourself,
|
|
49
|
+
and what it will cost.
|
|
50
|
+
|
|
51
|
+
- **Measured, not advertised.** Every capability score is backed by real graded probes you can
|
|
52
|
+
inspect (`describe(tool_id, ["evidence"])`).
|
|
53
|
+
- **Your keys never leave the server.** You connect a key once; HyperRoute runs the tool with it
|
|
54
|
+
and returns only the result. The key is never sent to your agent, never logged.
|
|
55
|
+
- **It tells you when NOT to route.** If nothing beats what your coordinator already does, the
|
|
56
|
+
verdict is `use_native` — do it yourself. That only works if the server knows which coordinator
|
|
57
|
+
it runs inside; see [Declaring your coordinator](#declaring-your-coordinator).
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
Not on PyPI yet — install from source:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/HyperRouteAI/hyperroute-mcp
|
|
65
|
+
cd hyperroute-mcp
|
|
66
|
+
python -m venv .venv && source .venv/bin/activate
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or straight from GitHub, without cloning:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install git+https://github.com/HyperRouteAI/hyperroute-mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Either way you get the `hyperroute-mcp` command on your PATH. Requires Python ≥ 3.10.
|
|
77
|
+
|
|
78
|
+
## Add it to your coordinator
|
|
79
|
+
|
|
80
|
+
**Claude Code**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
claude mcp add hyperroute -- hyperroute-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If you installed into a venv, `hyperroute-mcp` is only on your PATH while that venv is active — MCP
|
|
87
|
+
clients launch the server themselves, outside your shell. Give them the absolute path instead
|
|
88
|
+
(`/path/to/.venv/bin/hyperroute-mcp`), or install with [`pipx`](https://pipx.pypa.io) so the command
|
|
89
|
+
is always available.
|
|
90
|
+
|
|
91
|
+
**OpenCode** — copy [`opencode.json`](opencode.json) into your project. OpenCode is bring-your-own-model,
|
|
92
|
+
so the server can't infer what you're running from the client name alone: set `HYPERROUTE_COORDINATOR`
|
|
93
|
+
(or `HYPERROUTE_NATIVE_TOOLS`) to match the model you actually point it at, or HyperRoute will have no
|
|
94
|
+
baseline for you. [`AGENTS.md`](AGENTS.md) carries the operating loop and the methodology — drop it in
|
|
95
|
+
so the agent can both act correctly and explain how the routing works.
|
|
96
|
+
|
|
97
|
+
**Any MCP client** (`mcp.json` / `claude_desktop_config.json` / equivalent):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"hyperroute": {
|
|
103
|
+
"command": "hyperroute-mcp",
|
|
104
|
+
"env": { "HYPERROUTE_BASE_URL": "https://hyperroute.io" }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then just ask: *"Use HyperRoute to find the best tool for searching recent papers, connect my
|
|
111
|
+
key, and run it."* The agent calls `recommend` → `connect_info` → `onboard` → `execute` on its
|
|
112
|
+
own.
|
|
113
|
+
|
|
114
|
+
## Authenticate once
|
|
115
|
+
|
|
116
|
+
`recommend` and browsing are public — no account. Connecting keys and running tools need one.
|
|
117
|
+
|
|
118
|
+
Preferred: mint a personal access token at [hyperroute.io](https://hyperroute.io) and hand it to
|
|
119
|
+
the `use_token` tool (or set `HYPERROUTE_API_KEY`). Your password never enters the conversation.
|
|
120
|
+
|
|
121
|
+
The token is then **cached on disk** (`~/.hyperroute/token.json`, mode `0600`, keyed by router
|
|
122
|
+
URL), so every new MCP session restores your login silently. You are asked to authenticate again
|
|
123
|
+
only if the router invalidates the token. A full inline `register` → `verify` email-code flow is
|
|
124
|
+
also available for headless use.
|
|
125
|
+
|
|
126
|
+
## Declaring your coordinator
|
|
127
|
+
|
|
128
|
+
HyperRoute compares external tools against *what you can already do*. That baseline is the set of
|
|
129
|
+
coordinators that are free to you — and it is **empty by default**, because the router never
|
|
130
|
+
assumes you have one. An MCP server that does not declare itself gets an external tool
|
|
131
|
+
recommended for every task, including tasks the coordinator does better itself.
|
|
132
|
+
|
|
133
|
+
This server declares it for you. It reads the MCP client identity your coordinator sends on
|
|
134
|
+
connect and maps it to the coordinator HyperRoute models (`claude-code` → `claude_code`, …).
|
|
135
|
+
Check what it resolved with the `session_info` tool: if `native_tools` is empty, set it yourself.
|
|
136
|
+
|
|
137
|
+
```jsonc
|
|
138
|
+
"env": {
|
|
139
|
+
"HYPERROUTE_COORDINATOR": "claude_code", // or codex / cursor / goose / …; "none" disables
|
|
140
|
+
"HYPERROUTE_HELD": "anthropic_max_5x" // subscriptions you already pay for → priced at $0
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`HYPERROUTE_NATIVE_TOOLS` takes exact tool ids if you want to pin one model variant instead of the
|
|
145
|
+
whole product family.
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
| Variable | Default | Meaning |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `HYPERROUTE_BASE_URL` | `https://hyperroute.io` | Which router to talk to. Override to point at a different instance. |
|
|
152
|
+
| `HYPERROUTE_API_KEY` | — | `hyr_…` token to start already logged in. Externally managed: used, never cached. |
|
|
153
|
+
| `HYPERROUTE_TIMEOUT` | `30` | Per-request timeout, seconds. |
|
|
154
|
+
| `HYPERROUTE_TOKEN_FILE` | `~/.hyperroute/token.json` | Where the cached login lives. |
|
|
155
|
+
| `HYPERROUTE_COORDINATOR` | auto-detect | Which coordinator this runs inside; `none` disables the declaration. |
|
|
156
|
+
| `HYPERROUTE_NATIVE_TOOLS` | — | Exact coordinator tool ids, overriding detection. |
|
|
157
|
+
| `HYPERROUTE_HELD` | — | Comma-separated plan groups you hold, e.g. `anthropic_max_5x`. |
|
|
158
|
+
|
|
159
|
+
## Tools
|
|
160
|
+
|
|
161
|
+
| Tool | What it does |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `session_info` | Base URL, login state, and the coordinator this server declares. Call first. |
|
|
164
|
+
| `health` | Router readiness + the loaded model bundle. |
|
|
165
|
+
| `recommend` | **The main verb.** Task → ranked tools as a compact table + how to act. Public. |
|
|
166
|
+
| `describe` | Pull ONE tool's depth on demand: `about` · `price` · `facets` · `evidence`. |
|
|
167
|
+
| `facets_catalog` | Every facet a tool can be judged on, with defaults. Fetch once. |
|
|
168
|
+
| `get_preferences` / `set_preferences` | Your standing constraints, applied to every future route. |
|
|
169
|
+
| `connect_info` | A tool's onboarding process: signup URL, steps, whether you're connected. |
|
|
170
|
+
| `onboard` | Save + test one tool API key under your account. Stored encrypted, reused forever. |
|
|
171
|
+
| `list_credentials` | Your connected tools (keys masked). |
|
|
172
|
+
| `execute` | Run the chosen tool server-side with your held key; returns only the result. |
|
|
173
|
+
| `fetch_result` | Page through a result too large to inline. |
|
|
174
|
+
| `report_outcome` | Per-call feedback — the signal that sharpens future routing. |
|
|
175
|
+
| `report_narrative` | Open-ended feedback about a whole run. |
|
|
176
|
+
| `console` | Human-readable management view: history, tools, keys, stats. |
|
|
177
|
+
| `use_token` / `register` / `verify` / `login` / `login_link` / `verify_login` / `forgot_password` / `whoami` | Account lifecycle. |
|
|
178
|
+
| `hyperfeed` / `hyperfeed_digest` / `hyperfeed_subscribe` / `hyperfeed_react` | HyperFeed: curated agentic-AI news, agent releases, and SF events — plus your agent's personalized daily brief. |
|
|
179
|
+
|
|
180
|
+
### The wire is deliberately lean
|
|
181
|
+
|
|
182
|
+
`recommend` answers with a compact table, not a catalog dump:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
session: s-6d6c5a95f9f84d9a
|
|
186
|
+
verdict: interpose
|
|
187
|
+
refine: freshness, cited_references, source_quality
|
|
188
|
+
|
|
189
|
+
tool name price use why
|
|
190
|
+
→ opencitations OpenCitations Index free ready highest-ranked: capability 0.81 …
|
|
191
|
+
semantic_scholar Semantic Scholar Graph API free needs_key lower capability (0.75 vs 0.81).
|
|
192
|
+
|
|
193
|
+
confidence: med (on the pick)
|
|
194
|
+
act: execute('opencitations', <query>)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Everything else — descriptions, per-plan pricing, per-facet breakdowns, the probe evidence behind
|
|
198
|
+
a score — is pulled for the one tool that matters via `describe`. That keeps a route roughly an
|
|
199
|
+
order of magnitude cheaper in tokens than shipping the full object on every call.
|
|
200
|
+
|
|
201
|
+
The `use` column is the whole auth story: `ready` (run it) · `needs_key` (connect first) ·
|
|
202
|
+
`native` (do it yourself) · `soon` (not runnable server-side yet).
|
|
203
|
+
|
|
204
|
+
### Two-pass refinement
|
|
205
|
+
|
|
206
|
+
Pass 1 always returns a usable ranking. The `refine:` line names the unset preferences that would
|
|
207
|
+
reorder *these* candidates; fill the relevant ones and call `recommend` again with `facets` for a
|
|
208
|
+
personalized result. Durable constraints (GDPR, a budget cap) belong in `set_preferences`
|
|
209
|
+
instead — stored once, applied to every future route.
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install -e ".[dev]"
|
|
215
|
+
pytest
|
|
216
|
+
ruff check .
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The suite is fully offline — the router is faked, so no network and no real account are touched.
|
|
220
|
+
Set `HYPERROUTE_BASE_URL` to try it against a different router instance.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# hyperroute-mcp
|
|
2
|
+
|
|
3
|
+
The official [Model Context Protocol](https://modelcontextprotocol.io) server for
|
|
4
|
+
**[HyperRoute](https://hyperroute.io)**.
|
|
5
|
+
|
|
6
|
+
HyperRoute is a router for AI agents. Give it a task and it picks the best external tool for
|
|
7
|
+
*that* task — measured, not advertised — then runs the tool for you with your own key held
|
|
8
|
+
server-side, and learns from how it went. This MCP server is how a coordinator agent (Claude
|
|
9
|
+
Code, Codex, Goose, Cursor, LangGraph, …) drives it:
|
|
10
|
+
|
|
11
|
+
> recommend → onboard a key → execute the tool server-side → report the outcome
|
|
12
|
+
|
|
13
|
+
It talks to the router only over its public HTTP API and holds no product logic of its own.
|
|
14
|
+
|
|
15
|
+
## Why route at all
|
|
16
|
+
|
|
17
|
+
An agent with 100 tools bolted on has a context problem and a quality problem. HyperRoute
|
|
18
|
+
replaces both with one verb: your agent learns `recommend`, and HyperRoute decides which of
|
|
19
|
+
hundreds of tools actually answers this task, whether you can already do it better yourself,
|
|
20
|
+
and what it will cost.
|
|
21
|
+
|
|
22
|
+
- **Measured, not advertised.** Every capability score is backed by real graded probes you can
|
|
23
|
+
inspect (`describe(tool_id, ["evidence"])`).
|
|
24
|
+
- **Your keys never leave the server.** You connect a key once; HyperRoute runs the tool with it
|
|
25
|
+
and returns only the result. The key is never sent to your agent, never logged.
|
|
26
|
+
- **It tells you when NOT to route.** If nothing beats what your coordinator already does, the
|
|
27
|
+
verdict is `use_native` — do it yourself. That only works if the server knows which coordinator
|
|
28
|
+
it runs inside; see [Declaring your coordinator](#declaring-your-coordinator).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Not on PyPI yet — install from source:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/HyperRouteAI/hyperroute-mcp
|
|
36
|
+
cd hyperroute-mcp
|
|
37
|
+
python -m venv .venv && source .venv/bin/activate
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or straight from GitHub, without cloning:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install git+https://github.com/HyperRouteAI/hyperroute-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Either way you get the `hyperroute-mcp` command on your PATH. Requires Python ≥ 3.10.
|
|
48
|
+
|
|
49
|
+
## Add it to your coordinator
|
|
50
|
+
|
|
51
|
+
**Claude Code**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
claude mcp add hyperroute -- hyperroute-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you installed into a venv, `hyperroute-mcp` is only on your PATH while that venv is active — MCP
|
|
58
|
+
clients launch the server themselves, outside your shell. Give them the absolute path instead
|
|
59
|
+
(`/path/to/.venv/bin/hyperroute-mcp`), or install with [`pipx`](https://pipx.pypa.io) so the command
|
|
60
|
+
is always available.
|
|
61
|
+
|
|
62
|
+
**OpenCode** — copy [`opencode.json`](opencode.json) into your project. OpenCode is bring-your-own-model,
|
|
63
|
+
so the server can't infer what you're running from the client name alone: set `HYPERROUTE_COORDINATOR`
|
|
64
|
+
(or `HYPERROUTE_NATIVE_TOOLS`) to match the model you actually point it at, or HyperRoute will have no
|
|
65
|
+
baseline for you. [`AGENTS.md`](AGENTS.md) carries the operating loop and the methodology — drop it in
|
|
66
|
+
so the agent can both act correctly and explain how the routing works.
|
|
67
|
+
|
|
68
|
+
**Any MCP client** (`mcp.json` / `claude_desktop_config.json` / equivalent):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"hyperroute": {
|
|
74
|
+
"command": "hyperroute-mcp",
|
|
75
|
+
"env": { "HYPERROUTE_BASE_URL": "https://hyperroute.io" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then just ask: *"Use HyperRoute to find the best tool for searching recent papers, connect my
|
|
82
|
+
key, and run it."* The agent calls `recommend` → `connect_info` → `onboard` → `execute` on its
|
|
83
|
+
own.
|
|
84
|
+
|
|
85
|
+
## Authenticate once
|
|
86
|
+
|
|
87
|
+
`recommend` and browsing are public — no account. Connecting keys and running tools need one.
|
|
88
|
+
|
|
89
|
+
Preferred: mint a personal access token at [hyperroute.io](https://hyperroute.io) and hand it to
|
|
90
|
+
the `use_token` tool (or set `HYPERROUTE_API_KEY`). Your password never enters the conversation.
|
|
91
|
+
|
|
92
|
+
The token is then **cached on disk** (`~/.hyperroute/token.json`, mode `0600`, keyed by router
|
|
93
|
+
URL), so every new MCP session restores your login silently. You are asked to authenticate again
|
|
94
|
+
only if the router invalidates the token. A full inline `register` → `verify` email-code flow is
|
|
95
|
+
also available for headless use.
|
|
96
|
+
|
|
97
|
+
## Declaring your coordinator
|
|
98
|
+
|
|
99
|
+
HyperRoute compares external tools against *what you can already do*. That baseline is the set of
|
|
100
|
+
coordinators that are free to you — and it is **empty by default**, because the router never
|
|
101
|
+
assumes you have one. An MCP server that does not declare itself gets an external tool
|
|
102
|
+
recommended for every task, including tasks the coordinator does better itself.
|
|
103
|
+
|
|
104
|
+
This server declares it for you. It reads the MCP client identity your coordinator sends on
|
|
105
|
+
connect and maps it to the coordinator HyperRoute models (`claude-code` → `claude_code`, …).
|
|
106
|
+
Check what it resolved with the `session_info` tool: if `native_tools` is empty, set it yourself.
|
|
107
|
+
|
|
108
|
+
```jsonc
|
|
109
|
+
"env": {
|
|
110
|
+
"HYPERROUTE_COORDINATOR": "claude_code", // or codex / cursor / goose / …; "none" disables
|
|
111
|
+
"HYPERROUTE_HELD": "anthropic_max_5x" // subscriptions you already pay for → priced at $0
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`HYPERROUTE_NATIVE_TOOLS` takes exact tool ids if you want to pin one model variant instead of the
|
|
116
|
+
whole product family.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
| Variable | Default | Meaning |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `HYPERROUTE_BASE_URL` | `https://hyperroute.io` | Which router to talk to. Override to point at a different instance. |
|
|
123
|
+
| `HYPERROUTE_API_KEY` | — | `hyr_…` token to start already logged in. Externally managed: used, never cached. |
|
|
124
|
+
| `HYPERROUTE_TIMEOUT` | `30` | Per-request timeout, seconds. |
|
|
125
|
+
| `HYPERROUTE_TOKEN_FILE` | `~/.hyperroute/token.json` | Where the cached login lives. |
|
|
126
|
+
| `HYPERROUTE_COORDINATOR` | auto-detect | Which coordinator this runs inside; `none` disables the declaration. |
|
|
127
|
+
| `HYPERROUTE_NATIVE_TOOLS` | — | Exact coordinator tool ids, overriding detection. |
|
|
128
|
+
| `HYPERROUTE_HELD` | — | Comma-separated plan groups you hold, e.g. `anthropic_max_5x`. |
|
|
129
|
+
|
|
130
|
+
## Tools
|
|
131
|
+
|
|
132
|
+
| Tool | What it does |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `session_info` | Base URL, login state, and the coordinator this server declares. Call first. |
|
|
135
|
+
| `health` | Router readiness + the loaded model bundle. |
|
|
136
|
+
| `recommend` | **The main verb.** Task → ranked tools as a compact table + how to act. Public. |
|
|
137
|
+
| `describe` | Pull ONE tool's depth on demand: `about` · `price` · `facets` · `evidence`. |
|
|
138
|
+
| `facets_catalog` | Every facet a tool can be judged on, with defaults. Fetch once. |
|
|
139
|
+
| `get_preferences` / `set_preferences` | Your standing constraints, applied to every future route. |
|
|
140
|
+
| `connect_info` | A tool's onboarding process: signup URL, steps, whether you're connected. |
|
|
141
|
+
| `onboard` | Save + test one tool API key under your account. Stored encrypted, reused forever. |
|
|
142
|
+
| `list_credentials` | Your connected tools (keys masked). |
|
|
143
|
+
| `execute` | Run the chosen tool server-side with your held key; returns only the result. |
|
|
144
|
+
| `fetch_result` | Page through a result too large to inline. |
|
|
145
|
+
| `report_outcome` | Per-call feedback — the signal that sharpens future routing. |
|
|
146
|
+
| `report_narrative` | Open-ended feedback about a whole run. |
|
|
147
|
+
| `console` | Human-readable management view: history, tools, keys, stats. |
|
|
148
|
+
| `use_token` / `register` / `verify` / `login` / `login_link` / `verify_login` / `forgot_password` / `whoami` | Account lifecycle. |
|
|
149
|
+
| `hyperfeed` / `hyperfeed_digest` / `hyperfeed_subscribe` / `hyperfeed_react` | HyperFeed: curated agentic-AI news, agent releases, and SF events — plus your agent's personalized daily brief. |
|
|
150
|
+
|
|
151
|
+
### The wire is deliberately lean
|
|
152
|
+
|
|
153
|
+
`recommend` answers with a compact table, not a catalog dump:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
session: s-6d6c5a95f9f84d9a
|
|
157
|
+
verdict: interpose
|
|
158
|
+
refine: freshness, cited_references, source_quality
|
|
159
|
+
|
|
160
|
+
tool name price use why
|
|
161
|
+
→ opencitations OpenCitations Index free ready highest-ranked: capability 0.81 …
|
|
162
|
+
semantic_scholar Semantic Scholar Graph API free needs_key lower capability (0.75 vs 0.81).
|
|
163
|
+
|
|
164
|
+
confidence: med (on the pick)
|
|
165
|
+
act: execute('opencitations', <query>)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Everything else — descriptions, per-plan pricing, per-facet breakdowns, the probe evidence behind
|
|
169
|
+
a score — is pulled for the one tool that matters via `describe`. That keeps a route roughly an
|
|
170
|
+
order of magnitude cheaper in tokens than shipping the full object on every call.
|
|
171
|
+
|
|
172
|
+
The `use` column is the whole auth story: `ready` (run it) · `needs_key` (connect first) ·
|
|
173
|
+
`native` (do it yourself) · `soon` (not runnable server-side yet).
|
|
174
|
+
|
|
175
|
+
### Two-pass refinement
|
|
176
|
+
|
|
177
|
+
Pass 1 always returns a usable ranking. The `refine:` line names the unset preferences that would
|
|
178
|
+
reorder *these* candidates; fill the relevant ones and call `recommend` again with `facets` for a
|
|
179
|
+
personalized result. Durable constraints (GDPR, a budget cap) belong in `set_preferences`
|
|
180
|
+
instead — stored once, applied to every future route.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install -e ".[dev]"
|
|
186
|
+
pytest
|
|
187
|
+
ruff check .
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The suite is fully offline — the router is faked, so no network and no real account are touched.
|
|
191
|
+
Set `HYPERROUTE_BASE_URL` to try it against a different router instance.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""HyperRoute MCP — a Model Context Protocol server that exposes the HyperRoute router
|
|
2
|
+
(register, login, recommend, describe, onboard, execute, report_outcome, HyperFeed, …) as MCP
|
|
3
|
+
tools, so a coordinator agent (Claude Code, Codex, Goose, Cursor, …) can drive the whole product
|
|
4
|
+
end-to-end in one conversation.
|
|
5
|
+
|
|
6
|
+
It talks to the router only over its public HTTP API and holds no product logic of its own.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .server import mcp
|
|
12
|
+
|
|
13
|
+
__all__ = ["mcp", "__version__"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""`python -m hyperroute_mcp` — run the HyperRoute MCP server over stdio (the transport MCP
|
|
2
|
+
clients launch it with). Point it at a router with HYPERROUTE_BASE_URL; see config.py for the
|
|
3
|
+
full set of environment variables."""
|
|
4
|
+
|
|
5
|
+
from .server import mcp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
mcp.run() # stdio transport by default
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Thin async HTTP client over the HyperRoute router's public API.
|
|
2
|
+
|
|
3
|
+
One method per endpoint this server touches. No product logic lives here — the client only
|
|
4
|
+
knows how to (a) carry the session bearer token, and (b) turn the router's responses into
|
|
5
|
+
plain dicts, surfacing its structured `detail` error bodies instead of raising, so the MCP
|
|
6
|
+
tools can hand a coordinator an actionable object (e.g. `needs_onboard` + signup instructions)
|
|
7
|
+
rather than a stack trace.
|
|
8
|
+
|
|
9
|
+
One endpoint answers in text rather than JSON: `recommend` with `format=text` returns the
|
|
10
|
+
compact tabular coordinator wire, so `_request(..., as_text=True)` returns a `str`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Session:
|
|
21
|
+
"""The MCP process's login state: the active bearer token and the resolved user_id.
|
|
22
|
+
|
|
23
|
+
Mutated in place by `register` / `login` so every later recommend/onboard/execute call
|
|
24
|
+
is authenticated as the same user for the life of the MCP connection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
28
|
+
self.api_key = api_key
|
|
29
|
+
self.user_id: str | None = None
|
|
30
|
+
self.email: str | None = None
|
|
31
|
+
self.env_managed = False # token came from HYPERROUTE_API_KEY — don't read/write the cache
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def logged_in(self) -> bool:
|
|
35
|
+
return bool(self.api_key)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _unwrap(r: httpx.Response) -> Any:
|
|
39
|
+
"""Router response -> dict. Success bodies pass through; error bodies are normalized to
|
|
40
|
+
`{"_error": True, "_http_status": <code>, ...}` with the server's `detail` merged in."""
|
|
41
|
+
try:
|
|
42
|
+
body = r.json()
|
|
43
|
+
except ValueError:
|
|
44
|
+
body = {"raw": r.text}
|
|
45
|
+
if r.is_success:
|
|
46
|
+
return body
|
|
47
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
48
|
+
base = {"_error": True, "_http_status": r.status_code}
|
|
49
|
+
if isinstance(detail, dict):
|
|
50
|
+
return {**base, **detail}
|
|
51
|
+
if detail is not None:
|
|
52
|
+
return {**base, "message": detail}
|
|
53
|
+
return {**base, "body": body}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HyperRouteClient:
|
|
57
|
+
"""Stateless-per-call HTTP client bound to a mutable `Session` for auth."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, base_url: str, session: Session, timeout: float = 30.0) -> None:
|
|
60
|
+
self.base_url = base_url.rstrip("/")
|
|
61
|
+
self.session = session
|
|
62
|
+
self._timeout = timeout
|
|
63
|
+
|
|
64
|
+
def _headers(self, auth: bool) -> dict[str, str]:
|
|
65
|
+
h = {"content-type": "application/json"}
|
|
66
|
+
if auth and self.session.api_key:
|
|
67
|
+
h["authorization"] = f"Bearer {self.session.api_key}"
|
|
68
|
+
return h
|
|
69
|
+
|
|
70
|
+
async def _request(self, method: str, path: str, *, auth: bool = False,
|
|
71
|
+
json: dict | None = None, params: dict | None = None,
|
|
72
|
+
as_text: bool = False) -> Any:
|
|
73
|
+
if json is not None: # drop None fields so router defaults apply
|
|
74
|
+
json = {k: v for k, v in json.items() if v is not None}
|
|
75
|
+
if params is not None:
|
|
76
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
77
|
+
try:
|
|
78
|
+
async with httpx.AsyncClient(timeout=self._timeout) as c:
|
|
79
|
+
r = await c.request(method, f"{self.base_url}{path}",
|
|
80
|
+
headers=self._headers(auth), json=json, params=params)
|
|
81
|
+
except httpx.RequestError as e:
|
|
82
|
+
return {"_error": True, "message": f"could not reach router at {self.base_url}: {e}"}
|
|
83
|
+
if as_text and r.is_success:
|
|
84
|
+
return r.text
|
|
85
|
+
return _unwrap(r)
|
|
86
|
+
|
|
87
|
+
# -- surfaces -----------------------------------------------------------
|
|
88
|
+
async def health(self) -> Any:
|
|
89
|
+
return await self._request("GET", "/health")
|
|
90
|
+
|
|
91
|
+
async def register(self, email: str, password: str, display_name: str | None) -> Any:
|
|
92
|
+
return await self._request("POST", "/auth/register",
|
|
93
|
+
json={"email": email, "password": password,
|
|
94
|
+
"display_name": display_name})
|
|
95
|
+
|
|
96
|
+
async def verify(self, email: str, code: str) -> Any:
|
|
97
|
+
return await self._request("POST", "/auth/verify", json={"email": email, "code": code})
|
|
98
|
+
|
|
99
|
+
async def login(self, email: str, password: str) -> Any:
|
|
100
|
+
return await self._request("POST", "/auth/login", json={"email": email, "password": password})
|
|
101
|
+
|
|
102
|
+
async def request_login_code(self, email: str) -> Any:
|
|
103
|
+
return await self._request("POST", "/auth/login/request-code", json={"email": email})
|
|
104
|
+
|
|
105
|
+
async def login_with_code(self, email: str, code: str) -> Any:
|
|
106
|
+
return await self._request("POST", "/auth/login/code", json={"email": email, "code": code})
|
|
107
|
+
|
|
108
|
+
async def forgot_password(self, email: str) -> Any:
|
|
109
|
+
return await self._request("POST", "/auth/password/forgot", json={"email": email})
|
|
110
|
+
|
|
111
|
+
async def whoami(self) -> Any:
|
|
112
|
+
return await self._request("POST", "/auth/whoami", auth=True)
|
|
113
|
+
|
|
114
|
+
async def recommend_text(self, payload: dict) -> Any:
|
|
115
|
+
"""The coordinator wire: `detail=min` + `format=text` → compact tabular text (a `str`),
|
|
116
|
+
or the usual error dict. Depth is pulled per-tool afterwards via `describe`."""
|
|
117
|
+
return await self._request("POST", "/recommend", auth=True,
|
|
118
|
+
json={**payload, "detail": "min", "format": "text"},
|
|
119
|
+
as_text=True)
|
|
120
|
+
|
|
121
|
+
async def describe(self, payload: dict) -> Any:
|
|
122
|
+
return await self._request("POST", "/describe", auth=True, json=payload)
|
|
123
|
+
|
|
124
|
+
async def catalog(self) -> Any:
|
|
125
|
+
"""The router's tool catalog (id + kind + auth + capabilities). Public; used to resolve
|
|
126
|
+
which coordinator ids exist before declaring the native baseline."""
|
|
127
|
+
return await self._request("GET", "/console", params={"view": "tools", "format": "json"})
|
|
128
|
+
|
|
129
|
+
async def onboard_info(self, tool_id: str) -> Any:
|
|
130
|
+
return await self._request("GET", f"/onboard/{tool_id}", auth=True)
|
|
131
|
+
|
|
132
|
+
async def onboard(self, tool_id: str, api_key: str, label: str | None) -> Any:
|
|
133
|
+
return await self._request("POST", "/onboard", auth=True,
|
|
134
|
+
json={"tool_id": tool_id, "api_key": api_key, "label": label})
|
|
135
|
+
|
|
136
|
+
async def execute(self, tool_id: str, query: str) -> Any:
|
|
137
|
+
return await self._request("POST", "/execute", auth=True,
|
|
138
|
+
json={"tool_id": tool_id, "query": query})
|
|
139
|
+
|
|
140
|
+
async def read_result(self, ref: str, op: str, offset: int, limit: int,
|
|
141
|
+
path: list | None, query: str | None) -> Any:
|
|
142
|
+
return await self._request("POST", f"/result/{ref}/read", auth=True,
|
|
143
|
+
json={"op": op, "offset": offset, "limit": limit,
|
|
144
|
+
"path": path, "query": query})
|
|
145
|
+
|
|
146
|
+
async def list_credentials(self, user_id: str) -> Any:
|
|
147
|
+
return await self._request("GET", "/credentials", auth=True, params={"user_id": user_id})
|
|
148
|
+
|
|
149
|
+
async def report_outcome(self, payload: dict) -> Any:
|
|
150
|
+
return await self._request("POST", "/report_outcome", json=payload)
|
|
151
|
+
|
|
152
|
+
async def report_narrative(self, payload: dict) -> Any:
|
|
153
|
+
return await self._request("POST", "/report_narrative", json=payload)
|
|
154
|
+
|
|
155
|
+
async def console(self, view: str, user_id: str) -> Any:
|
|
156
|
+
return await self._request("GET", "/console", auth=True,
|
|
157
|
+
params={"view": view, "format": "json", "user_id": user_id})
|
|
158
|
+
|
|
159
|
+
async def facets_catalog(self) -> Any:
|
|
160
|
+
return await self._request("GET", "/facets/catalog")
|
|
161
|
+
|
|
162
|
+
async def get_preferences(self, project_id: str | None = None) -> Any:
|
|
163
|
+
return await self._request("GET", "/preferences", auth=True,
|
|
164
|
+
params={"project_id": project_id})
|
|
165
|
+
|
|
166
|
+
async def set_preferences(self, facets: dict, project_id: str | None = None) -> Any:
|
|
167
|
+
return await self._request("PUT", "/preferences", auth=True,
|
|
168
|
+
json={"facets": facets, "project_id": project_id})
|
|
169
|
+
|
|
170
|
+
# -- HyperFeed ----------------------------------------------------------
|
|
171
|
+
async def feed(self, stream: str | None = None, since: str | None = None,
|
|
172
|
+
limit: int = 50) -> Any:
|
|
173
|
+
return await self._request("GET", "/feed",
|
|
174
|
+
params={"stream": stream, "since": since, "limit": limit})
|
|
175
|
+
|
|
176
|
+
async def feed_streams(self) -> Any:
|
|
177
|
+
return await self._request("GET", "/feed/streams")
|
|
178
|
+
|
|
179
|
+
async def feed_digest(self, since: str | None = None, limit: int = 20) -> Any:
|
|
180
|
+
return await self._request("GET", "/feed/digest", auth=True,
|
|
181
|
+
params={"since": since, "limit": limit})
|
|
182
|
+
|
|
183
|
+
async def feed_subscribe(self, payload: dict) -> Any:
|
|
184
|
+
return await self._request("POST", "/feed/subscribe", auth=True, json=payload)
|
|
185
|
+
|
|
186
|
+
async def feed_react(self, item_id: str, action: str) -> Any:
|
|
187
|
+
return await self._request("POST", "/feed/react", auth=True,
|
|
188
|
+
json={"item_id": item_id, "action": action})
|