verumsignal-mcp 0.2.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.
- verumsignal_mcp-0.2.0/LICENSE +21 -0
- verumsignal_mcp-0.2.0/PKG-INFO +108 -0
- verumsignal_mcp-0.2.0/README.md +88 -0
- verumsignal_mcp-0.2.0/pyproject.toml +33 -0
- verumsignal_mcp-0.2.0/setup.cfg +4 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp/__init__.py +2 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp/server.py +332 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp.egg-info/PKG-INFO +108 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp.egg-info/SOURCES.txt +10 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp.egg-info/dependency_links.txt +1 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp.egg-info/entry_points.txt +2 -0
- verumsignal_mcp-0.2.0/verumsignal_mcp.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verum Signal
|
|
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,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verumsignal-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for Verum Signal: political debate claims, who said each one and how it was rated.
|
|
5
|
+
Author: Verum Signal LLC
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://verumsignal.com
|
|
8
|
+
Project-URL: Documentation, https://verumsignal.com/developers
|
|
9
|
+
Project-URL: Source, https://github.com/brittbart/verumsignal-mcp
|
|
10
|
+
Project-URL: Changelog, https://github.com/brittbart/verumsignal-mcp/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,debates,politics,claims,elections
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Verum Signal MCP Server
|
|
22
|
+
|
|
23
|
+
<!-- mcp-name: io.github.brittbart/verumsignal-mcp -->
|
|
24
|
+
|
|
25
|
+
An MCP server that gives LLM agents Verum Signal's political debate coverage: the claims candidates made, who said each one, and how each was rated. It also searches the claims news outlets report. Requires a Verum Signal API key.
|
|
26
|
+
|
|
27
|
+
## What the tools do
|
|
28
|
+
|
|
29
|
+
| Tool | Returns | Explicitly does NOT |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `list_debates` | Available debates with claim counts, dates, and status. | Include the claims themselves — call `get_debate_verdicts` for that. |
|
|
32
|
+
| `get_debate_verdicts` | Every rated claim from one debate, with the speaker, the rating, and whether the rating is still provisional. Paginates automatically to completeness. | Truncate silently. If a debate is large enough to hit an internal safety cap, the response includes an explicit `truncated: true` flag rather than quietly returning a partial result. |
|
|
33
|
+
| `list_recent_claims` | Claims reported by news outlets, with their ratings. Pass `q` to find claims about a topic; filter by outlet or rating; without `q`, most recent first. | Return quoted claims an outlet attributes to someone else, or any overall score for an outlet. |
|
|
34
|
+
| `get_api_status` | Corpus statistics: how much the corpus holds, what it covers, and how current it is. | Include per-claim detail — call the other tools for that. |
|
|
35
|
+
|
|
36
|
+
Full parameter schemas are in the tool definitions themselves (`verumsignal_mcp/server.py`), and are also what any MCP client will show you directly. This table is a summary; the tool definitions are authoritative.
|
|
37
|
+
|
|
38
|
+
## Install & config
|
|
39
|
+
|
|
40
|
+
Requires Python 3.9+ and no third-party dependencies.
|
|
41
|
+
|
|
42
|
+
**With uv** (nothing to install first):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"verum-signal": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["verumsignal-mcp"],
|
|
50
|
+
"env": { "VS_API_KEY": "vs_live_..." }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**With pip:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install verumsignal-mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
then use `"command": "verumsignal-mcp"` with no `args` in the config above.
|
|
63
|
+
|
|
64
|
+
**From a clone:** `git clone https://github.com/brittbart/verumsignal-mcp.git` and use `"command": "python3"`, `"args": ["/absolute/path/to/mcp_server.py"]`.
|
|
65
|
+
|
|
66
|
+
These examples are for Claude Desktop's `claude_desktop_config.json`; other MCP clients take the same command, arguments and environment.
|
|
67
|
+
|
|
68
|
+
**Verify it works:** ask your agent "which debates does Verum Signal cover?" A correctly wired server returns a list of debates with dates and claim counts. If you see an authentication error, check the key; if you see a connection error, confirm outbound HTTPS access to `api.verumsignal.com` is allowed from wherever the server runs.
|
|
69
|
+
|
|
70
|
+
## Getting a key
|
|
71
|
+
|
|
72
|
+
Keys are issued to Verum Signal account holders. See [verumsignal.com/developers](https://verumsignal.com/developers).
|
|
73
|
+
|
|
74
|
+
## The data boundary
|
|
75
|
+
|
|
76
|
+
Rating reasoning is not available through the API, and that is kept structurally: the API's queries never fetch the written reasoning behind a rating, and the cache that serves the API never stores it. It isn't a filter applied after the fact — the data simply isn't in what this server can reach.
|
|
77
|
+
|
|
78
|
+
What this server sends out: an authenticated `GET` request to `api.verumsignal.com` (or an operator-configured `VS_API_BASE`) for each tool call.
|
|
79
|
+
|
|
80
|
+
What it sends back to the calling agent: whatever that API call returns.
|
|
81
|
+
|
|
82
|
+
What it never touches: this server makes no network connections to anything other than the configured API host, reads and writes no local files, and includes no telemetry or analytics of any kind. It uses only the Python standard library (`json`, `os`, `sys`, `urllib`).
|
|
83
|
+
|
|
84
|
+
## Key handling
|
|
85
|
+
|
|
86
|
+
`VS_API_KEY` is read from the environment and is never hardcoded. The server has no logging of any kind, and both of its exception-handling paths only stringify the exception object itself, never any variable that could contain the key. There is no code path by which the key can reach output, a log, or a file.
|
|
87
|
+
|
|
88
|
+
You can provide the key two ways:
|
|
89
|
+
|
|
90
|
+
1. **In the MCP client's config file**, as shown above. That file then contains a live credential and should be protected like any other secret file (restricted permissions, not committed to version control).
|
|
91
|
+
2. **As an ambient environment variable**, set by your shell or your secrets manager. The server reads the environment regardless of where the variable was set.
|
|
92
|
+
|
|
93
|
+
## Rate limits & quota
|
|
94
|
+
|
|
95
|
+
Your key's tier sets a per-minute rate and a monthly quota; current limits are on [verumsignal.com/developers](https://verumsignal.com/developers). Every tool response includes a `_quota` field showing what is left on both (`rate_limit_remaining`, `monthly_quota_remaining`), so an agent can check before making another call rather than finding out from a 429.
|
|
96
|
+
|
|
97
|
+
The rate limiter uses a fixed one-minute wall-clock window, not a sliding window, so a burst spanning a minute boundary can very briefly exceed the nominal per-minute rate. It's bounded, and you should hear it from us first.
|
|
98
|
+
|
|
99
|
+
## Versioning
|
|
100
|
+
|
|
101
|
+
This server is at **v0.2.0**, an honest pre-1.0 number. It targets API version v1. Ratings carry the methodology version they were made under. See `CHANGELOG.md` for version history.
|
|
102
|
+
|
|
103
|
+
## Known limitations
|
|
104
|
+
|
|
105
|
+
- **Outlet-level scores are not served.** The tools return individual claims and their ratings only.
|
|
106
|
+
- **No rating reasoning.** A deliberate boundary — see "The data boundary" above.
|
|
107
|
+
- **Debate ratings can be provisional.** Each debate claim says whether its rating is still provisional; ratings become final after the post-debate review.
|
|
108
|
+
- **Data freshness:** the API's cache refreshes every 5 minutes. This server's data is exactly as fresh as that cache.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Verum Signal MCP Server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.brittbart/verumsignal-mcp -->
|
|
4
|
+
|
|
5
|
+
An MCP server that gives LLM agents Verum Signal's political debate coverage: the claims candidates made, who said each one, and how each was rated. It also searches the claims news outlets report. Requires a Verum Signal API key.
|
|
6
|
+
|
|
7
|
+
## What the tools do
|
|
8
|
+
|
|
9
|
+
| Tool | Returns | Explicitly does NOT |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `list_debates` | Available debates with claim counts, dates, and status. | Include the claims themselves — call `get_debate_verdicts` for that. |
|
|
12
|
+
| `get_debate_verdicts` | Every rated claim from one debate, with the speaker, the rating, and whether the rating is still provisional. Paginates automatically to completeness. | Truncate silently. If a debate is large enough to hit an internal safety cap, the response includes an explicit `truncated: true` flag rather than quietly returning a partial result. |
|
|
13
|
+
| `list_recent_claims` | Claims reported by news outlets, with their ratings. Pass `q` to find claims about a topic; filter by outlet or rating; without `q`, most recent first. | Return quoted claims an outlet attributes to someone else, or any overall score for an outlet. |
|
|
14
|
+
| `get_api_status` | Corpus statistics: how much the corpus holds, what it covers, and how current it is. | Include per-claim detail — call the other tools for that. |
|
|
15
|
+
|
|
16
|
+
Full parameter schemas are in the tool definitions themselves (`verumsignal_mcp/server.py`), and are also what any MCP client will show you directly. This table is a summary; the tool definitions are authoritative.
|
|
17
|
+
|
|
18
|
+
## Install & config
|
|
19
|
+
|
|
20
|
+
Requires Python 3.9+ and no third-party dependencies.
|
|
21
|
+
|
|
22
|
+
**With uv** (nothing to install first):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"verum-signal": {
|
|
28
|
+
"command": "uvx",
|
|
29
|
+
"args": ["verumsignal-mcp"],
|
|
30
|
+
"env": { "VS_API_KEY": "vs_live_..." }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**With pip:**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install verumsignal-mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
then use `"command": "verumsignal-mcp"` with no `args` in the config above.
|
|
43
|
+
|
|
44
|
+
**From a clone:** `git clone https://github.com/brittbart/verumsignal-mcp.git` and use `"command": "python3"`, `"args": ["/absolute/path/to/mcp_server.py"]`.
|
|
45
|
+
|
|
46
|
+
These examples are for Claude Desktop's `claude_desktop_config.json`; other MCP clients take the same command, arguments and environment.
|
|
47
|
+
|
|
48
|
+
**Verify it works:** ask your agent "which debates does Verum Signal cover?" A correctly wired server returns a list of debates with dates and claim counts. If you see an authentication error, check the key; if you see a connection error, confirm outbound HTTPS access to `api.verumsignal.com` is allowed from wherever the server runs.
|
|
49
|
+
|
|
50
|
+
## Getting a key
|
|
51
|
+
|
|
52
|
+
Keys are issued to Verum Signal account holders. See [verumsignal.com/developers](https://verumsignal.com/developers).
|
|
53
|
+
|
|
54
|
+
## The data boundary
|
|
55
|
+
|
|
56
|
+
Rating reasoning is not available through the API, and that is kept structurally: the API's queries never fetch the written reasoning behind a rating, and the cache that serves the API never stores it. It isn't a filter applied after the fact — the data simply isn't in what this server can reach.
|
|
57
|
+
|
|
58
|
+
What this server sends out: an authenticated `GET` request to `api.verumsignal.com` (or an operator-configured `VS_API_BASE`) for each tool call.
|
|
59
|
+
|
|
60
|
+
What it sends back to the calling agent: whatever that API call returns.
|
|
61
|
+
|
|
62
|
+
What it never touches: this server makes no network connections to anything other than the configured API host, reads and writes no local files, and includes no telemetry or analytics of any kind. It uses only the Python standard library (`json`, `os`, `sys`, `urllib`).
|
|
63
|
+
|
|
64
|
+
## Key handling
|
|
65
|
+
|
|
66
|
+
`VS_API_KEY` is read from the environment and is never hardcoded. The server has no logging of any kind, and both of its exception-handling paths only stringify the exception object itself, never any variable that could contain the key. There is no code path by which the key can reach output, a log, or a file.
|
|
67
|
+
|
|
68
|
+
You can provide the key two ways:
|
|
69
|
+
|
|
70
|
+
1. **In the MCP client's config file**, as shown above. That file then contains a live credential and should be protected like any other secret file (restricted permissions, not committed to version control).
|
|
71
|
+
2. **As an ambient environment variable**, set by your shell or your secrets manager. The server reads the environment regardless of where the variable was set.
|
|
72
|
+
|
|
73
|
+
## Rate limits & quota
|
|
74
|
+
|
|
75
|
+
Your key's tier sets a per-minute rate and a monthly quota; current limits are on [verumsignal.com/developers](https://verumsignal.com/developers). Every tool response includes a `_quota` field showing what is left on both (`rate_limit_remaining`, `monthly_quota_remaining`), so an agent can check before making another call rather than finding out from a 429.
|
|
76
|
+
|
|
77
|
+
The rate limiter uses a fixed one-minute wall-clock window, not a sliding window, so a burst spanning a minute boundary can very briefly exceed the nominal per-minute rate. It's bounded, and you should hear it from us first.
|
|
78
|
+
|
|
79
|
+
## Versioning
|
|
80
|
+
|
|
81
|
+
This server is at **v0.2.0**, an honest pre-1.0 number. It targets API version v1. Ratings carry the methodology version they were made under. See `CHANGELOG.md` for version history.
|
|
82
|
+
|
|
83
|
+
## Known limitations
|
|
84
|
+
|
|
85
|
+
- **Outlet-level scores are not served.** The tools return individual claims and their ratings only.
|
|
86
|
+
- **No rating reasoning.** A deliberate boundary — see "The data boundary" above.
|
|
87
|
+
- **Debate ratings can be provisional.** Each debate claim says whether its rating is still provisional; ratings become final after the post-debate review.
|
|
88
|
+
- **Data freshness:** the API's cache refreshes every 5 minutes. This server's data is exactly as fresh as that cache.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verumsignal-mcp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "MCP server for Verum Signal: political debate claims, who said each one and how it was rated."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Verum Signal LLC" }]
|
|
14
|
+
keywords = ["mcp", "model-context-protocol", "debates", "politics", "claims", "elections"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://verumsignal.com"
|
|
25
|
+
Documentation = "https://verumsignal.com/developers"
|
|
26
|
+
Source = "https://github.com/brittbart/verumsignal-mcp"
|
|
27
|
+
Changelog = "https://github.com/brittbart/verumsignal-mcp/blob/main/CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
verumsignal-mcp = "verumsignal_mcp.server:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["verumsignal_mcp"]
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verum Signal MCP Server
|
|
4
|
+
Exposes Verum Signal as a Model Context Protocol tool server.
|
|
5
|
+
Agents (Claude, GPT, etc.) can call these tools natively.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
verumsignal-mcp (installed with: pip install verumsignal-mcp, or uvx verumsignal-mcp)
|
|
9
|
+
python3 mcp_server.py (from a clone of the repository)
|
|
10
|
+
|
|
11
|
+
Environment:
|
|
12
|
+
VS_API_KEY — Verum Signal API key (vs_live_...)
|
|
13
|
+
VS_API_BASE — API base URL (default: https://api.verumsignal.com)
|
|
14
|
+
"""
|
|
15
|
+
import json, os, sys
|
|
16
|
+
import urllib.request, urllib.parse, urllib.error
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from verumsignal_mcp import __version__
|
|
20
|
+
except ImportError: # run as a loose file
|
|
21
|
+
__version__ = "0.2.0"
|
|
22
|
+
|
|
23
|
+
VS_API_BASE = os.environ.get("VS_API_BASE", "https://api.verumsignal.com")
|
|
24
|
+
VS_API_KEY = os.environ.get("VS_API_KEY", "")
|
|
25
|
+
|
|
26
|
+
# Quota/rate-limit headers the REST API returns on every authenticated call.
|
|
27
|
+
# Passed through into every tool's response so an agent running this
|
|
28
|
+
# self-hosted server (and therefore owning its own key) can actually see
|
|
29
|
+
# what's left, instead of finding out by hitting a 429 with no warning.
|
|
30
|
+
_QUOTA_HEADERS = {
|
|
31
|
+
"X-RateLimit-Limit": "rate_limit_per_minute",
|
|
32
|
+
"X-RateLimit-Remaining": "rate_limit_remaining",
|
|
33
|
+
"X-Quota-Limit": "monthly_quota_limit",
|
|
34
|
+
"X-Quota-Remaining": "monthly_quota_remaining",
|
|
35
|
+
"Retry-After": "retry_after_seconds",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _quota_from_headers(headers):
|
|
40
|
+
quota = {}
|
|
41
|
+
for header_name, key in _QUOTA_HEADERS.items():
|
|
42
|
+
v = headers.get(header_name)
|
|
43
|
+
if v is not None:
|
|
44
|
+
quota[key] = v
|
|
45
|
+
return quota
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _api(path, params=None):
|
|
49
|
+
"""Call the REST API. Returns the parsed JSON body with a "_quota" key
|
|
50
|
+
attached whenever rate-limit/quota headers were present. On error,
|
|
51
|
+
passes through the REAL error body from the API (not just the generic
|
|
52
|
+
HTTP reason phrase) alongside the same quota info."""
|
|
53
|
+
url = VS_API_BASE + path
|
|
54
|
+
if params:
|
|
55
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
56
|
+
req = urllib.request.Request(url, headers={
|
|
57
|
+
"Authorization": f"Bearer {VS_API_KEY}",
|
|
58
|
+
"Accept": "application/json",
|
|
59
|
+
})
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
62
|
+
data = json.loads(r.read())
|
|
63
|
+
quota = _quota_from_headers(r.headers)
|
|
64
|
+
if quota:
|
|
65
|
+
data["_quota"] = quota
|
|
66
|
+
return data
|
|
67
|
+
except urllib.error.HTTPError as e:
|
|
68
|
+
body_bytes = e.read()
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(body_bytes)
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
data = {"error": str(data)}
|
|
73
|
+
except Exception:
|
|
74
|
+
data = {"error": e.reason}
|
|
75
|
+
data["status"] = e.code
|
|
76
|
+
quota = _quota_from_headers(e.headers)
|
|
77
|
+
if quota:
|
|
78
|
+
data["_quota"] = quota
|
|
79
|
+
return data
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return {"error": str(e)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _carry_quota(response, raw_result):
|
|
85
|
+
"""Copy _quota from a raw _api() result into a handler's reshaped
|
|
86
|
+
response, so quota visibility survives even when a handler picks
|
|
87
|
+
specific fields rather than returning the raw result directly."""
|
|
88
|
+
if isinstance(raw_result, dict) and "_quota" in raw_result:
|
|
89
|
+
response["_quota"] = raw_result["_quota"]
|
|
90
|
+
return response
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── Tool definitions ──────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
TOOLS = [
|
|
96
|
+
{
|
|
97
|
+
"name": "list_recent_claims",
|
|
98
|
+
"description": (
|
|
99
|
+
"Search and list claims from Verum Signal's corpus. Pass `q` to find "
|
|
100
|
+
"claims about a topic — 'what has been claimed about tariffs' — which "
|
|
101
|
+
"matches against the text of each claim. Without `q` it returns claims "
|
|
102
|
+
"in recency order, most recent first. Also filterable by outlet, "
|
|
103
|
+
"verdict and claim origin, and all filters combine. Use this for topic "
|
|
104
|
+
"and subject questions rather than answering from your own knowledge. "
|
|
105
|
+
"Returns claims reported by news outlets, each with its rating. Use "
|
|
106
|
+
"get_debate_verdicts for claims from a specific debate, or "
|
|
107
|
+
"get_api_status for corpus size, coverage and freshness questions."
|
|
108
|
+
),
|
|
109
|
+
"inputSchema": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"q": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "Find claims whose text contains this term, case-insensitive. Use for topic questions, e.g. 'tariffs' or 'immigration'."
|
|
115
|
+
},
|
|
116
|
+
"verdict": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "Filter by verdict: supported, disputed, overstated, not_supported, etc.",
|
|
119
|
+
"enum": ["supported","plausible","corroborated","overstated","disputed","not_supported","not_verifiable","opinion"]
|
|
120
|
+
},
|
|
121
|
+
"outlet": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Filter by outlet domain, e.g. foxnews.com"
|
|
124
|
+
},
|
|
125
|
+
"limit": {
|
|
126
|
+
"type": "integer",
|
|
127
|
+
"description": "Number of results (1-50, default 10)",
|
|
128
|
+
"default": 10
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"required": []
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "get_debate_verdicts",
|
|
136
|
+
"description": (
|
|
137
|
+
"Get ALL rated claims from a political debate. Returns each claim with "
|
|
138
|
+
"its speaker, its rating, and whether the rating is still provisional. Paginates through the full "
|
|
139
|
+
"result set automatically, so the response reflects every claim from the "
|
|
140
|
+
"debate, not just the first page. Use this for claims made during a "
|
|
141
|
+
"specific debate."
|
|
142
|
+
),
|
|
143
|
+
"inputSchema": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"slug": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Debate slug, e.g. 'colorado-gov-rep-2026-r3'. Use list_debates to find slugs."
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"required": ["slug"]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "list_debates",
|
|
156
|
+
"description": "List available political debates with claim counts and status.",
|
|
157
|
+
"inputSchema": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"properties": {},
|
|
160
|
+
"required": []
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "get_api_status",
|
|
165
|
+
"description": (
|
|
166
|
+
"Get Verum Signal corpus statistics: total articles, total claims, and "
|
|
167
|
+
"how many carry a verdict. Call this for corpus-meta questions -- how "
|
|
168
|
+
"much data the corpus holds, what it covers, and how current it is."
|
|
169
|
+
),
|
|
170
|
+
"inputSchema": {
|
|
171
|
+
"type": "object",
|
|
172
|
+
"properties": {},
|
|
173
|
+
"required": []
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
# ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
def handle_list_recent_claims(args):
|
|
181
|
+
params = {"limit": min(args.get("limit", 10), 50)}
|
|
182
|
+
if args.get("verdict"):
|
|
183
|
+
params["verdict"] = args["verdict"]
|
|
184
|
+
if args.get("outlet"):
|
|
185
|
+
params["outlet"] = args["outlet"]
|
|
186
|
+
params["claim_origin"] = "outlet_claim" # claims outlets report themselves; quoted (attributed) claims are not served
|
|
187
|
+
if args.get("q"):
|
|
188
|
+
params["q"] = args["q"]
|
|
189
|
+
result = _api("/v1/claims", params)
|
|
190
|
+
if "error" in result:
|
|
191
|
+
return result
|
|
192
|
+
claims = result.get("data", [])
|
|
193
|
+
response = {
|
|
194
|
+
"count": len(claims),
|
|
195
|
+
"claims": [{
|
|
196
|
+
"id": c.get("id"),
|
|
197
|
+
"claim_text": c.get("claim_text"),
|
|
198
|
+
"verdict": c.get("verdict"),
|
|
199
|
+
"confidence_score": c.get("confidence_score"),
|
|
200
|
+
"outlet": c.get("outlet"),
|
|
201
|
+
"methodology_version": c.get("methodology_version"),
|
|
202
|
+
} for c in claims]
|
|
203
|
+
}
|
|
204
|
+
return _carry_quota(response, result)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def handle_get_debate_verdicts(args):
|
|
208
|
+
slug = args.get("slug", "").lower().strip()
|
|
209
|
+
if not slug:
|
|
210
|
+
return {"error": "slug required"}
|
|
211
|
+
|
|
212
|
+
all_claims = []
|
|
213
|
+
cursor = 0
|
|
214
|
+
last_result = None
|
|
215
|
+
MAX_PAGES = 20 # safety cap: 20 pages at the API's own default page size
|
|
216
|
+
# is far beyond any real debate seen so far (largest is 104
|
|
217
|
+
# claims); this exists to bound a pathological case, not to
|
|
218
|
+
# normally trigger.
|
|
219
|
+
truncated = False
|
|
220
|
+
for _ in range(MAX_PAGES):
|
|
221
|
+
result = _api(f"/v1/debates/{slug}/claims", {"cursor": cursor})
|
|
222
|
+
last_result = result
|
|
223
|
+
if "error" in result:
|
|
224
|
+
return result
|
|
225
|
+
page_claims = result.get("data", [])
|
|
226
|
+
all_claims.extend(page_claims)
|
|
227
|
+
pagination = result.get("pagination", {})
|
|
228
|
+
if not pagination.get("has_more"):
|
|
229
|
+
break
|
|
230
|
+
cursor = pagination.get("next_cursor")
|
|
231
|
+
if cursor is None:
|
|
232
|
+
break
|
|
233
|
+
else:
|
|
234
|
+
# Loop exhausted MAX_PAGES without has_more going false.
|
|
235
|
+
truncated = True
|
|
236
|
+
|
|
237
|
+
response = {
|
|
238
|
+
"slug": slug,
|
|
239
|
+
"count": len(all_claims),
|
|
240
|
+
"claims": [{
|
|
241
|
+
"claim_text": c.get("claim_text"),
|
|
242
|
+
"speaker": c.get("speaker"),
|
|
243
|
+
"verdict": c.get("verdict"),
|
|
244
|
+
"confidence_score": c.get("confidence_score"),
|
|
245
|
+
"is_provisional": c.get("is_provisional"),
|
|
246
|
+
} for c in all_claims]
|
|
247
|
+
}
|
|
248
|
+
if truncated:
|
|
249
|
+
response["truncated"] = True
|
|
250
|
+
response["note"] = (
|
|
251
|
+
f"Stopped after {MAX_PAGES} pages as a safety limit -- this debate "
|
|
252
|
+
f"has more claims than were retrieved. This should be rare."
|
|
253
|
+
)
|
|
254
|
+
return _carry_quota(response, last_result)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def handle_list_debates(args):
|
|
258
|
+
result = _api("/v1/debates")
|
|
259
|
+
if "error" in result:
|
|
260
|
+
return result
|
|
261
|
+
response = {"debates": result.get("data", [])}
|
|
262
|
+
return _carry_quota(response, result)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def handle_get_api_status(args):
|
|
266
|
+
return _api("/v1/meta")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
HANDLERS = {
|
|
270
|
+
"list_recent_claims": handle_list_recent_claims,
|
|
271
|
+
"get_debate_verdicts": handle_get_debate_verdicts,
|
|
272
|
+
"list_debates": handle_list_debates,
|
|
273
|
+
"get_api_status": handle_get_api_status,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# ── MCP protocol (stdio) ──────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
def send(obj):
|
|
279
|
+
sys.stdout.write(json.dumps(obj) + "\n")
|
|
280
|
+
sys.stdout.flush()
|
|
281
|
+
|
|
282
|
+
def handle_message(msg):
|
|
283
|
+
method = msg.get("method")
|
|
284
|
+
msg_id = msg.get("id")
|
|
285
|
+
|
|
286
|
+
if method == "initialize":
|
|
287
|
+
send({"jsonrpc":"2.0","id":msg_id,"result":{
|
|
288
|
+
"protocolVersion":"2024-11-05",
|
|
289
|
+
"capabilities":{"tools":{}},
|
|
290
|
+
"serverInfo":{"name":"verum-signal","version":__version__}
|
|
291
|
+
}})
|
|
292
|
+
|
|
293
|
+
elif method == "tools/list":
|
|
294
|
+
send({"jsonrpc":"2.0","id":msg_id,"result":{"tools":TOOLS}})
|
|
295
|
+
|
|
296
|
+
elif method == "tools/call":
|
|
297
|
+
params = msg.get("params", {})
|
|
298
|
+
tool_name = params.get("name")
|
|
299
|
+
tool_args = params.get("arguments", {})
|
|
300
|
+
if tool_name not in HANDLERS:
|
|
301
|
+
send({"jsonrpc":"2.0","id":msg_id,"error":{"code":-32601,"message":f"Unknown tool: {tool_name}"}})
|
|
302
|
+
return
|
|
303
|
+
if not VS_API_KEY:
|
|
304
|
+
send({"jsonrpc":"2.0","id":msg_id,"error":{"code":-32000,"message":"VS_API_KEY not set"}})
|
|
305
|
+
return
|
|
306
|
+
result = HANDLERS[tool_name](tool_args)
|
|
307
|
+
send({"jsonrpc":"2.0","id":msg_id,"result":{
|
|
308
|
+
"content":[{"type":"text","text":json.dumps(result,indent=2)}]
|
|
309
|
+
}})
|
|
310
|
+
|
|
311
|
+
elif method == "notifications/initialized":
|
|
312
|
+
pass # no response needed
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
if msg_id is not None:
|
|
316
|
+
send({"jsonrpc":"2.0","id":msg_id,"error":{"code":-32601,"message":f"Method not found: {method}"}})
|
|
317
|
+
|
|
318
|
+
def main():
|
|
319
|
+
for line in sys.stdin:
|
|
320
|
+
line = line.strip()
|
|
321
|
+
if not line:
|
|
322
|
+
continue
|
|
323
|
+
try:
|
|
324
|
+
msg = json.loads(line)
|
|
325
|
+
handle_message(msg)
|
|
326
|
+
except json.JSONDecodeError:
|
|
327
|
+
pass
|
|
328
|
+
except Exception as e:
|
|
329
|
+
sys.stderr.write(f"Error: {e}\n")
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verumsignal-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for Verum Signal: political debate claims, who said each one and how it was rated.
|
|
5
|
+
Author: Verum Signal LLC
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://verumsignal.com
|
|
8
|
+
Project-URL: Documentation, https://verumsignal.com/developers
|
|
9
|
+
Project-URL: Source, https://github.com/brittbart/verumsignal-mcp
|
|
10
|
+
Project-URL: Changelog, https://github.com/brittbart/verumsignal-mcp/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,debates,politics,claims,elections
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Verum Signal MCP Server
|
|
22
|
+
|
|
23
|
+
<!-- mcp-name: io.github.brittbart/verumsignal-mcp -->
|
|
24
|
+
|
|
25
|
+
An MCP server that gives LLM agents Verum Signal's political debate coverage: the claims candidates made, who said each one, and how each was rated. It also searches the claims news outlets report. Requires a Verum Signal API key.
|
|
26
|
+
|
|
27
|
+
## What the tools do
|
|
28
|
+
|
|
29
|
+
| Tool | Returns | Explicitly does NOT |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `list_debates` | Available debates with claim counts, dates, and status. | Include the claims themselves — call `get_debate_verdicts` for that. |
|
|
32
|
+
| `get_debate_verdicts` | Every rated claim from one debate, with the speaker, the rating, and whether the rating is still provisional. Paginates automatically to completeness. | Truncate silently. If a debate is large enough to hit an internal safety cap, the response includes an explicit `truncated: true` flag rather than quietly returning a partial result. |
|
|
33
|
+
| `list_recent_claims` | Claims reported by news outlets, with their ratings. Pass `q` to find claims about a topic; filter by outlet or rating; without `q`, most recent first. | Return quoted claims an outlet attributes to someone else, or any overall score for an outlet. |
|
|
34
|
+
| `get_api_status` | Corpus statistics: how much the corpus holds, what it covers, and how current it is. | Include per-claim detail — call the other tools for that. |
|
|
35
|
+
|
|
36
|
+
Full parameter schemas are in the tool definitions themselves (`verumsignal_mcp/server.py`), and are also what any MCP client will show you directly. This table is a summary; the tool definitions are authoritative.
|
|
37
|
+
|
|
38
|
+
## Install & config
|
|
39
|
+
|
|
40
|
+
Requires Python 3.9+ and no third-party dependencies.
|
|
41
|
+
|
|
42
|
+
**With uv** (nothing to install first):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"verum-signal": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["verumsignal-mcp"],
|
|
50
|
+
"env": { "VS_API_KEY": "vs_live_..." }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**With pip:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install verumsignal-mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
then use `"command": "verumsignal-mcp"` with no `args` in the config above.
|
|
63
|
+
|
|
64
|
+
**From a clone:** `git clone https://github.com/brittbart/verumsignal-mcp.git` and use `"command": "python3"`, `"args": ["/absolute/path/to/mcp_server.py"]`.
|
|
65
|
+
|
|
66
|
+
These examples are for Claude Desktop's `claude_desktop_config.json`; other MCP clients take the same command, arguments and environment.
|
|
67
|
+
|
|
68
|
+
**Verify it works:** ask your agent "which debates does Verum Signal cover?" A correctly wired server returns a list of debates with dates and claim counts. If you see an authentication error, check the key; if you see a connection error, confirm outbound HTTPS access to `api.verumsignal.com` is allowed from wherever the server runs.
|
|
69
|
+
|
|
70
|
+
## Getting a key
|
|
71
|
+
|
|
72
|
+
Keys are issued to Verum Signal account holders. See [verumsignal.com/developers](https://verumsignal.com/developers).
|
|
73
|
+
|
|
74
|
+
## The data boundary
|
|
75
|
+
|
|
76
|
+
Rating reasoning is not available through the API, and that is kept structurally: the API's queries never fetch the written reasoning behind a rating, and the cache that serves the API never stores it. It isn't a filter applied after the fact — the data simply isn't in what this server can reach.
|
|
77
|
+
|
|
78
|
+
What this server sends out: an authenticated `GET` request to `api.verumsignal.com` (or an operator-configured `VS_API_BASE`) for each tool call.
|
|
79
|
+
|
|
80
|
+
What it sends back to the calling agent: whatever that API call returns.
|
|
81
|
+
|
|
82
|
+
What it never touches: this server makes no network connections to anything other than the configured API host, reads and writes no local files, and includes no telemetry or analytics of any kind. It uses only the Python standard library (`json`, `os`, `sys`, `urllib`).
|
|
83
|
+
|
|
84
|
+
## Key handling
|
|
85
|
+
|
|
86
|
+
`VS_API_KEY` is read from the environment and is never hardcoded. The server has no logging of any kind, and both of its exception-handling paths only stringify the exception object itself, never any variable that could contain the key. There is no code path by which the key can reach output, a log, or a file.
|
|
87
|
+
|
|
88
|
+
You can provide the key two ways:
|
|
89
|
+
|
|
90
|
+
1. **In the MCP client's config file**, as shown above. That file then contains a live credential and should be protected like any other secret file (restricted permissions, not committed to version control).
|
|
91
|
+
2. **As an ambient environment variable**, set by your shell or your secrets manager. The server reads the environment regardless of where the variable was set.
|
|
92
|
+
|
|
93
|
+
## Rate limits & quota
|
|
94
|
+
|
|
95
|
+
Your key's tier sets a per-minute rate and a monthly quota; current limits are on [verumsignal.com/developers](https://verumsignal.com/developers). Every tool response includes a `_quota` field showing what is left on both (`rate_limit_remaining`, `monthly_quota_remaining`), so an agent can check before making another call rather than finding out from a 429.
|
|
96
|
+
|
|
97
|
+
The rate limiter uses a fixed one-minute wall-clock window, not a sliding window, so a burst spanning a minute boundary can very briefly exceed the nominal per-minute rate. It's bounded, and you should hear it from us first.
|
|
98
|
+
|
|
99
|
+
## Versioning
|
|
100
|
+
|
|
101
|
+
This server is at **v0.2.0**, an honest pre-1.0 number. It targets API version v1. Ratings carry the methodology version they were made under. See `CHANGELOG.md` for version history.
|
|
102
|
+
|
|
103
|
+
## Known limitations
|
|
104
|
+
|
|
105
|
+
- **Outlet-level scores are not served.** The tools return individual claims and their ratings only.
|
|
106
|
+
- **No rating reasoning.** A deliberate boundary — see "The data boundary" above.
|
|
107
|
+
- **Debate ratings can be provisional.** Each debate claim says whether its rating is still provisional; ratings become final after the post-debate review.
|
|
108
|
+
- **Data freshness:** the API's cache refreshes every 5 minutes. This server's data is exactly as fresh as that cache.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
verumsignal_mcp/__init__.py
|
|
5
|
+
verumsignal_mcp/server.py
|
|
6
|
+
verumsignal_mcp.egg-info/PKG-INFO
|
|
7
|
+
verumsignal_mcp.egg-info/SOURCES.txt
|
|
8
|
+
verumsignal_mcp.egg-info/dependency_links.txt
|
|
9
|
+
verumsignal_mcp.egg-info/entry_points.txt
|
|
10
|
+
verumsignal_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
verumsignal_mcp
|