Skip to main content
Glama
Luissalet

LinksHoard MCP Server

by Luissalet

Links Hoard

Local-first read-later bookmark library: save a URL, get the extracted article text, tag it, highlight passages and search everything — stored in a single SQLite file on your own computer and exposed to an assistant through MCP.

Spanish version: README.es.md.

Run

Requires Node.js 22.13 or later (it uses the built-in node:sqlite). No native modules, no Docker.

npm install
npm run build
npm start          # http://127.0.0.1:5181

npm run open starts the server and opens the browser on Windows. npm run dev runs the API (node --watch) and Vite together with the /api proxy configured automatically.

The server binds to 127.0.0.1 only. If port 5181 is busy it walks up to the next free port and prints the address; set PORT_STRICT=1 to fail instead.

Environment variables

Variable

Purpose

LINKS_PORT / PORT

Preferred port (default 5181).

PORT_STRICT=1

Do not fall back to another port.

LINKS_DATA_DIR

Data folder (default <repo>/data, gitignored). Contains links-hoard.db and mcp-token.

LINKS_ALLOWED_HOSTS

Extra host names accepted behind a tunnel (see below).

LINKS_URL

MCP bridge: base URL of the running app (default http://127.0.0.1:5181). Must be local.

LINKS_TOKEN_FILE / LINKS_TOKEN

MCP bridge: where to read the bearer token (default <data dir>/mcp-token).

Access from your phone (behind a tunnel)

The server binds 127.0.0.1 and only answers requests whose Host is localhost, 127.0.0.1 or [::1]. To reach it from your phone through a tunnel that fronts the app (a private mesh network, a reverse proxy), list the extra host names in LINKS_ALLOWED_HOSTS, comma-separated, exact names or *.suffix: LINKS_ALLOWED_HOSTS=my-pc.example,*.ts.net. Port and letter case are ignored, and the Origin of API calls must resolve to one of those hosts too (any scheme or port). Cross-site fetches are still refused; opening the app from another page (a link, a bookmarklet, the share sheet) is a normal navigation and works.

Related MCP server: xmcp-core

What it does

  • Save instantly. Paste a URL (or use the bookmarklet, the browser share sheet once installed as a PWA, or an assistant's save_link tool) and it is stored right away with fetch_status: pending. A background queue (2 at a time) downloads the page, extracts the article with Readability and fills in title, byline, excerpt and full text — the row updates itself, no reload needed.

  • Read cleanly. The reader shows the extracted text at an adjustable font size, with the original URL, site, author and reading time. Select any text to highlight it, with an optional note.

  • Organize. Tags, favorites, archive, read/unread. Sidebar shows tag counts; the list can filter by site too.

  • Search everything. Full-text search (SQLite FTS5) over title, description, extracted text, notes and tags. If the runtime's SQLite build lacks FTS5 the app falls back to a plain LIKE search automatically and says so in Ajustes.

  • Import in bulk. Netscape bookmarks HTML (what every browser exports) or a plain list of URLs, one per line. Both dedupe against what you already have.

  • Weekly digest. GET /api/digest?since= (and the link_digest MCP tool) lists what was saved since a date, grouped by site, with excerpts.

  • Install as an app. manifest.webmanifest declares a share_target, so once installed on Android you can share a page from any app straight into Links Hoard.

URL normalization (the dedupe key)

Saving is idempotent on a normalized form of the URL: utm_*, fbclid, gclid and similar tracking params are stripped, the fragment is dropped, the host is lowercased and a trailing slash on the path is removed. Saving an already-saved page (even with different tracking params) returns the existing link with existing: true instead of duplicating it.

Connect an assistant

In Ajustes (or via faustus-plugin.json) an assistant configured for local MCP servers over stdio can connect using server/mcp.js, LINKS_URL and LINKS_TOKEN_FILE. The bridge never opens the database itself: every call is proxied over HTTP to the running app, authenticated with a random token written fresh to <data dir>/mcp-token at every startup.

Tools:

Tool

Use

save_link

Save a URL (idempotent); waits up to 10 s for the fetch so it can report the real title/excerpt.

list_links

List by state (unread/read/archived/all), tag, site, since.

search_links

Full-text search.

read_link

Read the extracted text, paginated by characters; includes highlights.

tag_link

Add/remove tags.

mark_link

Toggle read/unread/archived/favorite.

add_highlight

Save a highlighted quote with a note.

link_digest

What was saved since a date, grouped by site.

refetch_link

Re-download and re-extract.

delete_link

Permanently delete (destructive; confirm first).

list_tags

Every tag in use, with counts.

11 tools in total. GET /api/agent/tools always reflects the live list. Tool descriptions end with a Sinónimos: line of Spanish words, so a Spanish-speaking user's phrasing ("guarda esto", "resumen de la semana") matches the right tool.

The assistant is instructed to summarize or quote a link only from the text read_link returns, never from the title alone, and to say plainly when a fetch is still pending or failed rather than guessing.

Data and limits

  • data/links-hoard.db: links, highlights and settings, SQLite with WAL.

  • data/mcp-token: local credential created at startup; not published or included anywhere else.

  • Fetch: 15 s timeout, a browser-like User-Agent header, 5 MB body cap. HTML goes through linkedom + @mozilla/readability, with a manual fallback (title + meta description + stripped body) when Readability finds nothing usable. Before extraction, common boilerplate (infoboxes, navboxes, sidebars, reference markers, tables of contents, edit-section links, <nav>/<aside>) is stripped from the page, and the remaining HTML is converted to text block-by-block (a newline after each paragraph/heading/list item/table row, a space between table cells) so text never gets glued together across cells or blocks the way plain textContent would. The excerpt picks the first real paragraph (≥ 80 characters with sentence punctuation) rather than whatever text happens to come first in the markup, such as an infobox. PDFs and images are recorded with a filename-derived title (no OCR, no rendering). YouTube/Vimeo get an oEmbed title with no API key required. A link saved before this extraction logic improved can be fixed up in place with refetch_link / POST /api/links/:id/refetch (or the "Reintentar descarga" button in the reader) — it re-runs the current extraction code against the same URL and overwrites the stored title, excerpt and text.

  • Search: FTS5 when the runtime's SQLite build supports it (verified at every startup); otherwise a LIKE fallback across the same fields, both reported in GET /api/state.

Verification

npm test
npm run build

Tests use temporary data directories and a local HTTP server for fixtures — nothing touches your real data or the network. They cover URL normalization, Readability extraction against an HTML fixture, save idempotency, full-text search, the digest, highlights, bookmarks/URL-list import parsing, agent token auth, and a full API round-trip.

Design and decisions: DESIGN.md.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables AI assistants to save, search, and manage bookmarks with semantic search, automatic metadata extraction, and optional LLM-powered enrichment, all running locally.
    8
    6
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A generic X/bookmark intelligence MCP core for ingesting, enriching, and retrieving bookmarks with semantic search, brief generation, and academic paper scanning, exposed as MCP tools.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Turns saved X posts and long-form articles into a private, searchable context base accessible via MCP. Lets coding agents import, search, read, and enrich the library using the user's existing browser session.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    A local, read-only MCP server that makes bookmarks from browsers, read-later apps, and export files accessible to AI assistants, enabling search, triage, and summarization without sending data anywhere.
    8
    38 npm
    MIT