cablegram-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.
- cablegram_mcp-0.1.0/.github/workflows/publish.yml +28 -0
- cablegram_mcp-0.1.0/.github/workflows/tests.yml +21 -0
- cablegram_mcp-0.1.0/.gitignore +20 -0
- cablegram_mcp-0.1.0/.python-version +1 -0
- cablegram_mcp-0.1.0/LICENSE +21 -0
- cablegram_mcp-0.1.0/PKG-INFO +152 -0
- cablegram_mcp-0.1.0/README.md +138 -0
- cablegram_mcp-0.1.0/deploy/README.md +18 -0
- cablegram_mcp-0.1.0/deploy/cablegram-poll.service +11 -0
- cablegram_mcp-0.1.0/deploy/cablegram-poll.timer +10 -0
- cablegram_mcp-0.1.0/docs/design.md +134 -0
- cablegram_mcp-0.1.0/pyproject.toml +30 -0
- cablegram_mcp-0.1.0/src/cablegram/__init__.py +0 -0
- cablegram_mcp-0.1.0/src/cablegram/archive.py +324 -0
- cablegram_mcp-0.1.0/src/cablegram/cli.py +105 -0
- cablegram_mcp-0.1.0/src/cablegram/cls.py +136 -0
- cablegram_mcp-0.1.0/src/cablegram/fetch.py +197 -0
- cablegram_mcp-0.1.0/src/cablegram/hn.py +98 -0
- cablegram_mcp-0.1.0/src/cablegram/poll.py +165 -0
- cablegram_mcp-0.1.0/src/cablegram/render.py +370 -0
- cablegram_mcp-0.1.0/src/cablegram/rss.py +245 -0
- cablegram_mcp-0.1.0/src/cablegram/server.py +268 -0
- cablegram_mcp-0.1.0/src/cablegram/sources.py +173 -0
- cablegram_mcp-0.1.0/src/cablegram/store.py +556 -0
- cablegram_mcp-0.1.0/src/cablegram/telegram.py +200 -0
- cablegram_mcp-0.1.0/src/cablegram/urls.py +169 -0
- cablegram_mcp-0.1.0/tests/samples/cls_subject.json +36 -0
- cablegram_mcp-0.1.0/tests/samples/hn_search.json +55 -0
- cablegram_mcp-0.1.0/tests/samples/telegram_channel.html +126 -0
- cablegram_mcp-0.1.0/tests/test_archive.py +183 -0
- cablegram_mcp-0.1.0/tests/test_cli.py +71 -0
- cablegram_mcp-0.1.0/tests/test_cls.py +158 -0
- cablegram_mcp-0.1.0/tests/test_fetch.py +223 -0
- cablegram_mcp-0.1.0/tests/test_hn.py +126 -0
- cablegram_mcp-0.1.0/tests/test_identity.py +259 -0
- cablegram_mcp-0.1.0/tests/test_poll.py +224 -0
- cablegram_mcp-0.1.0/tests/test_queries.py +172 -0
- cablegram_mcp-0.1.0/tests/test_render.py +265 -0
- cablegram_mcp-0.1.0/tests/test_rss.py +436 -0
- cablegram_mcp-0.1.0/tests/test_server.py +221 -0
- cablegram_mcp-0.1.0/tests/test_sources.py +61 -0
- cablegram_mcp-0.1.0/tests/test_store.py +647 -0
- cablegram_mcp-0.1.0/tests/test_telegram.py +235 -0
- cablegram_mcp-0.1.0/tests/test_urls.py +176 -0
- cablegram_mcp-0.1.0/uv.lock +716 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed. No API token anywhere:
|
|
4
|
+
# PyPI's trusted publishing verifies the workflow's identity directly, so
|
|
5
|
+
# there is no secret to leak, rotate, or accidentally commit.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write # this is what trusted publishing checks
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
# The same suite the tests workflow runs. A release that cannot pass it
|
|
23
|
+
# should not exist, and a tag is the last place to find that out.
|
|
24
|
+
- run: uv sync --locked
|
|
25
|
+
- run: uv run pytest -q
|
|
26
|
+
|
|
27
|
+
- run: uv build
|
|
28
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
# 3.12 is the floor the project declares; 3.14 is what a fresh install
|
|
13
|
+
# gets. The identity of an archived item must not differ between them.
|
|
14
|
+
python-version: ["3.12", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --locked
|
|
21
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Levon Gabrielyan
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cablegram-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Raw dispatches from 19 tech, AI and Chinese/Russian sources. Date-filtered only, never ranked.
|
|
5
|
+
Project-URL: Homepage, https://github.com/levongabrielyan/cablegram-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/levongabrielyan/cablegram-mcp
|
|
7
|
+
Author: Levon Gabrielyan
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,mcp,model-context-protocol,news,rss,tech-signals
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: mcp>=2.1.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cablegram-mcp
|
|
16
|
+
|
|
17
|
+
Raw dispatches from tech, AI and Chinese/Russian sources — filtered by date,
|
|
18
|
+
never ranked.
|
|
19
|
+
|
|
20
|
+
A *cablegram* was the unedited message that arrived over the submarine cables,
|
|
21
|
+
before an editor turned it into a story. This server is the cable: it brings the
|
|
22
|
+
dispatches. Your model is the editor.
|
|
23
|
+
|
|
24
|
+
**The point is the cable, not the news.** A launch discussed in Chinese or
|
|
25
|
+
Russian today reaches English-language coverage days later, filtered through
|
|
26
|
+
whoever decided it was worth translating — and often it never arrives at all.
|
|
27
|
+
Nineteen sources in three languages, read directly, put a reader in California
|
|
28
|
+
in the same week as a reader in Shanghai or Moscow.
|
|
29
|
+
|
|
30
|
+
Headlines are never translated. Each dispatch carries its language, and the
|
|
31
|
+
model reading it has more context for that than any translation step would.
|
|
32
|
+
It also means the same story is kept in every language that carried it: OpenAI
|
|
33
|
+
titles a post *"Pacing model development in an era of cyber-critical
|
|
34
|
+
capabilities"* while a Russian channel titles the same URL *"OpenAI stopped RL
|
|
35
|
+
for two weeks on its latest models"*. Both are stored against the same id, and
|
|
36
|
+
either can be searched.
|
|
37
|
+
|
|
38
|
+
A full day of all nineteen costs a few thousand tokens. Six hours of them,
|
|
39
|
+
grouped by source with the cuts declared, is around 700.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
CABLEGRAM v0.1 | 2026-08-30T09:09Z..2026-08-30T14:09Z | 11 items | 19/19 sources
|
|
43
|
+
CUT habr=2/4 hn=2/159 kr36=2/3 (newest kept)
|
|
44
|
+
COLS id hh:mm title times UTC | body: wire_read(ids=[...])
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## cls zh early,finance 1/1
|
|
48
|
+
-- 08-30
|
|
49
|
+
e60e27faa7fc 10:04 AI数据中心扩张“限制性因素”浮现 马斯克:SpaceX正铸造燃气轮机叶片
|
|
50
|
+
|
|
51
|
+
## data_secrets ru telegram 1/1
|
|
52
|
+
-- 08-30
|
|
53
|
+
a50d137e3147 10:49 Агенты OpenAI одну за одной автономно создали три цивилизации
|
|
54
|
+
|
|
55
|
+
## hn en community,searchable 2/159
|
|
56
|
+
-- 08-30
|
|
57
|
+
2929f114895f 14:06 METR and Redwood Offer Postmortem of the HuggingFace Hack (thezvi.wordpress.com)
|
|
58
|
+
7901d3fb4e1c 14:04 Google removed the URLs. Only for the people who resell them (scraping.club)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
*Five hours of nineteen sources, verbatim. `CUT` says what was left out and how
|
|
62
|
+
much there was; `19/19` is how many answered.*
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Status
|
|
66
|
+
|
|
67
|
+
v0.1 — the nineteen sources work; the tool API may still move. All nineteen sources have an adapter and were verified
|
|
68
|
+
against the live endpoints: eleven RSS feeds, Hacker News through its search
|
|
69
|
+
index, a signed Chinese financial API, and six public Telegram channels.
|
|
70
|
+
|
|
71
|
+
Two of them are worth knowing about before you rely on them:
|
|
72
|
+
|
|
73
|
+
* **cls.cn is reverse-engineered.** An undocumented internal API with a signed
|
|
74
|
+
request. It holds 3.34 days at most and cannot page backwards, so a gap in
|
|
75
|
+
polling is permanent. `wire_sources` marks it `fragile`.
|
|
76
|
+
* **Telegram is HTML with no contract.** The public preview view can change
|
|
77
|
+
without a version number to notice it by.
|
|
78
|
+
|
|
79
|
+
Both are declared in the output rather than explained afterwards.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
Requires Python 3.12+.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/levongabrielyan/cablegram-mcp
|
|
87
|
+
cd cablegram-mcp
|
|
88
|
+
uv sync
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Register it with an MCP client — for Claude Code:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add cablegram --scope user -- \
|
|
95
|
+
/path/to/cablegram-mcp/.venv/bin/python -m cablegram.cli serve
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then fill the archive, and keep filling it:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cablegram poll # once, now
|
|
102
|
+
cablegram sources # what it knows about
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Feeds expose a window of days, so put `cablegram poll` on a timer — an hour
|
|
106
|
+
nobody polls is an hour no endpoint will serve again. A systemd user unit is in
|
|
107
|
+
[`deploy/`](deploy/).
|
|
108
|
+
|
|
109
|
+
## The four tools
|
|
110
|
+
|
|
111
|
+
| Tool | Question |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `wire_latest` | What did the sources publish in the last N hours? |
|
|
114
|
+
| `wire_read` | Give me the stored text of these ids |
|
|
115
|
+
| `wire_search` | When did this term start appearing? |
|
|
116
|
+
| `wire_sources` | What exists, and what is currently broken? |
|
|
117
|
+
|
|
118
|
+
All four are read-only and return plain text. The same information as JSON with
|
|
119
|
+
indent costs roughly six times the tokens and truncates.
|
|
120
|
+
|
|
121
|
+
## The local archive
|
|
122
|
+
|
|
123
|
+
RSS feeds expose only their last few dozen entries; last week is unrecoverable.
|
|
124
|
+
So everything fetched is stored in a SQLite file under your platform's data
|
|
125
|
+
directory (`~/.local/share/cablegram/archive.db` on Linux; override with
|
|
126
|
+
`CABLEGRAM_DB`).
|
|
127
|
+
|
|
128
|
+
It is created on first run and grows by a few megabytes a year. Nothing is
|
|
129
|
+
uploaded anywhere, and no seed database ships with this repository — the server
|
|
130
|
+
fetches on your behalf and does not redistribute anyone's content.
|
|
131
|
+
|
|
132
|
+
**`wire_search` only reads what this archive holds, which starts the day you
|
|
133
|
+
first ran it.** Zero hits means "not in what we can search", never "nobody is
|
|
134
|
+
talking about it", and the output says so on every reply.
|
|
135
|
+
|
|
136
|
+
## Design
|
|
137
|
+
|
|
138
|
+
[`docs/design.md`](docs/design.md) covers why the identity of an item is a pure
|
|
139
|
+
function of its URL, why a failure is a value rather than an exception, why the
|
|
140
|
+
full-text index needs a trigram tokenizer for Chinese to work at all, and what
|
|
141
|
+
this server deliberately does not do.
|
|
142
|
+
|
|
143
|
+
## Notes
|
|
144
|
+
|
|
145
|
+
Only public endpoints are used: no credentials, no authentication bypass,
|
|
146
|
+
no scraping behind a login. Conditional requests (`ETag`, `If-Modified-Since`)
|
|
147
|
+
mean an unchanged feed is not re-downloaded. Intended for personal research —
|
|
148
|
+
respect each source's terms of service.
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
MIT. Built by [Levon Gabrielyan](https://github.com/levongabrielyan).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# cablegram-mcp
|
|
2
|
+
|
|
3
|
+
Raw dispatches from tech, AI and Chinese/Russian sources — filtered by date,
|
|
4
|
+
never ranked.
|
|
5
|
+
|
|
6
|
+
A *cablegram* was the unedited message that arrived over the submarine cables,
|
|
7
|
+
before an editor turned it into a story. This server is the cable: it brings the
|
|
8
|
+
dispatches. Your model is the editor.
|
|
9
|
+
|
|
10
|
+
**The point is the cable, not the news.** A launch discussed in Chinese or
|
|
11
|
+
Russian today reaches English-language coverage days later, filtered through
|
|
12
|
+
whoever decided it was worth translating — and often it never arrives at all.
|
|
13
|
+
Nineteen sources in three languages, read directly, put a reader in California
|
|
14
|
+
in the same week as a reader in Shanghai or Moscow.
|
|
15
|
+
|
|
16
|
+
Headlines are never translated. Each dispatch carries its language, and the
|
|
17
|
+
model reading it has more context for that than any translation step would.
|
|
18
|
+
It also means the same story is kept in every language that carried it: OpenAI
|
|
19
|
+
titles a post *"Pacing model development in an era of cyber-critical
|
|
20
|
+
capabilities"* while a Russian channel titles the same URL *"OpenAI stopped RL
|
|
21
|
+
for two weeks on its latest models"*. Both are stored against the same id, and
|
|
22
|
+
either can be searched.
|
|
23
|
+
|
|
24
|
+
A full day of all nineteen costs a few thousand tokens. Six hours of them,
|
|
25
|
+
grouped by source with the cuts declared, is around 700.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
CABLEGRAM v0.1 | 2026-08-30T09:09Z..2026-08-30T14:09Z | 11 items | 19/19 sources
|
|
29
|
+
CUT habr=2/4 hn=2/159 kr36=2/3 (newest kept)
|
|
30
|
+
COLS id hh:mm title times UTC | body: wire_read(ids=[...])
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## cls zh early,finance 1/1
|
|
34
|
+
-- 08-30
|
|
35
|
+
e60e27faa7fc 10:04 AI数据中心扩张“限制性因素”浮现 马斯克:SpaceX正铸造燃气轮机叶片
|
|
36
|
+
|
|
37
|
+
## data_secrets ru telegram 1/1
|
|
38
|
+
-- 08-30
|
|
39
|
+
a50d137e3147 10:49 Агенты OpenAI одну за одной автономно создали три цивилизации
|
|
40
|
+
|
|
41
|
+
## hn en community,searchable 2/159
|
|
42
|
+
-- 08-30
|
|
43
|
+
2929f114895f 14:06 METR and Redwood Offer Postmortem of the HuggingFace Hack (thezvi.wordpress.com)
|
|
44
|
+
7901d3fb4e1c 14:04 Google removed the URLs. Only for the people who resell them (scraping.club)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
*Five hours of nineteen sources, verbatim. `CUT` says what was left out and how
|
|
48
|
+
much there was; `19/19` is how many answered.*
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Status
|
|
52
|
+
|
|
53
|
+
v0.1 — the nineteen sources work; the tool API may still move. All nineteen sources have an adapter and were verified
|
|
54
|
+
against the live endpoints: eleven RSS feeds, Hacker News through its search
|
|
55
|
+
index, a signed Chinese financial API, and six public Telegram channels.
|
|
56
|
+
|
|
57
|
+
Two of them are worth knowing about before you rely on them:
|
|
58
|
+
|
|
59
|
+
* **cls.cn is reverse-engineered.** An undocumented internal API with a signed
|
|
60
|
+
request. It holds 3.34 days at most and cannot page backwards, so a gap in
|
|
61
|
+
polling is permanent. `wire_sources` marks it `fragile`.
|
|
62
|
+
* **Telegram is HTML with no contract.** The public preview view can change
|
|
63
|
+
without a version number to notice it by.
|
|
64
|
+
|
|
65
|
+
Both are declared in the output rather than explained afterwards.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
Requires Python 3.12+.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/levongabrielyan/cablegram-mcp
|
|
73
|
+
cd cablegram-mcp
|
|
74
|
+
uv sync
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Register it with an MCP client — for Claude Code:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add cablegram --scope user -- \
|
|
81
|
+
/path/to/cablegram-mcp/.venv/bin/python -m cablegram.cli serve
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then fill the archive, and keep filling it:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cablegram poll # once, now
|
|
88
|
+
cablegram sources # what it knows about
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Feeds expose a window of days, so put `cablegram poll` on a timer — an hour
|
|
92
|
+
nobody polls is an hour no endpoint will serve again. A systemd user unit is in
|
|
93
|
+
[`deploy/`](deploy/).
|
|
94
|
+
|
|
95
|
+
## The four tools
|
|
96
|
+
|
|
97
|
+
| Tool | Question |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `wire_latest` | What did the sources publish in the last N hours? |
|
|
100
|
+
| `wire_read` | Give me the stored text of these ids |
|
|
101
|
+
| `wire_search` | When did this term start appearing? |
|
|
102
|
+
| `wire_sources` | What exists, and what is currently broken? |
|
|
103
|
+
|
|
104
|
+
All four are read-only and return plain text. The same information as JSON with
|
|
105
|
+
indent costs roughly six times the tokens and truncates.
|
|
106
|
+
|
|
107
|
+
## The local archive
|
|
108
|
+
|
|
109
|
+
RSS feeds expose only their last few dozen entries; last week is unrecoverable.
|
|
110
|
+
So everything fetched is stored in a SQLite file under your platform's data
|
|
111
|
+
directory (`~/.local/share/cablegram/archive.db` on Linux; override with
|
|
112
|
+
`CABLEGRAM_DB`).
|
|
113
|
+
|
|
114
|
+
It is created on first run and grows by a few megabytes a year. Nothing is
|
|
115
|
+
uploaded anywhere, and no seed database ships with this repository — the server
|
|
116
|
+
fetches on your behalf and does not redistribute anyone's content.
|
|
117
|
+
|
|
118
|
+
**`wire_search` only reads what this archive holds, which starts the day you
|
|
119
|
+
first ran it.** Zero hits means "not in what we can search", never "nobody is
|
|
120
|
+
talking about it", and the output says so on every reply.
|
|
121
|
+
|
|
122
|
+
## Design
|
|
123
|
+
|
|
124
|
+
[`docs/design.md`](docs/design.md) covers why the identity of an item is a pure
|
|
125
|
+
function of its URL, why a failure is a value rather than an exception, why the
|
|
126
|
+
full-text index needs a trigram tokenizer for Chinese to work at all, and what
|
|
127
|
+
this server deliberately does not do.
|
|
128
|
+
|
|
129
|
+
## Notes
|
|
130
|
+
|
|
131
|
+
Only public endpoints are used: no credentials, no authentication bypass,
|
|
132
|
+
no scraping behind a login. Conditional requests (`ETag`, `If-Modified-Since`)
|
|
133
|
+
mean an unchanged feed is not re-downloaded. Intended for personal research —
|
|
134
|
+
respect each source's terms of service.
|
|
135
|
+
|
|
136
|
+
## Licence
|
|
137
|
+
|
|
138
|
+
MIT. Built by [Levon Gabrielyan](https://github.com/levongabrielyan).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Running the poller on a timer
|
|
2
|
+
|
|
3
|
+
The archive is only worth what has been put in it, and feeds expose a window of
|
|
4
|
+
days: an hour nobody polls is an hour no endpoint will serve again. So the
|
|
5
|
+
poller belongs on a timer, independently of whether an MCP client is running.
|
|
6
|
+
|
|
7
|
+
Copy both files into `~/.config/systemd/user/`, adjust the path in the service,
|
|
8
|
+
and:
|
|
9
|
+
|
|
10
|
+
systemctl --user daemon-reload
|
|
11
|
+
systemctl --user enable --now cablegram-poll.timer
|
|
12
|
+
systemctl --user list-timers cablegram-poll.timer
|
|
13
|
+
|
|
14
|
+
`cablegram poll` exits non-zero when every source failed, so a failure is
|
|
15
|
+
visible to `systemctl --user status` rather than silent.
|
|
16
|
+
|
|
17
|
+
Any scheduler works — this is just the one with no daemon to install. Hourly is
|
|
18
|
+
comfortable for the RSS sources.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=cablegram: fetch every source once and archive it
|
|
3
|
+
|
|
4
|
+
[Service]
|
|
5
|
+
Type=oneshot
|
|
6
|
+
# Adjust to where you cloned it. `uv run` is used rather than a bare
|
|
7
|
+
# `cablegram` because the documented install (git clone + uv sync) leaves the
|
|
8
|
+
# entry point inside the project's own .venv and never on PATH — copying a
|
|
9
|
+
# %h/.local/bin path here earns a status=203/EXEC on the first run.
|
|
10
|
+
WorkingDirectory=%h/cablegram-mcp
|
|
11
|
+
ExecStart=/usr/bin/env uv run cablegram poll
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Design notes
|
|
2
|
+
|
|
3
|
+
Why this server is shaped the way it is. The code says what it does; this says
|
|
4
|
+
what it refused to do, and what went wrong on the way.
|
|
5
|
+
|
|
6
|
+
## The one assumption everything follows from
|
|
7
|
+
|
|
8
|
+
**A model reads this output. A person does not.**
|
|
9
|
+
|
|
10
|
+
That single fact decides most of what follows. A human operator notices when a
|
|
11
|
+
tool returns an empty list on a busy day, or when a summary reads oddly. Nobody
|
|
12
|
+
is watching here, so a wrong answer that looks well-formed is never caught. The
|
|
13
|
+
design is therefore biased, everywhere, towards making a failure *visible in the
|
|
14
|
+
payload itself* rather than towards making the payload tidy.
|
|
15
|
+
|
|
16
|
+
Concretely, the output states:
|
|
17
|
+
|
|
18
|
+
| Marking | Without it |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `DOWN cls=HTTP403` | An absent source cannot be known to exist; its silence reads as "nothing happened there" |
|
|
21
|
+
| `CUT hn=25/57` | An undeclared cut is indistinguishable from a source with little to say |
|
|
22
|
+
| `~a3f9c2e1` | A capture time presented as a publication time files the item under the wrong day |
|
|
23
|
+
| `PENDING` | A source with no adapter reported as broken buries the one that is actually broken |
|
|
24
|
+
| `body=description 240c` | Reporting *how much* of an article arrived, without guessing whether it is all of it |
|
|
25
|
+
| `"0 hits" does NOT mean nobody is talking about it` | The single most likely false conclusion this server can cause |
|
|
26
|
+
|
|
27
|
+
## Date-filtered, never ranked
|
|
28
|
+
|
|
29
|
+
The server brings dispatches; the model is the editor. Ranking would mean
|
|
30
|
+
deciding what matters, with far less context than the model has — and a ranked
|
|
31
|
+
list cannot be un-ranked by the reader, while an unranked one can be sorted.
|
|
32
|
+
|
|
33
|
+
Depth is a different matter: `detail="headlines"` trims how much of each item is
|
|
34
|
+
returned, uniformly and on request. Trimming depth is not selecting.
|
|
35
|
+
|
|
36
|
+
## Identity
|
|
37
|
+
|
|
38
|
+
`id = sha1(normalise(url))[:12]`, a pure function of the URL: no clock, no
|
|
39
|
+
network, no state. Two calls hours apart agree without the server remembering
|
|
40
|
+
anything.
|
|
41
|
+
|
|
42
|
+
Normalisation errs, everywhere, towards keeping URLs apart:
|
|
43
|
+
|
|
44
|
+
* **Merging two articles is permanent.** `url_norm` is UNIQUE, so the second one
|
|
45
|
+
never enters the archive and nothing reports it.
|
|
46
|
+
* **Splitting one article is a duplicate** — recoverable, though not harmless:
|
|
47
|
+
each split quietly lowers the cross-source count.
|
|
48
|
+
|
|
49
|
+
Both are bad; only one cannot be undone. Hence a *denylist* of tracking
|
|
50
|
+
parameters rather than an allowlist: an allowlist drops any key it has not heard
|
|
51
|
+
of, so `?sid=1` and `?sid=2` became the same id and the second article was
|
|
52
|
+
rejected in silence.
|
|
53
|
+
|
|
54
|
+
Twelve hex digits, not eight. At eight, a 50% chance of collision arrives at
|
|
55
|
+
77,000 items — about five months of these feeds — and the id is a PRIMARY KEY,
|
|
56
|
+
so a collision is a real article silently refused.
|
|
57
|
+
|
|
58
|
+
The archive records the recipe that produced its ids and refuses to open if it
|
|
59
|
+
changed. Otherwise every stored id would stop matching, every item would be
|
|
60
|
+
inserted again, and the archive would double while reporting nothing wrong.
|
|
61
|
+
|
|
62
|
+
## The archive is the point
|
|
63
|
+
|
|
64
|
+
Feeds expose a window of days and no history of their own. Everything else here
|
|
65
|
+
can be rewritten in an afternoon; this file is the only part whose value depends
|
|
66
|
+
on when it was started. It lives outside the project directory, under the
|
|
67
|
+
platform's data directory and never under a cache directory — caches are
|
|
68
|
+
declared deletable and cleaners act on that.
|
|
69
|
+
|
|
70
|
+
`sighting` is a separate table because `item` can only name whichever source got
|
|
71
|
+
there first. Without it the cross-source count reads 1 for everything, which
|
|
72
|
+
looks like a story nobody else picked up rather than a missing feature. Each
|
|
73
|
+
sighting keeps the headline that source used: one outlet writes 智谱 where
|
|
74
|
+
another writes Zhipu for the same link, and that pairing is the only bridge
|
|
75
|
+
between a Chinese story and an English query.
|
|
76
|
+
|
|
77
|
+
Full-text search uses `tokenize='trigram'`. With SQLite's default tokenizer
|
|
78
|
+
every Chinese query returns zero hits, silently — Chinese has no spaces, so a
|
|
79
|
+
whole headline becomes one token. Terms shorter than three characters cannot use
|
|
80
|
+
a trigram index at all, and the common Chinese company names are exactly two, so
|
|
81
|
+
those fall back to a substring scan. The output says which engine answered,
|
|
82
|
+
because their recall differs.
|
|
83
|
+
|
|
84
|
+
## Failure handling
|
|
85
|
+
|
|
86
|
+
* **A failure is a value, not an exception.** One source failing must not cost
|
|
87
|
+
the other eighteen, and the caller receives one result per source in the order
|
|
88
|
+
given — a source never disappears from the list because it failed.
|
|
89
|
+
* **The deadline cancels only what is still in flight.** Cancelling the batch
|
|
90
|
+
turned one slow feed into eleven dead ones, which nothing downstream could
|
|
91
|
+
tell apart from a real outage.
|
|
92
|
+
* **A 304 is a success.** The source answered and had nothing new. Counting it
|
|
93
|
+
as a failure turns a quiet week into a fake outage; it also must not overwrite
|
|
94
|
+
the last real result with a zero.
|
|
95
|
+
* **A failed poll never touches `last_ok` or the stored validators.** `last_ok`
|
|
96
|
+
is how anyone notices a source has been mute for days, and wiping an ETag
|
|
97
|
+
re-downloads the whole feed for good, with no error to show for it.
|
|
98
|
+
* **One entry, one transaction.** A single unparseable entry used to roll back
|
|
99
|
+
everything already written for that source.
|
|
100
|
+
* **A feed that parses to zero entries has its own state.** That is what a feed
|
|
101
|
+
looks like the day it changes format, and as a plain success it is
|
|
102
|
+
indistinguishable from a source with no news.
|
|
103
|
+
|
|
104
|
+
## Third-party input is hostile
|
|
105
|
+
|
|
106
|
+
Feeds are fetched from servers nobody here controls.
|
|
107
|
+
|
|
108
|
+
Entity expansion is bounded by measurement, not by inspecting declarations.
|
|
109
|
+
Three earlier guards each looked for the *shape* of the last attack — nested
|
|
110
|
+
declarations, a padded prologue, a comment hiding the DOCTYPE — and each left
|
|
111
|
+
the property open; a flat bomb (one entity, no nesting, referenced seven hundred
|
|
112
|
+
times) walked through all three. Fields are capped as a second line of defence,
|
|
113
|
+
the one that reaches the database and the index.
|
|
114
|
+
|
|
115
|
+
Responses are capped on bytes received rather than on `Content-Length`, which
|
|
116
|
+
can lie.
|
|
117
|
+
|
|
118
|
+
## Zero dependencies beyond the MCP SDK
|
|
119
|
+
|
|
120
|
+
Nineteen fixed sources and eleven RSS feeds do not justify a parser dependency:
|
|
121
|
+
a dependency earns its place when it encapsulates knowledge that shifts or that
|
|
122
|
+
fails silently. The SDK ships an HTTP client, and the standard library parses
|
|
123
|
+
XML and RFC-822 dates.
|
|
124
|
+
|
|
125
|
+
## What this server does not do
|
|
126
|
+
|
|
127
|
+
* It does not translate. Each dispatch carries its language, and the model
|
|
128
|
+
reading it has more context for that than any translation step would.
|
|
129
|
+
* It does not rank, score or deduplicate across sources beyond counting.
|
|
130
|
+
* It does not search the web. `wire_search` reads only what this archive holds,
|
|
131
|
+
which starts the day the server was first run.
|
|
132
|
+
* It does not fetch article bodies. What a feed ships is what is stored.
|
|
133
|
+
* Sources are fixed, not configurable. A configurable aggregator is a different
|
|
134
|
+
product; this one is tuned for one question.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cablegram-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Raw dispatches from 19 tech, AI and Chinese/Russian sources. Date-filtered only, never ranked."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Levon Gabrielyan" }]
|
|
9
|
+
keywords = ["mcp", "model-context-protocol", "rss", "news", "ai", "tech-signals"]
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp>=2.1.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
cablegram = "cablegram.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/levongabrielyan/cablegram-mcp"
|
|
20
|
+
Repository = "https://github.com/levongabrielyan/cablegram-mcp"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest>=8.0"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/cablegram"]
|
|
File without changes
|