Radius 0.1.0

Radius is a self-hosted MCP server that makes the twitter/X bookmarks you have already saved searchable by an AI agent.

Your links land in a database you own. Claude — or any MCP client — queries them over authenticated HTTP.

It's built on ④ pieces. This page covers setup, the tools, how auth works, and shipping it to production.

Piece 01 Ingestion Your export upserts into the database, safe to re-run Piece 02 Turso One hosted database, the single source of truth Piece 03 Search FTS5 ranking, with fuzzy matching for near misses Piece 04 Auth RS256 tokens, scoped to one tool at a time
01 — Overview

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
Ingestion
You bring the JSON export; radius upserts it, safe to re-run
Storage
One Turso database — authors, tweets and media as real tables
Search
FTS5 lexical ranking and rapidfuzz similarity
Auth
RS256 JWTs signed by a local key pair, with per-tool scopes
Hosting
Locally, or as a single Python function on Vercel
02 — Setup

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" }]
    }
  ]
}
03 — For agents

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.

Paste into your agent
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.

04 — Connect

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"
POST /mcp
The MCP endpoint. Bearer token required. On Vercel this is /api/mcp
GET /health
Liveness check. Public
GET /.well-known/jwks.json
Published verification keys. Public

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.

05 — Tools

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.

fetch_bookmarks
Top-k search over the corpus. Takes query, search_mode (exact or fuzzy) and top_k from 1 to 50. Returns id, handle, display name, content, created_at and a score where higher is better
whoami
Echoes the client, scopes, issuer and expiry behind the calling token — the fastest way to confirm a client authenticates as who you think it does

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.

06 — Auth

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
Layer one
Tokens that are missing, malformed, expired, wrongly signed, or carrying the wrong issuer or audience are rejected before any tool code runs
Layer two
A valid token only opens the tools its scope covers. Others are hidden from tools/list, and calling one reports “unknown tool” rather than “forbidden”
Revocation
All-or-nothing. Cutting off one token early means rotating the key and reissuing for every client, so keep lifetimes short
07 — Commands

Everything runs through one entrypoint.

radius serve
Run the MCP server
radius ingest [file]
Load bookmarks from a raw JSON export. Applies the schema first
radius migrate
Apply the schema on its own. Prints the connection mode it resolved
radius keys init
Generate the signing key pair and JWKS. --force rotates and invalidates every issued token
radius keys show
Print the public JWKS
radius token issue
Mint a token. -c names the client, -s grants a scope, --ttl sets the lifetime
radius token inspect
Verify a token and print its claims
08 — Deploy

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.

09 — When it breaks

The five things that actually go wrong.

401 on everything
Issuer and audience are compared exactly, so a token minted before you changed JWT_ISSUER — or before keys init --force — will never verify again. radius token inspect shows what it carries
Ingest worked, search is empty
The rows went to a different database. Run radius migrate where you ingested and where you serve, and compare the mode each prints
fts5: syntax error
Exact mode passes your query to FTS5 verbatim, so + - : ^ and unbalanced quotes are parse errors. Quote the term, or use fuzzy
Fuzzy finds nothing
It keeps only matches scoring 85 or better. Below that was noise, so there is no partial-credit tier — try exact with a shorter keyword
Ingestion takes minutes
Every write is a round trip to Turso. A backfill pays that once; incremental runs finish in seconds. A closer Turso region is the cheapest fix
10 — Help

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.