Bookmarks are a bet on your future self that usually loses.
A flat list, no real search, no way to see how anything relates to anything else. Radius makes that bet pay off: ask an agent what have I saved about X and get an answer grounded in your own saved data.
It pulls your bookmarks on a schedule using a browser session you already established, stores them as real relational tables rather than a JSON blob, and exposes that corpus to any MCP-speaking client over authenticated HTTP. No third-party API access, no password ever stored or scripted, one database you own.
bookmark → ingestion → Turso (FTS5) → search → MCP (JWT) → agent
bookmark → ingestion
↓
Turso (FTS5) → search
↓
MCP (JWT) → agent
Five commands and a database you own.
Needs Python 3.12+ and uv. Radius never scripts a login and never stores a password, which has a consequence worth knowing before you start: it does not ship an exporter. You bring the JSON, from whatever tool already holds your session — a browser extension, a userscript, a cookie-based CLI. The shape it has to be in is below.
# install git clone https://github.com/ashishk1331/radius && cd radius uv sync cp .env.example .env.local # the key that signs your tokens uv run radius keys init # the corpus — put the URL and token into .env.local turso db create radius turso db show radius --url turso db tokens create radius # load your bookmarks (applies the schema first) uv run radius ingest bookmarks.json # run it, then check: curl 127.0.0.1:9000/health uv run radius serve
Paste the Turso URL and token into .env.local before ingesting. Skip it and radius writes to a local file instead — nothing fails, the rows just are not where you expect. radius migrate prints which database it resolved. Leave the URL unset on purpose and that local file is the whole setup, which is what offline work and the test suite use.
The JSON it expects
A top-level data array. Extra fields are ignored. Ingestion upserts on id, so re-running over an overlapping export is safe.
{
"data": [
{
"id": "2059675872408260816",
"text": "the bookmark text",
"createdAt": "Wed May 27 16:39:29 +0000 2026",
"author": {
"id": "…", "screenName": "handle",
"name": "Display Name", "profileImageUrl": "…"
},
"media": [{ "url": "…", "type": "photo" }]
}
]
}
Or hand the whole setup to an agent.
Everything above is the human path. If you would rather not run it yourself, paste the block below into Claude Code — or any agent with shell access. It has the repository, the order of operations, the two questions the agent needs to ask you, and how to tell that it worked.
Set up radius for me — a self-hosted MCP server that makes my saved
twitter/X bookmarks searchable. Repo: https://github.com/ashishk1331/radius
Read AGENTS.md in the repo before you start, then work through this:
1. git clone https://github.com/ashishk1331/radius && cd radius
2. uv sync
3. cp .env.example .env.local
4. uv run radius keys init
5. Ask me for a Turso database URL and auth token, and write them into
.env.local as TURSO_DATABASE_URL and TURSO_AUTH_TOKEN. If I do not
have one, skip this — radius falls back to a local file.
6. uv run radius migrate
7. Ask me where my twitter/X bookmark JSON export is, then
uv run radius ingest <that path>
8. uv run radius token issue -c <a short name for this client>
9. uv run radius serve
10. Register the server with my MCP client, passing the token from
step 8 as an Authorization: Bearer header against
http://127.0.0.1:9000/mcp
Then call the whoami tool and show me what it returns. It should report
my client name and the bookmarks:read scope.
If you get a 401, JWT_ISSUER in .env.local has to match the URL the
client is calling — that is almost always the cause. Do not commit
.env.local or anything under keys/.The agent only needs two things from you: the Turso credentials and the path to your export. Everything else it can read from AGENTS.md, which documents the commands, the layout, and the failure modes worth knowing about.
Mint a token, point a client at it.
Nothing about an issued token is stored server-side. It carries an expiry and a scope, and that is the whole of it.
TOKEN=$(uv run radius token issue -c claude-code --ttl 2592000 | tail -2 | head -1) claude mcp add --transport http radius http://127.0.0.1:9000/mcp \ --header "Authorization: Bearer $TOKEN"
The --ttl above is 30 days; leave it off and you get an hour. When a token lapses the connector just stops working and nothing says why. And radius serve has to stay running for a local client to reach it — deploy it if you would rather not babysit a terminal.
Two tools, one scope.
You do not call these yourself. Ask your agent in plain language — what have I saved about local-first software? — and it picks the tool, fills the arguments, and reads the results back to you.
Exact ranks inside the database with FTS5 bm25 and returns in about 0.2s warm; the query is FTS5 syntax, so characters it treats as operators need quoting. Fuzzy transfers rows and scores them in process, keeping only matches at 85 or above — slower, and the right choice for approximate or misspelled wording.
Radius is its own token issuer.
One local RSA key pair signs every token, the public half is published as a JWKS, and the server verifies against it. There are no credentials in the database.
radius token issue ──signs with──▶ keys/private.pem
│
client ──Bearer JWT──▶ MCP ──verifies with──▶ keys/public.pem
│
▼
require_scopes per tool
radius token issue │ signs with ▼ keys/private.pem client ─Bearer JWT─▶ MCP │ verifies with ▼ keys/public.pem │ ▼ require_scopes per tool
Everything runs through one entrypoint.
One Python function, and no port required.
Vercel's own MCP documentation is TypeScript-only, but none of it is needed — the Python runtime runs ASGI apps and runs the lifespan protocol, which is the one thing FastMCP's session manager requires.
Code and data ship on separate tracks. Ingestion runs locally against Turso, so new bookmarks are live the moment the job finishes; the deployment only changes when the code does.
npx vercel link
npx vercel env add TURSO_DATABASE_URL production
npx vercel env add TURSO_AUTH_TOKEN production
npx vercel env add JWT_AUDIENCE production
npx vercel env add RADIUS_PUBLIC_KEY_PEM production < keys/public.pem
npx vercel --prod
# then set JWT_ISSUER to the deployed URL and redeploy
Only the public key reaches Vercel. The private key never leaves your machine, so tokens can only ever be minted locally. A 401 with a WWW-Authenticate header on the first request is the signal that the whole chain came up.
The five things that actually go wrong.
Stuck, or built something with it? Tell me.
Radius is a personal project rather than a supported product, but I read everything. For anything reproducible, an issue with the command you ran and what came back is the fastest route to a fix. For a quick question, X is easier.