rtings-mcp 0.2.1__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.
Files changed (50) hide show
  1. rtings_mcp-0.2.1/.github/workflows/publish.yml +75 -0
  2. rtings_mcp-0.2.1/.gitignore +41 -0
  3. rtings_mcp-0.2.1/LICENSE +21 -0
  4. rtings_mcp-0.2.1/PKG-INFO +240 -0
  5. rtings_mcp-0.2.1/README.md +211 -0
  6. rtings_mcp-0.2.1/RECON.md +1608 -0
  7. rtings_mcp-0.2.1/SPEC.md +1658 -0
  8. rtings_mcp-0.2.1/pyproject.toml +85 -0
  9. rtings_mcp-0.2.1/src/rtings_mcp/__init__.py +14 -0
  10. rtings_mcp-0.2.1/src/rtings_mcp/api.py +242 -0
  11. rtings_mcp-0.2.1/src/rtings_mcp/auth.py +737 -0
  12. rtings_mcp-0.2.1/src/rtings_mcp/auth_tools.py +526 -0
  13. rtings_mcp-0.2.1/src/rtings_mcp/browser_auth.py +397 -0
  14. rtings_mcp-0.2.1/src/rtings_mcp/cache.py +602 -0
  15. rtings_mcp-0.2.1/src/rtings_mcp/cli.py +343 -0
  16. rtings_mcp-0.2.1/src/rtings_mcp/config.py +222 -0
  17. rtings_mcp-0.2.1/src/rtings_mcp/context.py +58 -0
  18. rtings_mcp-0.2.1/src/rtings_mcp/envelope.py +220 -0
  19. rtings_mcp-0.2.1/src/rtings_mcp/errors.py +88 -0
  20. rtings_mcp-0.2.1/src/rtings_mcp/fsutil.py +131 -0
  21. rtings_mcp-0.2.1/src/rtings_mcp/htmlprobe.py +390 -0
  22. rtings_mcp-0.2.1/src/rtings_mcp/http.py +605 -0
  23. rtings_mcp-0.2.1/src/rtings_mcp/models.py +544 -0
  24. rtings_mcp-0.2.1/src/rtings_mcp/normalize.py +546 -0
  25. rtings_mcp-0.2.1/src/rtings_mcp/observations.py +276 -0
  26. rtings_mcp-0.2.1/src/rtings_mcp/ratelimit.py +197 -0
  27. rtings_mcp-0.2.1/src/rtings_mcp/repository.py +2281 -0
  28. rtings_mcp-0.2.1/src/rtings_mcp/schema.py +473 -0
  29. rtings_mcp-0.2.1/src/rtings_mcp/server.py +547 -0
  30. rtings_mcp-0.2.1/src/rtings_mcp/services.py +3200 -0
  31. rtings_mcp-0.2.1/tests/conftest.py +75 -0
  32. rtings_mcp-0.2.1/tests/fixtures/column_options_min.json +211 -0
  33. rtings_mcp-0.2.1/tests/fixtures/recommendation_static_min.html +42 -0
  34. rtings_mcp-0.2.1/tests/test_auth.py +566 -0
  35. rtings_mcp-0.2.1/tests/test_auth_tools.py +387 -0
  36. rtings_mcp-0.2.1/tests/test_cache.py +221 -0
  37. rtings_mcp-0.2.1/tests/test_envelope.py +153 -0
  38. rtings_mcp-0.2.1/tests/test_fsutil.py +101 -0
  39. rtings_mcp-0.2.1/tests/test_htmlprobe.py +215 -0
  40. rtings_mcp-0.2.1/tests/test_http.py +251 -0
  41. rtings_mcp-0.2.1/tests/test_live.py +213 -0
  42. rtings_mcp-0.2.1/tests/test_normalize.py +382 -0
  43. rtings_mcp-0.2.1/tests/test_observations.py +172 -0
  44. rtings_mcp-0.2.1/tests/test_ratelimit.py +114 -0
  45. rtings_mcp-0.2.1/tests/test_repository.py +280 -0
  46. rtings_mcp-0.2.1/tests/test_schema.py +80 -0
  47. rtings_mcp-0.2.1/tests/test_server.py +204 -0
  48. rtings_mcp-0.2.1/tests/test_services.py +226 -0
  49. rtings_mcp-0.2.1/tests/test_tools_offline.py +3255 -0
  50. rtings_mcp-0.2.1/uv.lock +911 -0
