MCP server for full-text search and thread reading on LynxChan imageboards (endchan.org, kohlchan.net, …). Read-only.
  • Python 91.1%
  • Nix 8.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Aleksandr 0817d1d2f9 0.3.0: безопасность адресов, устойчивость разбора и тесты
Ad-hoc инстанс из параметра `instance` принимается только как публичный
https-адрес: loopback, приватные, link-local и зарезервированные адреса
отклоняются, включая случай, когда на них указывает имя хоста или ведёт
редирект. Редиректы поэтому разбираются вручную — адрес проверяется на
каждом шаге. Снять проверку для своего инстанса в локальной сети можно
переменной LYNXCHAN_MCP_ALLOW_PRIVATE.

Кривая запись в каталоге доски больше не роняет поиск целиком: каталог и
список досок разбираются так же защищённо, как тред. Числовые поля
переживают нечисловое значение, таймстамп без смещения считается UTC.

Бюджет времени поиска проверяется и в фазе каталогов — раньше он покрывал
только дозагрузку тел тредов, и поиск мог идти вдвое дольше заявленного.
Слово запроса матчится по началу слова: `ru` больше не находит `true`,
а словоформы ловятся по-прежнему. Бонус за фразу не зависит от пунктуации.

Кэш ограничен объёмом, а не числом записей, тело больше 8 МБ не
скачивается; переменные окружения валидируются — MAX_CONCURRENT=0 больше
не вешает сервер. HTTP-клиент закрывается при остановке.

Пол mcp поднят до 1.19.0 — первый релиз с аргументом meta у декоратора
FastMCP.tool, без которого сервер падал на импорте.

Тесты: 89 штук, без сети, гоняются и в nix build.

Co-Authored-By: Eva
2026-07-31 21:44:55 +03:00
nix 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
src/lynxchan_mcp 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
tests 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
.gitignore Ядро сервера: конфиг, HTTP-клиент, модели, поиск, MCP-инструменты 2026-07-31 16:03:30 +00:00
AGENTS.md 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
CHANGELOG.md 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
flake.lock flake.lock: зафиксированы версии nixpkgs и flake-utils 2026-07-31 16:28:37 +00:00
flake.nix 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
pyproject.toml 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00
README.md 0.3.0: безопасность адресов, устойчивость разбора и тесты 2026-07-31 21:44:55 +03:00

LynxChan MCP Server

MCP (Model Context Protocol) server for searching and reading threads on imageboards powered by the LynxChan engine (endchan.org by default; any other via config or a plain URL).

LynxChan has no server-side full-text search, and boards have no archive — so this server finds "where was X discussed" by scanning board catalogs and, when needed, fetching thread bodies to match reply text. Read-only: it never posts, reports or logs in.

Tools

Tool What it does
search_threads Full-text search across boards and instances. All words of the query must appear somewhere in the thread (OP or replies); a verbatim phrase scores higher. Words match at the start of a word, so cat finds cats but not concat. Filters: boards, instance, since_days, op_only, has_files, pinned, locked; sort by relevance or date; limit. Returns ranked threads — board, id, title, dates, reply/file counts, flags, URL — with snippets showing each match in context, plus coverage stats (boards/threads scanned, reply-scan budget).
read_thread Reads a whole thread by board + thread_id: OP and replies in order with dates, plain-text bodies and attachments (original name, MIME, size, direct URL). max_posts caps the replies shown.
get_catalog Lists a board's live threads — pinned first, then by last bump — with counters, flags and URLs.
list_boards Lists boards of the configured instances: URI, name, description, posts per hour.
list_instances Lists the configured instances (names usable as the instance parameter).

All tools are read-only (readOnlyHint) and call out to the open web (openWorldHint).

How the search works

  1. Board catalogs (/<board>/catalog.json) are fetched in parallel — the OP text of every live thread comes free with the catalog.
  2. Threads whose OP already covers all query words are scored immediately.
  3. For the rest, thread bodies (/<board>/res/<id>.json) are fetched in parallel — newest first — until every word is matched or the budget runs out. Budgets: a wall-clock limit (default 45 s) and a cap on fetched thread bodies (default 250 per instance). Both phases check the clock between batches of requests, so the budget bounds the whole search, not just the reply scan. When a budget truncates the scan, the answer says so.
  4. Scoring: a word in the subject > in the OP > in a reply; extra bonus for the verbatim multi-word phrase and for recent bumps. Results are sorted by relevance (then freshness) or by date.

A query word matches at the start of a word: поиск finds поиска and cat finds cats, but neither matches concat — word forms are caught without a stemmer while accidental substrings are not.

Cached catalogs/threads make repeated and follow-up searches cheap.

Instances

Instances come from the LYNXCHAN_MCP_INSTANCES environment variable — a JSON list of {"name", "url"}:

[
  {"name": "endchan",  "url": "https://endchan.org"},
  {"name": "kohlchan", "url": "https://kohlchan.net"}
]

