abyss-mcp
Provides web and academic paper search through a SearXNG instance, returning URL and snippet per result. Supports categories such as general or science, result limiting, and filtering of blocked domains.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@abyss-mcp search the web for what zram is and give me a few short results"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
llm-abyss
Opinionated tooling around the abyss of LLMs: composable, async-first retrieval steps, servable as MCP tools.
The repo has two parts:
abyss, the library. Small components (searchers, filters, and soon crawlers, chunkers and rankers) that snap together into streaming pipelines with>>. The core depends only onhttpx.Local serving. SearXNG in Docker plus a llama.cpp server whose web UI can call abyss pipelines as tools. See docs/DEPLOY.md.
Install
uv add "llm-abyss @ git+https://github.com/NISH1001/llm-abyss" # core
uv add "llm-abyss[mcp] @ git+https://github.com/NISH1001/llm-abyss" # + MCP serving and SettingsThe distribution is llm-abyss; the import is abyss. Try it without installing anything, against any
SearXNG instance with the JSON format enabled:
uv run --with "llm-abyss @ git+https://github.com/NISH1001/llm-abyss" python -c '
import asyncio
from abyss import SearxngSearcher, NotBlocked, Take
search = SearxngSearcher("http://127.0.0.1:4243") >> NotBlocked() >> Take(3)
for c in asyncio.run(search.run("what is zram")):
print(c.url, "|", c.text)'Related MCP server: jupsearch-mcp
How it composes
Everything is a Step: something that turns an async stream of items into another async stream. >>
chains steps into a new step, and building a chain runs nothing.
search = SearxngSearcher(url) >> NotBlocked() >> Take(5)
chunks = await search.run("zram vs zswap") # list of everything
async for chunk in search.stream("zram vs zswap"): # items as they arrive
...Items stream. A step passes each result on the moment it is ready, so item 1 can be three steps downstream while item 5 is still in the first.
Take(n)stops early. Once it has n items it closes the stream and cancels the work still running upstream.Concurrency lives in the step. Async per-item steps run their items concurrently, with an optional
concurrency=cap.Plain functions chain too. A function becomes a
Map, sosearch >> (lambda c: c.url)works.
Data: one Chunk type
Every stage passes the same small record:
@dataclass
class Chunk:
url: str
text: str # snippet after search; page or window text after later stages
title: str = ""
score: float = 0.0
meta: dict = field(default_factory=dict) # query, engine, rank, date, ...Searchers tag each chunk with meta["query"], so later steps (a ranker) can read the query from the
stream instead of needing it passed in.
Building blocks
Step | Shape | Notes |
| 1 → 1 | returning |
| 1 → N |
|
| 1 → 0..1 |
|
| N → n | cancels upstream once full |
| source and sink, used by |
Components
Component | Shape | Status |
| query → N chunks | base class: implement |
| query → N chunks |
|
| query → N chunks | grep-style scan of local files, no index; whole file as text |
| filter | drops social and video sites |
| filter | drops stub pages and empty snippets |
| 1 → 1 | cuts text to the words around the best match for |
Crawler, Chunker, Ranker | chunk → chunk(s) | planned |
Example: searching a folder of notes
DocumentSearcher is grep-style: every query scans the files, so there is no index to build or keep fresh.
On its own it returns every matching file, best first, each with the whole file as its text:
from abyss import DocumentSearcher, MinWords, Take, Window
searcher = DocumentSearcher("~/notes", globs=("pages/**/*.md", "journals/**/*.md"))
chunks = await searcher.run("zram swap") # every matching file, whole pages
for c in chunks[:3]:
print(c.score, c.url, c.meta["terms"])Chain steps onto it to shape what comes out. Each >> returns a new Chain; the searcher itself is
unchanged, and nothing runs until .run() or .stream():
kb = searcher >> MinWords(7) >> Take(3) >> Window(64)
chunks = await kb.run("zram swap") # 3 chunks of about 64 words
async for chunk in kb.stream("zram swap"): # or one at a time, as they arrive
print(chunk.url, chunk.text)
print(kb) # DocumentSearcher('/home/you/notes', globs=(...)) >> MinWords(7) >> Take(3) >> Window(64)Step | What it does here |
| finds the files that contain the query words, best first |
| drops stub pages that are only a title or a tag |
| keeps the first three, then stops the stream |
| cuts each page to the 64 words around its best match for the query |
Order matters. MinWords before Take judges whole pages and lets Take fill up from the next results.
Window after Take cuts only the pages that are kept.
A chain is a plain value: build it once and run it on any query, or branch one searcher into several chains:
whole_pages = searcher >> MinWords(7) >> Take(5)
excerpts = searcher >> MinWords(7) >> Take(3) >> Window(64) # what kb_search servesHow matching and ordering work:
Matching is case-insensitive at word starts, so "swap" finds "swapfile" and "helm" skips "overwhelm". With
include_title=True(the default) the file name is searched along with the body.Order is a simple tally: more distinct query words first, then more hits, plus a bonus for words in the file name. Very long pages that mention everything can rank high; a ranking step that accounts for page length (BM25) is planned to sit between the searcher and
Take.Windowskips query words already in the page's title when it picks the excerpt. On a page named "Zram", the query "zram setup" shows the part about setup rather than the parts that just repeat "zram".
Write your own component
Subclass the role and implement one method. Chaining, streaming and concurrency come from the base class.
from abyss import Chunk, Searcher, Take
class NotesSearcher(Searcher):
async def search(self, query: str) -> list[Chunk]:
return [Chunk(url="file:///notes.md", text=f"my notes about {query}")]
notes = NotesSearcher() >> Take(5) # chains like any other stepFor a step that is not a searcher, subclass Map, FlatMap or Filter, or subclass Step and implement
__call__(items) -> AsyncIterator.
Serve steps as MCP tools
Needs the [mcp] extra. Any step or chain becomes a tool that takes one query: str and returns plain
text, one result per block as URL plus text. Input and output stay minimal so the model's context stays
small.
# my_server.py
from abyss import NotBlocked, SearxngSearcher, Settings, Take
from abyss.mcp import MCP
s = Settings() # .env and env vars
web = SearxngSearcher(s.searxng_url) >> NotBlocked() >> Take(s.search_top_k)
papers = SearxngSearcher(s.searxng_url, categories="science") >> Take(3)
mcp = MCP("abyss")
mcp.add(web, name="web_search", description="Search the web. Returns URL and snippet per result.")
mcp.add(papers, name="paper_search", description="Search academic papers. Returns URL and snippet per result.")
if __name__ == "__main__":
mcp.run() # stdioPoint any MCP client at python my_server.py. Pass render= to add to control the output text.
The package also ships a stock server, abyss-mcp <group>, built from Settings: abyss-mcp web serves web
search over SearXNG, and abyss-mcp kb serves grep-style search over a Logseq graph (LOGSEQ_KB).
Settings
abyss.settings.Settings (pydantic-settings, in the [mcp] extra) holds project-level defaults. It reads
.env from the working directory; environment variables win; unknown keys are ignored. Components never
read it: they take constructor arguments, and whatever assembles them reads Settings and passes values in.
Setting | Default | Used by |
|
| search |
|
| search |
|
| search |
|
| search |
| unset | path of a Logseq graph; enables the |
|
|
|
|
| log per-call details to stderr |
|
| crawler (planned) |
|
|
|
|
| chunker (planned) |
Local serving
SearXNG runs in Docker, llama-server runs on the host in tmux, and llama-server spawns abyss-mcp itself
over stdio, so there is no MCP port.
cp .env.example .env # set SEARXNG_SECRET
uv sync --extra mcp
docker compose up -d # SearXNG
deploy/llm.sh start # llama-server with the web_search toolEverything else, including settings, remote access over Tailscale and troubleshooting, is in docs/DEPLOY.md.
Development
uv sync --extra mcp
uv run pytest # parallel via xdist; -n 0 for a quick serial run
uv run ruff check abyss tests && uv run ruff format abyss testsLicense
MIT
Available Tools
1 toolsearchA
Search the web for current information. Returns the top results as URL and snippet.
Inputs: query (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 1 tool
There is only one tool, so there is no possibility of an agent confusing it with another action. Its purpose ('search the web') is unambiguous.
The single name 'search' is a clean, conventional verb choice, but with only one tool there is no pattern to demonstrate consistency against.
A one-tool server is inherently thin; even for a narrow web-search purpose, a single tool leaves no room for query variants, site scoping, or content retrieval.
Search returning only URLs and snippets covers the stated core purpose, but there is no way to fetch or read page contents, which is a common dead end for an agent needing current information.
Maintenance
Related MCP Connectors
Live AI-native web search with citations. One tool for every MCP client. Flat per-request pricing.
A hosted MCP suite providing workflow primitives: web search, content fetching, compression, deduplication, cost/budget management, and agent memory pipelines.
LLM-ready web search + instant answers + URL-to-clean-text fetch for agents and RAG.
Search and inspect source-pinned agent skills, plugins and tools through a free public MCP.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables MCP-compatible agents to perform web searches via the agent-web-search engine, returning ranked results with sources.1-
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients to run unified web searches across multiple providers such as Exa, Serper, Tavily, Firecrawl, and Jina, fusing results with reciprocal rank fusion, and to extract readable content from public web pages, PDFs, and video pages as Markdown. Provider chains are configurable, failures are isolated per provider, and responses report attempted, succeeded, failed, and skipped channels with source tracing and honest warnings.MIT
- AlicenseAqualityCmaintenanceExposes DeepSeek's web search and page-fetch tools to any MCP client, letting any LLM run searches with quality signals like rerank score and source authority, plus advanced query syntax. It also retrieves single pages as Markdown, with built-in throttling, retries, and size-conscious summaries.2133 npm1MIT
- AlicenseAqualityBmaintenanceEnables MCP clients to run web searches and fetch pages, returning structured search results and Markdown page content through two tools.2MIT