@@ -0,0 +1,75 @@
1
+ name: Publish to PyPI
2
+
3
+ # Trusted publishing (OIDC): no API token is stored anywhere. PyPI is told, once, that
4
+ # releases of the project may come from this repository's `publish.yml` running in the
5
+ # `pypi` environment; each run then mints a short-lived token for itself.
6
+ #
7
+ # Trigger: publishing a GitHub release whose tag is the version (`v0.2.1`). The tag must
8
+ # match `src/rtings_mcp/__init__.py`, or the job refuses — one source of truth for the
9
+ # version, and a release page that says one thing while the wheel says another is the
10
+ # kind of quiet inconsistency the project spends its rules avoiding.
11
+
12
+ on:
13
+ release:
14
+ types: [published]
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ check:
22
+ name: Lint and offline tests
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: astral-sh/setup-uv@v5
27
+ with:
28
+ python-version: "3.12"
29
+ - name: Install
30
+ run: uv sync --extra dev
31
+ - name: Lint
32
+ run: uv run ruff check src/ tests/
33
+ - name: Offline tests
34
+ run: uv run pytest tests/ -q
35
+
36
+ build:
37
+ name: Build sdist and wheel
38
+ needs: check
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v5
43
+ with:
44
+ python-version: "3.12"
45
+ - name: Release tag must match the package version
46
+ if: github.event_name == 'release'
47
+ run: |
48
+ tag="${GITHUB_REF_NAME#v}"
49
+ version="$(python3 -c "import re,pathlib; print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('src/rtings_mcp/__init__.py').read_text()).group(1))")"
50
+ if [ "$tag" != "$version" ]; then
51
+ echo "release tag v$tag does not match __version__ $version" >&2
52
+ exit 1
53
+ fi
54
+ - name: Build
55
+ run: uv build
56
+ - uses: actions/upload-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ publish:
62
+ name: Publish
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ environment:
66
+ name: pypi
67
+ url: https://pypi.org/project/rtings-mcp/
68
+ permissions:
69
+ id-token: write
70
+ steps:
71
+ - uses: actions/download-artifact@v4
72
+ with:
73
+ name: dist
74
+ path: dist/
75
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,41 @@
1
+ # Delegated-review run artifacts: full prompts, stdout and rollouts of subagent runs.
2
+ # They quote repo source verbatim and are per-machine scratch, never part of the project.
3
+ .codex-dobby/
4
+
5
+ # Browser-automation recon artifacts — contain analytics client IDs, coarse geo,
6
+ # UA fingerprints and verbatim scrapes of third-party page content.
7
+ .playwright-mcp/
8
+
9
+ # Credentials and cache — never committed. The runtime cache legitimately holds
10
+ # the user's own member data; the repo never does.
11
+ .cache/
12
+ session.json
13
+ *.session.json
14
+
15
+ # Python
16
+ .venv/
17
+ venv/
18
+ __pycache__/
19
+ *.py[cod]
20
+ *.egg-info/
21
+ .eggs/
22
+ dist/
23
+ build/
24
+ .pytest_cache/
25
+ .ruff_cache/
26
+ .mypy_cache/
27
+ .coverage
28
+ htmlcov/
29
+
30
+ # OS / editor
31
+ .DS_Store
32
+ *.swp
33
+ .idea/
34
+ .vscode/
35
+
36
+ # Recon/scratch artifacts — screenshots and page captures contain third-party
37
+ # content and browser fingerprints. Findings belong in RECON.md as text.
38
+ *.png
39
+ *.jpg
40
+ *.har
41
+ scratch/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avery Levitt
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,240 @@
1
+ Metadata-Version: 2.5
2
+ Name: rtings-mcp
3
+ Version: 0.2.1
4
+ Summary: MCP server exposing RTINGS.com test data as structured tools
5
+ Project-URL: Homepage, https://github.com/Averyy/rtings-mcp
6
+ Project-URL: Repository, https://github.com/Averyy/rtings-mcp
7
+ Author: Avery Levitt
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: mcp,model-context-protocol,reviews,rtings,test-data
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: mcp>=2.1.1
21
+ Requires-Dist: wafer-py>=0.5.0
22
+ Provides-Extra: browser
23
+ Requires-Dist: playwright>=1.40; extra == 'browser'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=1.4; extra == 'dev'
26
+ Requires-Dist: pytest>=9.1; extra == 'dev'
27
+ Requires-Dist: ruff>=0.16; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # rtings-mcp
31
+
32
+ > ### ⚠️ Not a paywall bypass
33
+ > RTINGS gates some test values and scores server-side. This server reads what your own session
34
+ > is already entitled to. On the categories that enforce the paywall, gated values come back
35
+ > `null` without a membership. The tool can't unlock them, and it never estimates, interpolates
36
+ > or infers them. For those numbers you have to pay RTINGS and sign in.
37
+
38
+ An MCP server that exposes [RTINGS.com](https://www.rtings.com) test data as structured tools:
39
+ 0–10 scores, scalar measurements with units, specs, and the measurement curves behind them. It
40
+ runs locally. Nothing is hosted and nothing is shared.
41
+
42
+ RTINGS' JSON API is keyless and public, and a lot comes back anonymously. Signing in adds data on
43
+ the 12 categories that gate.
44
+
45
+ > **Status: working, anonymous and signed in.** All seven data tools run against the live API, and
46
+ > two more connect a membership.
47
+ > **A membership cookie does unblur the JSON API** — measured 2026-09-06 on a bought membership:
48
+ > 588 of 588 withheld rows came back with real values where anonymous got none
49
+ > ([`RECON.md`](RECON.md) §13). Member mode is on by default as of that measurement, so a
50
+ > signed-in session caches its rows under their own tier; `RTINGS_MEMBER_MODE=0` pins everything
51
+ > back to `anonymous`.
52
+ > [`SPEC.md`](SPEC.md) is the design of record and [`RECON.md`](RECON.md) the measured evidence.
53
+
54
+ ## What anonymous gets
55
+
56
+ An anonymous sweep of all 28 categories ([`RECON.md` §11](RECON.md)) found the rule:
57
+
58
+ ```
59
+ blurred ⇔ product.published == false (Early Access, an Insider perk separate from the category paywall)
60
+ ∨ (test.insider_only ∧ the CATEGORY enforces the paywall)
61
+ ```
62
+
63
+ Enforcement is per-category and binary. 12 of 28 enforce, 16 don't.
64
+
65
+ | | Categories | Anonymous gets |
66
+ |---|---|---|
67
+ | **Open (16)** | mattress, vacuum, air-purifier, air-fryer, refrigerator, microwave, toaster, toaster-oven, air-conditioner, dehumidifier, humidifier, blender, vpn, keyboard-switch, camera, running-shoes | test values, scores, ranking and comparison. The numbers are served. |
68
+ | **Gated (12)** | tv, headphones, monitor, mouse, keyboard, soundbar, speaker, printer, laptop, robot-vacuum, projector, router | catalog, schema, search, **RTINGS' written verdicts, pros and cons**, review prose, ranked best-of lists, published curve data, and a handful of public spec fields with their scores |
69
+
70
+ Every category reports `has_paywall: true`, so that flag carries no information. Only the served
71
+ data shows the split. What decides it is unknown and it will change, so the server never hardcodes
72
+ it; `rt_silos()` reports what your machine observed. The map is re-scanned before each release
73
+ against [`docs/enforcement-snapshot.json`](docs/enforcement-snapshot.json) (baseline 2026-09-03,
74
+ re-verified 2026-09-06: 12/16, unchanged — and re-verified *with* a membership stored, which the
75
+ scan ignores by construction, so the map stays an anonymous measurement).
76
+
77
+ ## Honest nulls
78
+
79
+ Gated values come back `null` with a typed output schema and structural `auth_state` /
80
+ `data_tier` / `scores_available` fields. Every row says why it has no value: measured but hidden,
81
+ not applicable, not tested, Early Access, or outside what the server can show it has fetched. An
82
+ agent can't read "no member session" as "RTINGS did not test this."
83
+
84
+ ## Install
85
+
86
+ From PyPI, with nothing to clone (`uvx` fetches and caches the package on first run):
87
+
88
+ ```bash
89
+ claude mcp add rtings --scope user -- uvx rtings-mcp
90
+ ```
91
+
92
+ For the in-conversation browser sign-in (`rt_sign_in`) install the `browser` extra instead:
93
+
94
+ ```bash
95
+ claude mcp add rtings --scope user -- uvx --from "rtings-mcp[browser]" rtings-mcp
96
+ ```
97
+
98
+ Or from a checkout, if you want to work on it:
99
+
100
+ ```bash
101
+ git clone https://github.com/Averyy/rtings-mcp && cd rtings-mcp
102
+ uv venv && uv pip install -e .
103
+ claude mcp add rtings --scope user -- uv --directory /absolute/path/to/rtings-mcp run rtings-mcp
104
+ ```
105
+
106
+ `--scope user` makes it available in every project. Drop it to scope the server to the current
107
+ directory. Verify with:
108
+
109
+ ```bash
110
+ claude mcp get rtings # expect: Status: ✔ Connected
111
+ ```
112
+
113
+ A connected server keeps running the code it started with, so restart the session after editing
114
+ the source. `/reload-plugins` won't do it; it only handles servers bundled inside plugins.
115
+
116
+ No configuration is needed to start. Anonymous is the default and never an error.
117
+
118
+ ## Tools
119
+
120
+ | Tool | Returns |
121
+ |---|---|
122
+ | `rt_silos(silos?)` | the 28 categories with **observed** paywall enforcement |
123
+ | `rt_schema(silo, bench?, group?, find?)` | test/usage definitions: name, kind, unit, hierarchy, `insider_only`; `find` searches the bench by name |
124
+ | `rt_ratings(silo, tests?, usages?, filters?, sort?, limit?)` | catalog + 0–10 usage scores, with an optional scalar projection; `filters.product_ids` for a head-to-head |
125
+ | `rt_product(product, group?, tests?)` | one review: test results (bounded by `group` or `tests`), plus RTINGS' verdicts and pros/cons with `include_verdicts` (`include_results=false` for the words alone) |
126
+ | `rt_graph(product, test)` | one test's measurement curve, resampled by selecting shipped points |
127
+ | `rt_search(query)` | model name/number → candidates across all categories |
128
+ | `rt_recommendations(silo, list?, limit?)` | the category's best-of lists, or one ranked list with reasoning |
129
+ | `rt_sign_in(force?)` | opens a browser window on RTINGS' sign-in page and stores the resulting cookie |
130
+ | `rt_auth_status(wait_s?)` | what credential is stored, and how a sign-in in progress is going |
131
+
132
+ Results are compared within a **test bench**, RTINGS' version of its methodology. Cross-bench
133
+ results come back nested in separate groups and `limit` applies within each.
134
+
135
+ `filters` and `sort` take a test's id or its name, plus `brand`, `name_contains`, `published` and
136
+ `variant`. `variant` is the size RTINGS tested, which is how you ask for 65-inch TVs, since most
137
+ categories have no "Size" test. A field you filter or sort on is fetched for you. When one can't be
138
+ compared the predicate is **not applied** and the response names why: gated for this session,
139
+ absent from the bench queried, or measured-but-empty. Otherwise an empty result reads as "no
140
+ product qualifies" when the truth is "you can't see it". Rows with no comparable value sort last in
141
+ both directions.
142
+
143
+ **On a gated category, ask for the verdicts.** `rt_product(url, include_verdicts=true)` returns
144
+ RTINGS' per-usage judgement in their own words, their pros and cons, and how each usage score is
145
+ composed. Those come through even where every measurement is null.
146
+
147
+ ⚠️ **`rt_product` can cost you something.** On a free account it hits the endpoint RTINGS meters,
148
+ so each new review spends one of your limited previews. It refuses by default and requires an
149
+ explicit `consume_preview=true`, reports how many remain, and never silently re-fetches a cached
150
+ review.
151
+
152
+ ## Configuration
153
+
154
+ | Variable | Default | Purpose |
155
+ |---|---|---|
156
+ | `RTINGS_SESSION_COOKIE` | unset | your session cookie (see below) |
157
+ | `RTINGS_CACHE_DIR` | `~/.cache/rtings-mcp` | cache location |
158
+ | `RTINGS_CACHE_MAX_MB` | `1024` | cache ceiling, LRU-evicted |
159
+ | `RTINGS_ENABLE_GRAPH` | `true` | set `false` to disable `rt_graph` entirely |
160
+ | `RTINGS_RATE_INTERVAL_S` | `2.0` | seconds per token refilled into the rtings.com request budget |
161
+ | `RTINGS_MIN_REQUEST_INTERVAL_S` | — | **deprecated**; pins `RTINGS_RATE_INTERVAL_S` and forces burst 1, i.e. the old flat interval. Warns when set |
162
+ | `RTINGS_RATE_BURST` | `5` | requests available immediately after an idle period |
163
+ | `RTINGS_MAX_PREVIEW_SPEND` | `1` | metered previews `rt_product` may spend per run; `0` forbids it |
164
+ | `RTINGS_CONFIG_DIR` | `~/.config/rtings-mcp` | where the credential is stored |
165
+ | `RTINGS_CACHE_TTL_DAYS` | `30` | TTL for measurements and reviews; routing data has its own shorter clocks |
166
+ | `RTINGS_CONCURRENCY` | `1` | in-flight requests to rtings.com |
167
+ | `RTINGS_GRAPH_MAX_POINTS` | `200` | default curve resampling target |
168
+ | `RTINGS_MAX_RESPONSE_CHARS` | `40000` | `rt_ratings` trims each group's page to fit this many characters on the wire and says where to page from |
169
+ | `RTINGS_CDN_RATE_INTERVAL_S` / `RTINGS_CDN_RATE_BURST` | `0.25` / `10` | the asset CDN's own budget |
170
+ | `RTINGS_TELEMETRY` | `true` | append header-only request records to `telemetry/requests.jsonl` |
171
+ | `RTINGS_MEMBER_MODE` | `true` | member-tier caching. Set `0` to pin every cached file to the `anonymous` tier — a signed-in session then serves its rows but cannot cache them |
172
+ | `RTINGS_SESSION_OVERRIDE` | unset | `member`/`free`/`anonymous` — assert your own tier if the probe reads it wrong |
173
+
174
+ ## Signing in (optional, and only adds anything on the 12 gated categories)
175
+
176
+ You supply your own RTINGS session. **No password is ever requested and login is never automated** —
177
+ you type it into RTINGS' own page, and the server keeps the resulting cookie.
178
+
179
+ **In a conversation** (works in Claude Desktop and Claude Code):
180
+
181
+ > "sign me in to RTINGS"
182
+
183
+ That calls `rt_sign_in`, which opens a browser window on the sign-in page and answers straight
184
+ away; `rt_auth_status(wait_s=45)` waits for you to finish. It needs the browser extra:
185
+
186
+ ```bash
187
+ uv sync --extra browser # or: pip install "rtings-mcp[browser]"
188
+ ```
189
+
190
+ **In a terminal**, the same capture without an MCP client:
191
+
192
+ ```bash
193
+ .venv/bin/rtings-mcp auth --browser
194
+ ```
195
+
196
+ **Without a browser at all**, paste one instead. `_rtings_session` is **HttpOnly**, so
197
+ `document.cookie` can't read it and "Copy as cURL" is the only way to get it by hand: open
198
+ rtings.com logged in → DevTools → Network → right-click any request → Copy → Copy as cURL, then:
199
+
200
+ ```bash
201
+ .venv/bin/rtings-mcp auth # paste, press Ctrl-D; it validates and tells you what you have
202
+ ```
203
+
204
+ Every path validates the cookie against RTINGS before storing it. A session that doesn't come back
205
+ signed in is discarded, which matters more than it sounds: RTINGS hands anonymous visitors a
206
+ `_rtings_session` too, so "a cookie appeared" proves nothing on its own.
207
+
208
+ The cookie is stored `0600` in your config dir, sent only to `www.rtings.com`, never written to
209
+ the cache, never logged, never returned in tool output. It grants access to your account, so treat
210
+ the file accordingly and revoke it by logging out of RTINGS. `--status` shows the current session
211
+ and `--clear` removes the stored copy.
212
+
213
+ You shouldn't have to do this again. RTINGS re-issues the cookie on every response with a fresh
214
+ 30-day expiry, so it's a sliding idle window rather than a deadline from login. The server persists
215
+ each re-issued value that came back with your account signed in, and discards the one an anonymous
216
+ request mints. `RTINGS_SESSION_COOKIE` can't be refreshed this way, so prefer `rtings-mcp auth` for
217
+ anything long-running.
218
+
219
+ Running an automated client against your own account is your call and carries whatever obligations
220
+ your account does. The server is cache-first, rate-limited, and does no bulk crawling.
221
+
222
+ ## Development
223
+
224
+ ```bash
225
+ uv pip install -e ".[dev]"
226
+ uv pip install -e ".[browser]" # only to run the browser sign-in itself
227
+ .venv/bin/pytest tests/ -q # offline (the default; no network)
228
+ .venv/bin/pytest -m live -q # live, anonymous, against the real API
229
+ .venv/bin/pytest -m "live and slow" -q -s # + the 28-category enforcement re-scan
230
+ .venv/bin/ruff check src/ tests/
231
+ ```
232
+
233
+ ## Legal
234
+
235
+ MIT licensed ([LICENSE](LICENSE)). Not affiliated with or endorsed by RTINGS.com.
236
+
237
+ The server calls the same keyless public JSON API that rtings.com's own front end calls, reads
238
+ only what that API returns to the caller, and never attempts to reveal anything the server
239
+ withholds. All test data and measurements are RTINGS' own, subject to their
240
+ [Terms of Use](https://www.rtings.com/company/terms-of-use). Cache locally, don't redistribute it.
@@ -0,0 +1,211 @@
1
+ # rtings-mcp
2
+
3
+ > ### ⚠️ Not a paywall bypass
4
+ > RTINGS gates some test values and scores server-side. This server reads what your own session
5
+ > is already entitled to. On the categories that enforce the paywall, gated values come back
6
+ > `null` without a membership. The tool can't unlock them, and it never estimates, interpolates
7
+ > or infers them. For those numbers you have to pay RTINGS and sign in.
8
+
9
+ An MCP server that exposes [RTINGS.com](https://www.rtings.com) test data as structured tools:
10
+ 0–10 scores, scalar measurements with units, specs, and the measurement curves behind them. It
11
+ runs locally. Nothing is hosted and nothing is shared.
12
+
13
+ RTINGS' JSON API is keyless and public, and a lot comes back anonymously. Signing in adds data on
14
+ the 12 categories that gate.
15
+
16
+ > **Status: working, anonymous and signed in.** All seven data tools run against the live API, and
17
+ > two more connect a membership.
18
+ > **A membership cookie does unblur the JSON API** — measured 2026-09-06 on a bought membership:
19
+ > 588 of 588 withheld rows came back with real values where anonymous got none
20
+ > ([`RECON.md`](RECON.md) §13). Member mode is on by default as of that measurement, so a
21
+ > signed-in session caches its rows under their own tier; `RTINGS_MEMBER_MODE=0` pins everything
22
+ > back to `anonymous`.
23
+ > [`SPEC.md`](SPEC.md) is the design of record and [`RECON.md`](RECON.md) the measured evidence.
24
+
25
+ ## What anonymous gets
26
+
27
+ An anonymous sweep of all 28 categories ([`RECON.md` §11](RECON.md)) found the rule:
28
+
29
+ ```
30
+ blurred ⇔ product.published == false (Early Access, an Insider perk separate from the category paywall)
31
+ ∨ (test.insider_only ∧ the CATEGORY enforces the paywall)
32
+ ```
33
+
34
+ Enforcement is per-category and binary. 12 of 28 enforce, 16 don't.
35
+
36
+ | | Categories | Anonymous gets |
37
+ |---|---|---|
38
+ | **Open (16)** | mattress, vacuum, air-purifier, air-fryer, refrigerator, microwave, toaster, toaster-oven, air-conditioner, dehumidifier, humidifier, blender, vpn, keyboard-switch, camera, running-shoes | test values, scores, ranking and comparison. The numbers are served. |
39
+ | **Gated (12)** | tv, headphones, monitor, mouse, keyboard, soundbar, speaker, printer, laptop, robot-vacuum, projector, router | catalog, schema, search, **RTINGS' written verdicts, pros and cons**, review prose, ranked best-of lists, published curve data, and a handful of public spec fields with their scores |
40
+
41
+ Every category reports `has_paywall: true`, so that flag carries no information. Only the served
42
+ data shows the split. What decides it is unknown and it will change, so the server never hardcodes
43
+ it; `rt_silos()` reports what your machine observed. The map is re-scanned before each release
44
+ against [`docs/enforcement-snapshot.json`](docs/enforcement-snapshot.json) (baseline 2026-09-03,
45
+ re-verified 2026-09-06: 12/16, unchanged — and re-verified *with* a membership stored, which the
46
+ scan ignores by construction, so the map stays an anonymous measurement).
47
+
48
+ ## Honest nulls
49
+
50
+ Gated values come back `null` with a typed output schema and structural `auth_state` /
51
+ `data_tier` / `scores_available` fields. Every row says why it has no value: measured but hidden,
52
+ not applicable, not tested, Early Access, or outside what the server can show it has fetched. An
53
+ agent can't read "no member session" as "RTINGS did not test this."
54
+
55
+ ## Install
56
+
57
+ From PyPI, with nothing to clone (`uvx` fetches and caches the package on first run):
58
+
59
+ ```bash
60
+ claude mcp add rtings --scope user -- uvx rtings-mcp
61
+ ```
62
+
63
+ For the in-conversation browser sign-in (`rt_sign_in`) install the `browser` extra instead:
64
+
65
+ ```bash
66
+ claude mcp add rtings --scope user -- uvx --from "rtings-mcp[browser]" rtings-mcp
67
+ ```
68
+
69
+ Or from a checkout, if you want to work on it:
70
+
71
+ ```bash
72
+ git clone https://github.com/Averyy/rtings-mcp && cd rtings-mcp
73
+ uv venv && uv pip install -e .
74
+ claude mcp add rtings --scope user -- uv --directory /absolute/path/to/rtings-mcp run rtings-mcp
75
+ ```
76
+
77
+ `--scope user` makes it available in every project. Drop it to scope the server to the current
78
+ directory. Verify with:
79
+
80
+ ```bash
81
+ claude mcp get rtings # expect: Status: ✔ Connected
82
+ ```
83
+
84
+ A connected server keeps running the code it started with, so restart the session after editing
85
+ the source. `/reload-plugins` won't do it; it only handles servers bundled inside plugins.
86
+
87
+ No configuration is needed to start. Anonymous is the default and never an error.
88
+
89
+ ## Tools
90
+
91
+ | Tool | Returns |
92
+ |---|---|
93
+ | `rt_silos(silos?)` | the 28 categories with **observed** paywall enforcement |
94
+ | `rt_schema(silo, bench?, group?, find?)` | test/usage definitions: name, kind, unit, hierarchy, `insider_only`; `find` searches the bench by name |
95
+ | `rt_ratings(silo, tests?, usages?, filters?, sort?, limit?)` | catalog + 0–10 usage scores, with an optional scalar projection; `filters.product_ids` for a head-to-head |
96
+ | `rt_product(product, group?, tests?)` | one review: test results (bounded by `group` or `tests`), plus RTINGS' verdicts and pros/cons with `include_verdicts` (`include_results=false` for the words alone) |
97
+ | `rt_graph(product, test)` | one test's measurement curve, resampled by selecting shipped points |
98
+ | `rt_search(query)` | model name/number → candidates across all categories |
99
+ | `rt_recommendations(silo, list?, limit?)` | the category's best-of lists, or one ranked list with reasoning |
100
+ | `rt_sign_in(force?)` | opens a browser window on RTINGS' sign-in page and stores the resulting cookie |
101
+ | `rt_auth_status(wait_s?)` | what credential is stored, and how a sign-in in progress is going |
102
+
103
+ Results are compared within a **test bench**, RTINGS' version of its methodology. Cross-bench
104
+ results come back nested in separate groups and `limit` applies within each.
105
+
106
+ `filters` and `sort` take a test's id or its name, plus `brand`, `name_contains`, `published` and
107
+ `variant`. `variant` is the size RTINGS tested, which is how you ask for 65-inch TVs, since most
108
+ categories have no "Size" test. A field you filter or sort on is fetched for you. When one can't be
109
+ compared the predicate is **not applied** and the response names why: gated for this session,
110
+ absent from the bench queried, or measured-but-empty. Otherwise an empty result reads as "no
111
+ product qualifies" when the truth is "you can't see it". Rows with no comparable value sort last in
112
+ both directions.
113
+
114
+ **On a gated category, ask for the verdicts.** `rt_product(url, include_verdicts=true)` returns
115
+ RTINGS' per-usage judgement in their own words, their pros and cons, and how each usage score is
116
+ composed. Those come through even where every measurement is null.
117
+
118
+ ⚠️ **`rt_product` can cost you something.** On a free account it hits the endpoint RTINGS meters,
119
+ so each new review spends one of your limited previews. It refuses by default and requires an
120
+ explicit `consume_preview=true`, reports how many remain, and never silently re-fetches a cached
121
+ review.
122
+
123
+ ## Configuration
124
+
125
+ | Variable | Default | Purpose |
126
+ |---|---|---|
127
+ | `RTINGS_SESSION_COOKIE` | unset | your session cookie (see below) |
128
+ | `RTINGS_CACHE_DIR` | `~/.cache/rtings-mcp` | cache location |
129
+ | `RTINGS_CACHE_MAX_MB` | `1024` | cache ceiling, LRU-evicted |
130
+ | `RTINGS_ENABLE_GRAPH` | `true` | set `false` to disable `rt_graph` entirely |
131
+ | `RTINGS_RATE_INTERVAL_S` | `2.0` | seconds per token refilled into the rtings.com request budget |
132
+ | `RTINGS_MIN_REQUEST_INTERVAL_S` | — | **deprecated**; pins `RTINGS_RATE_INTERVAL_S` and forces burst 1, i.e. the old flat interval. Warns when set |
133
+ | `RTINGS_RATE_BURST` | `5` | requests available immediately after an idle period |
134
+ | `RTINGS_MAX_PREVIEW_SPEND` | `1` | metered previews `rt_product` may spend per run; `0` forbids it |
135
+ | `RTINGS_CONFIG_DIR` | `~/.config/rtings-mcp` | where the credential is stored |
136
+ | `RTINGS_CACHE_TTL_DAYS` | `30` | TTL for measurements and reviews; routing data has its own shorter clocks |
137
+ | `RTINGS_CONCURRENCY` | `1` | in-flight requests to rtings.com |
138
+ | `RTINGS_GRAPH_MAX_POINTS` | `200` | default curve resampling target |
139
+ | `RTINGS_MAX_RESPONSE_CHARS` | `40000` | `rt_ratings` trims each group's page to fit this many characters on the wire and says where to page from |
140
+ | `RTINGS_CDN_RATE_INTERVAL_S` / `RTINGS_CDN_RATE_BURST` | `0.25` / `10` | the asset CDN's own budget |
141
+ | `RTINGS_TELEMETRY` | `true` | append header-only request records to `telemetry/requests.jsonl` |
142
+ | `RTINGS_MEMBER_MODE` | `true` | member-tier caching. Set `0` to pin every cached file to the `anonymous` tier — a signed-in session then serves its rows but cannot cache them |
143
+ | `RTINGS_SESSION_OVERRIDE` | unset | `member`/`free`/`anonymous` — assert your own tier if the probe reads it wrong |
144
+
145
+ ## Signing in (optional, and only adds anything on the 12 gated categories)
146
+
147
+ You supply your own RTINGS session. **No password is ever requested and login is never automated** —
148
+ you type it into RTINGS' own page, and the server keeps the resulting cookie.
149
+
150
+ **In a conversation** (works in Claude Desktop and Claude Code):
151
+
152
+ > "sign me in to RTINGS"
153
+
154
+ That calls `rt_sign_in`, which opens a browser window on the sign-in page and answers straight
155
+ away; `rt_auth_status(wait_s=45)` waits for you to finish. It needs the browser extra:
156
+
157
+ ```bash
158
+ uv sync --extra browser # or: pip install "rtings-mcp[browser]"
159
+ ```
160
+
161
+ **In a terminal**, the same capture without an MCP client:
162
+
163
+ ```bash
164
+ .venv/bin/rtings-mcp auth --browser
165
+ ```
166
+
167
+ **Without a browser at all**, paste one instead. `_rtings_session` is **HttpOnly**, so
168
+ `document.cookie` can't read it and "Copy as cURL" is the only way to get it by hand: open
169
+ rtings.com logged in → DevTools → Network → right-click any request → Copy → Copy as cURL, then:
170
+
171
+ ```bash
172
+ .venv/bin/rtings-mcp auth # paste, press Ctrl-D; it validates and tells you what you have
173
+ ```
174
+
175
+ Every path validates the cookie against RTINGS before storing it. A session that doesn't come back
176
+ signed in is discarded, which matters more than it sounds: RTINGS hands anonymous visitors a
177
+ `_rtings_session` too, so "a cookie appeared" proves nothing on its own.
178
+
179
+ The cookie is stored `0600` in your config dir, sent only to `www.rtings.com`, never written to
180
+ the cache, never logged, never returned in tool output. It grants access to your account, so treat
181
+ the file accordingly and revoke it by logging out of RTINGS. `--status` shows the current session
182
+ and `--clear` removes the stored copy.
183
+
184
+ You shouldn't have to do this again. RTINGS re-issues the cookie on every response with a fresh
185
+ 30-day expiry, so it's a sliding idle window rather than a deadline from login. The server persists
186
+ each re-issued value that came back with your account signed in, and discards the one an anonymous
187
+ request mints. `RTINGS_SESSION_COOKIE` can't be refreshed this way, so prefer `rtings-mcp auth` for
188
+ anything long-running.
189
+
190
+ Running an automated client against your own account is your call and carries whatever obligations
191
+ your account does. The server is cache-first, rate-limited, and does no bulk crawling.
192
+
193
+ ## Development
194
+
195
+ ```bash
196
+ uv pip install -e ".[dev]"
197
+ uv pip install -e ".[browser]" # only to run the browser sign-in itself
198
+ .venv/bin/pytest tests/ -q # offline (the default; no network)
199
+ .venv/bin/pytest -m live -q # live, anonymous, against the real API
200
+ .venv/bin/pytest -m "live and slow" -q -s # + the 28-category enforcement re-scan
201
+ .venv/bin/ruff check src/ tests/
202
+ ```
203
+
204
+ ## Legal
205
+
206
+ MIT licensed ([LICENSE](LICENSE)). Not affiliated with or endorsed by RTINGS.com.
207
+
208
+ The server calls the same keyless public JSON API that rtings.com's own front end calls, reads
209
+ only what that API returns to the caller, and never attempts to reveal anything the server
210
+ withholds. All test data and measurements are RTINGS' own, subject to their
211
+ [Terms of Use](https://www.rtings.com/company/terms-of-use). Cache locally, don't redistribute it.