Without the variable, endchan.org is the built-in default (public and verified against this server). The instance parameter of every tool accepts either a configured name or a base URL (https://…, a bare host works too) for an ad-hoc instance — no config change needed. Note that some LynxChan sites sit behind anti-bot interstitials (e.g. 8chan.moe) and cannot be scraped.

Ad-hoc instances must be public https:// addresses. The instance argument is chosen by a model that reads untrusted text from imageboards, so a thread cannot talk it into pointing the server at 127.0.0.1 or a private network: loopback, private, link-local and reserved addresses are refused, including after a redirect and including hostnames that resolve to them. To run against a LynxChan instance inside your own network, list it in LYNXCHAN_MCP_INSTANCES and set LYNXCHAN_MCP_ALLOW_PRIVATE=1.

Environment variables

Variable Default Meaning
LYNXCHAN_MCP_INSTANCES endchan JSON list of instances (name, url)
LYNXCHAN_MCP_USER_AGENT lynxchan-mcp/<ver> (+<repo>) User-Agent sent to instances
LYNXCHAN_MCP_TIMEOUT 20 Total request timeout, seconds
LYNXCHAN_MCP_CONNECT_TIMEOUT 8 Connect timeout, seconds
LYNXCHAN_MCP_MAX_CONCURRENT 4 Max simultaneous requests per host
LYNXCHAN_MCP_MAX_RESPONSE_BYTES 8388608 Largest response body accepted from an instance
LYNXCHAN_MCP_CACHE_MAX_BYTES 67108864 Total size of the response cache
LYNXCHAN_MCP_MAX_REDIRECTS 5 Redirect hops followed per request
LYNXCHAN_MCP_ALLOW_PRIVATE 0 Allow private/loopback addresses (own instance in a LAN)
LYNXCHAN_MCP_CACHE_TTL 300 Cache TTL for board lists and catalogs, seconds
LYNXCHAN_MCP_THREAD_CACHE_TTL 600 Cache TTL for full threads, seconds
LYNXCHAN_MCP_SEARCH_BUDGET 45 Wall-clock search budget per call, seconds
LYNXCHAN_MCP_SEARCH_MAX_FETCHES 250 Max thread bodies fetched per instance per search
LYNXCHAN_MCP_SEARCH_MAX_BOARDS 20 Max boards scanned per instance when boards is unset
LYNXCHAN_MCP_MAX_LIMIT 50 Hard cap on the limit parameter
LYNXCHAN_MCP_RETRIES 1 Retries on network errors (never on HTTP errors)
LYNXCHAN_MCP_TRANSPORT stdio Default transport for the CLI
LYNXCHAN_MCP_HOST / LYNXCHAN_MCP_PORT 127.0.0.1 / 8080 Default bind for HTTP transports

Running

From a checkout (uv / pip)

uv sync                 # or: pip install .
lynxchan-mcp                                # stdio transport (default)
lynxchan-mcp --transport streamable-http --host 127.0.0.1 --port 8080
# equivalent: python -m lynxchan_mcp [args]

Register the stdio variant with your MCP client, e.g.:

{
  "mcpServers": {
    "lynxchan": {
      "command": "lynxchan-mcp"
    }
  }
}

Nix flake

nix run github:your/lynxchan-mcp            # stdio
nix build github:your/lynxchan-mcp          # package in ./result, runs the tests
nix develop                                 # dev shell: python, uv, ruff, pytest

Tests

nix develop -c pytest        # or: pytest, with the package installed
nix develop -c ruff check .

NixOS module

The flake exports nixosModules.default (alias nixosModules.lynxchan-mcp):

{
  inputs.lynxchan-mcp.url = "github:your/lynxchan-mcp";

  outputs = { nixpkgs, lynxchan-mcp, ... }: {
    nixosConfigurations.host = nixpkgs.lib.nixosSystem {
      modules = [
        lynxchan-mcp.nixosModules.default
        {
          services.lynxchan-mcp = {
            enable = true;
            host = "127.0.0.1";
            port = 8080;
            # memoryMax = "512M";
            # instances = [ { name = "endchan"; url = "https://endchan.org"; } ];
            # environmentFile = config.sops.secrets.lynxchan-mcp-env.path;
            # extraEnvironment.LYNXCHAN_MCP_SEARCH_BUDGET = "60";
            # dataDir = "lynxchan-mcp";  # only if you want /var/lib/lynxchan-mcp
          };
        }
      ];
    };
  };
}

The service runs as a dynamic user with strict sandboxing (no capabilities, ProtectSystem=strict, no devices, AF_INET/AF_INET6 only) under a memory limit (memoryMax, 512M by default). It keeps no on-disk state; StateDirectory is created only when dataDir is set.

Politeness towards the boards

We are guests on other people's servers. Deliberate choices, enforced in http_client.py:

  • Honest User-Agent with the project URL, so admins can identify and contact us (LYNXCHAN_MCP_USER_AGENT overrides).
  • Hard timeouts — total 20 s, connect 8 s — so a dead instance never stalls a search.
  • Per-host concurrency cap of 4 simultaneous requests, however wide the search fans out.
  • TTL cache — board lists and catalogs for 5 minutes, thread bodies for 10 — repeated searches don't re-hit the server.
  • One retry, and only on network errors. HTTP errors (4xx/5xx) are reported, never retried — no retry storms.
  • Search budgets (45 s / 250 bodies per instance), checked between batches in both search phases, bound the total load one query can cause.
  • Size caps — a response body above 8 MB is refused rather than pulled in full, and the cache is bounded by total size.

LynxChan API notes

  • There is no JSON endpoint listing boards — the list is parsed from the /boards.js page (a.linkBoard, span.divDescription, span.labelPostsPerHour), with /index.json topBoards as a fallback.
  • /<board>/catalog.json — array of thread summaries. Older engines (e.g. endchan) omit fileCount; counters then show reply counts only.
  • /<board>/res/<id>.json — the full thread: OP fields at the top level, replies in posts[]. The OP has no postId — it equals the thread id.

License

MIT