Skip to content

TinySearch

TinySearch is a self-hosted web-retrieval layer for AI agents. Use fast, backend-ordered search to find pages, then inspect only the URLs worth putting in your model’s context.

TinySearch does not make another model call to write the final answer. MCP tools return XML-shaped context for your client model; the Python and FastAPI APIs return stable, JSON-serializable results for applications that need to inspect or transform them.

With uv installed, add TinySearch to your MCP client:

{
"mcpServers": {
"tinysearch": {
"command": "uvx",
"args": [
"--python",
"3.12",
"--from",
"tinysuite-search[server]",
"tinysearch"
]
}
}
}

The client starts TinySearch over stdio when it needs it. This path uses DDGS for search and does not require a repository clone, SearXNG instance, hosted account, or paid search key.

Fast search does not initialize Chromium or an embedding model. The first scrape initializes Chromium; focused scraping and the deprecated research tool also initialize the configured local embedding model. Pre-warm both before those workflows if you want to avoid the first-use delay:

Terminal window
uvx --from "tinysuite-search[server]" tinysearch setup

Check an existing installation without downloading anything:

Terminal window
uvx --from "tinysuite-search[server]" tinysearch doctor
Use case Entry point Default search backend
Python application pip install tinysuite-search DDGS
MCP client launches the server uvx --from "tinysuite-search[server]" tinysearch DDGS
Full self-hosted HTTP stack Docker Compose Bundled SearXNG

Run the published TinySearch image with its own SearXNG instance:

Terminal window
docker compose -f "https://github.com/TinySuiteHQ/TinySearch.git#main:compose.quickstart.yaml" up -d

Connect your MCP client to the Streamable HTTP endpoint:

{
"mcpServers": {
"tinysearch": {
"url": "http://localhost:8000/mcp"
}
}
}

The quick-start stack includes:

  • tinysearch: the MCP server on port 8000.
  • searxng: local web search with JSON output enabled.
  • models: persistent storage for downloaded local embedding models.

Legacy SSE is available at http://localhost:8000/mcp/sse.

Stop the stack later:

Terminal window
docker compose -f "https://github.com/TinySuiteHQ/TinySearch.git#main:compose.quickstart.yaml" down

Install the core package:

Terminal window
pip install tinysuite-search
import asyncio
from tinysearch import scrape_urls, search
async def main():
results = await search("How does asyncio cancellation work?")
evidence = await scrape_urls([{
"url": results["results"][0]["url"],
"query": "How does asyncio cancellation work?",
}])
print(evidence["results"])
asyncio.run(main())

search(), scrape_urls(), and the deprecated research() return schema-v1 dictionaries. Programmatic calls accept a TinySearchConfig or a partial config mapping for per-call overrides and do not read server environment configuration.

Use source mode when you want to inspect or modify TinySearch:

Terminal window
git clone https://github.com/TinySuiteHQ/TinySearch
cd TinySearch
python -m venv .venv
source .venv/bin/activate
pip install -e ".[server]"
tinysearch setup

Run tinysearch or tinysearch mcp for stdio, and tinysearch serve for Streamable HTTP on port 8000.

TinySearch exposes four focused MCP tools. They return context for the calling model; they do not make another model call or write the final answer. The recommended flow is search followed by scrape_urls.

Returns the current date and time in UTC.

Call it before research involving “latest,” “today,” “last month,” or another relative date, and before adding date context to a query.

The result is XML-shaped context containing the current UTC date and time.

<current_datetime>
<date_utc>2026-08-08</date_utc>
<time_utc>10:30:00</time_utc>
</current_datetime>

Use search(query) for fast top-level discovery. It returns backend-ordered titles, URLs, previews, and upstream dates when available. It does not crawl pages or rerank results.

Parameter Type Description
query string The user’s question. Pass it as written; only add temporal context when genuinely needed after checking the current date.

Example:

search("What changed in the latest TinySearch release?")

Search results are returned in an XML <search_results> block. search_max_results in the server configuration sets the MCP result count; it is not a tool parameter.

Use scrape_urls(items) when one or more URLs are already known. It accepts one to five items, each with a required url and optional query.

Item field Type Description
url string The exact public HTTP or HTTPS URL supplied by the user or found through search.
query string, optional Omit it or use "*" for clean page-order Markdown. Provide a focused query to chunk and hybrid-rank that page.

This is the right tool when:

  • search() identified one or more pages to inspect.
  • The user pasted one or more URLs.
  • You need focused evidence from a known page.

Example:

scrape_urls([{
"url": "https://github.com/TinySuiteHQ/TinySearch",
"query": "What MCP tools does it expose?"
}])

For page-order mode, omit the query or use "*":

scrape_urls([{"url": "https://example.com", "query": "*"}])

Each item returns independently, so one failed URL does not discard successful items. scrape_max_tokens in the server configuration sets each MCP item’s content budget; it is not a tool parameter.

Scraping an HTML page also returns up to scrape_max_links bounded candidate links (default 8) found on that page, each with its URL and label:

<related_links>
<link rank="1">
<url>https://example.com/getting-started</url>
<text>Getting started guide</text>
</link>
</related_links>
  • With a focused query, candidates are ranked with the same hybrid BM25 + embedding retrieval used for content chunks.
  • In page-order mode (query omitted or "*"), candidates keep their order on the page and are not scored.
  • TinySearch never follows these links itself. Inspect related_links, then pass any URL worth pursuing to another scrape_urls call.
  • Document scrapes (PDF, DOCX) do not produce related links; related_links is empty for those items.

research(query) retains the former all-in-one search, crawl, and hybrid-ranking workflow for compatibility. New MCP integrations should compose search with scrape_urls instead.

The MCP tools intentionally expose prompt-shaped responses only. They do not accept an output_format parameter.

Use the Python API when you need structured schema-v1 results or per-call limits:

from tinysearch import scrape_urls, search
results = await search("How does asyncio cancellation work?", limit=10)
evidence = await scrape_urls([{"url": results["results"][0]["url"]}], max_tokens=4000)

The optional FastAPI adapter accepts "output_format": "prompt" or "json" on /search and /research; /scrape returns structured per-item outcomes.

TinySearch accepts a flat JSON configuration object. The repository includes an annotated Docker-oriented example at configs/tinysearch_config.json.

Server processes load TINYSEARCH_CONFIG_PATH when it is set. A native pip or uvx install otherwise uses TinySearch’s OS-specific per-user config directory. If no file exists, built-in defaults apply.

The Python API does not read server files or environment variables. Pass a partial mapping or TinySearchConfig directly:

from tinysearch import TinySearchConfig, research
config = TinySearchConfig(
search_backend="ddgs",
search_max_results_to_keep=6,
chunk_max_results_to_keep=8,
)
evidence = await research("What is reciprocal rank fusion?", config=config)

Mount an explicit file when you want to customize the container:

Terminal window
docker run --rm \
-p 8000:8000 \
-v tinysearch-models:/data/models \
-v "$PWD/configs/tinysearch_config.json:/config/tinysearch_config.json:ro" \
-e TINYSEARCH_CONFIG_PATH=/config/tinysearch_config.json \
-e MCP_TRANSPORT=streamable-http \
-e MCP_HOST=0.0.0.0 \
marcellm01/tinysearch:latest

The bundled compose.quickstart.yaml uses built-in config defaults but overrides the search backend to its SearXNG service. The repository’s full compose.yaml mounts configs/tinysearch_config.json, whose broader research limits intentionally differ from the built-in defaults below.

The MCP interface keeps request parameters intentionally small. Set its limits in JSON configuration instead of asking client models to choose them:

Setting Built-in default Use
search_max_results 10 Number of backend-ordered results returned by MCP search.
scrape_max_tokens 2000 Maximum content tokens returned for each MCP scrape_urls item.
scrape_max_links 8 Maximum related-link candidates returned for each MCP scrape_urls item.

Python and FastAPI remain separate: Python search() accepts limit, Python scrape_urls() accepts max_tokens, and FastAPI exposes equivalent request fields.

Variable Use
TINYSEARCH_CONFIG_PATH Load an explicit JSON config file.
TINYSEARCH_SEARCH_BACKEND Override search_backend.
SEARXNG_URL Override search_backend_url.
TINYSEARCH_EMBEDDING_BACKEND Override embedding_backend.
TINYSEARCH_EMBEDDING_MODEL Override embedding_model.
TINYSEARCH_MODELS_DIR Move the local ONNX model cache.
TINYSEARCH_ONNX_MODEL_DIR Point at one exact local ONNX bundle directory.
BRAVE_SEARCH_API_KEY Enable Brave as a keyed fallback when DDGS errors or returns no results.
TINYSEARCH_BROWSER_CDP_URL Override browser_cdp_url. Connect through an external browser’s CDP endpoint instead of the bundled Chromium.

Environment values listed above override values from the JSON file.

Variable Use
MCP_TRANSPORT stdio, sse, or streamable-http. The CLI defaults to stdio; tinysearch serve selects Streamable HTTP.
MCP_HOST / MCP_PORT Bind host and port for SSE or Streamable HTTP. Defaults: 127.0.0.1:8000.
MCP_CORS_ORIGINS Comma-separated allowed origins for browser MCP clients. Defaults to *.
TINYSEARCH_DUMP_TRACEBACK_AFTER Repeatedly dump server thread tracebacks after this many seconds for hang diagnosis.

Transport belongs in the process environment, not the JSON config.

The optional FastAPI adapter exposes GET /config. PUT /config remains read-only unless both conditions are met:

TINYSEARCH_CONFIG_WRITABLE=1
TINYSEARCH_CONFIG_PATH=/an/explicit/writable/tinysearch_config.json

This prevents accidental writes to implicit or read-only configuration.

The native default is ddgs; Docker’s quick start overrides it to searxng.

Setting Built-in default Use
search_backend ddgs ddgs, searxng, duckduckgo, or auto.
search_backend_url http://searxng:8080/search SearXNG JSON endpoint.
search_backend_fallback true For searxng, fall back when SearXNG errors or reports every engine unresponsive.
search_engines [] Optional SearXNG engines, for example ["google", "bing"].
search_region "" Optional backend language or region, for example us-en.
ddgs_backend auto DDGS backend selection. Pin only when necessary.
ddgs_timeout_seconds 20.0 Timeout for one DDGS request.
blocked_domains [] Domains excluded from search and crawl. Subdomains are blocked too.
search_top_k 10 Raw web results requested before ranking.
search_max_results_to_keep 5 Ranked search results kept for crawling.
search_dense_weight 0.5 Dense embedding share in search-result fusion. Must be greater than zero.
search_rrf_cutoff 0.0 Minimum fused search score. Zero disables score filtering.

Backend behavior:

  • ddgs uses the DDGS package’s automatic backend selection.
  • duckduckgo forces DDGS to use its DuckDuckGo backend.
  • searxng queries search_backend_url. When fallback is enabled, request failures and empty responses with SearXNG unresponsive_engines fall back to DDGS; if BRAVE_SEARCH_API_KEY is set, Brave is the final fallback when DDGS also fails or returns no results.
  • auto tries SearXNG and then DDGS.

For ddgs and duckduckgo, setting BRAVE_SEARCH_API_KEY adds Brave’s official Web Search API as a fallback only when the primary request errors or returns no results.

Minimal SearXNG config:

{
"search_backend": "searxng",
"search_backend_url": "http://searxng:8080/search",
"search_engines": ["google", "bing"],
"search_region": "us-en",
"search_backend_fallback": true
}

Block domains:

{
"blocked_domains": ["example.com", "spammy-site.test"]
}

Entries match the domain and its subdomains. URL-style entries such as https://example.com/path are normalized to the hostname.

TinySearch uses its bundled Playwright Chromium by default. To use a browser you operate separately, set its Chrome DevTools Protocol endpoint in the config file:

{
"browser_cdp_url": "http://browser:9222"
}

Server processes also accept TINYSEARCH_BROWSER_CDP_URL. When either setting is present, TinySearch connects through Crawl4AI instead of installing or launching the bundled Chromium. The external browser owns its executable, profile, proxy, and fingerprint configuration; TinySearch does not select or install a particular browser backend.

Treat a CDP endpoint as privileged remote control of the browser. Keep it on a private network or loopback interface, require authentication when it crosses a host boundary, and do not expose port 9222 directly to the public internet. When TinySearch itself runs in Docker, localhost refers to the TinySearch container, so use an endpoint reachable from that container.

The CDP endpoint is operator-managed and cannot be changed through the HTTP PUT /config endpoint, even when configuration writes are enabled. Set it in the startup environment or the file selected by TINYSEARCH_CONFIG_PATH, then restart TinySearch. HTTP clients can continue updating other settings by omitting browser_cdp_url from their partial update.

Setting Built-in default Use
max_concurrent_crawls 5 Maximum pages crawled in parallel.
pipeline_timeout_seconds 120.0 Overall research timeout. Use null to disable it.
crawl_fit_markdown_mode bm25 bm25, pruning, or none.
crawl_fit_min_chars 200 Minimum fitted markdown length before accepting filtered content.
crawl_bm25_threshold 1.5 Query-aware Crawl4AI BM25 filter threshold.
crawl_bm25_language english Language used by the BM25 content filter.
crawl_pruning_threshold 0.48 Query-independent pruning threshold.
crawl_max_page_tokens 0 Maximum retained page tokens before chunking. Zero leaves the page uncapped.
crawl_max_chunk_tokens 300 Target token size for candidate chunks.
crawl_overlap_tokens 80 Token overlap between adjacent chunks.
Setting Built-in default Use
chunk_max_results_to_keep 2 Final research evidence chunks. Scrape uses its own token budget.
chunk_rank_oversample 3 Candidate multiplier before deduplication and source quotas.
chunk_max_per_source_url 4 Per-page cap so one source cannot dominate evidence.
chunk_dense_weight 0.5 Dense embedding share in chunk fusion. Must be greater than zero.
chunk_rrf_cutoff 0.0 Minimum fused chunk score. Zero disables score filtering.
chunk_dedupe_jaccard_threshold 0.92 Near-duplicate chunk cutoff. Lower values deduplicate more aggressively.

TinySearch requires dense embeddings. The default backend is local ONNX.

Setting Built-in default Use
embedding_backend onnx Local ONNX or openai_compatible.
embedding_model fast fast, balanced, quality, or a custom Hugging Face ONNX repo id.
embedding_openai_env_file .env Credentials and model file for openai_compatible.
max_concurrent_embedding_calls 3 Maximum concurrent embedding batches.
embedding_timeout_seconds 60.0 Timeout for an embedding call or batch.
embedding_timeout_retries 2 Retries after an embedding timeout.
dense_query_prefix non-empty retrieval instruction Optional text prepended before embedding the query.
dense_document_prefix "" Optional text prepended before embedding results and chunks.
dense_document_embed_batch_size 32 Documents or chunks sent to the embedder per batch.
encoding_name o200k_base Tokenizer for budgets. Use embedding to follow the selected embedding model.
trace_path "" Optional pipeline trace file. Empty disables tracing.

Built-in local model presets:

Preset Hugging Face repository
fast onnx-models/all-MiniLM-L6-v2-onnx
balanced BAAI/bge-small-en-v1.5
quality BAAI/bge-base-en-v1.5

Pre-download Chromium and the selected ONNX model with tinysearch setup. If embedding_backend is openai_compatible, setup skips the local model download.

OpenAI-compatible config:

{
"embedding_backend": "openai_compatible",
"embedding_openai_env_file": ".env"
}
OPENAI_BASE_URL=
OPENAI_API_KEY=
OPENAI_EMBEDDING_MODEL=

OPENAI_BASE_URL is optional for api.openai.com. API_KEY, EMBEDDING_MODEL, and MODEL_NAME are accepted aliases for the corresponding values.

Run the readiness check before debugging individual requests:

Terminal window
tinysearch doctor

It verifies the resolved config location, Chromium installation, local ONNX model bundle, and writable config directory without downloading anything.

Install or repair the browser and configured local model with:

Terminal window
tinysearch setup

On Linux, include OS-level Chromium dependencies when needed:

Terminal window
tinysearch setup --with-system-deps

The Python library has a smaller core install. MCP, FastAPI, and Uvicorn are in the server extra:

Terminal window
pip install "tinysuite-search[server]"

Fast search begins without Chromium or a local embedding model. The first scrape initializes Chromium; a focused scrape and deprecated research also initialize the configured ONNX model. That first use can therefore take a minute or two.

Run tinysearch setup before starting the client. In Docker, keep a volume mounted at /data/models so the model cache survives restarts.

For a client-launched stdio server, run tinysearch doctor using the same Python or uvx environment as the client.

For Docker or tinysearch serve, verify the server is running and use:

http://localhost:8000/mcp

Streamable HTTP is served on /mcp; legacy SSE is served on /mcp/sse.

TinySearch needs SearXNG JSON output. Add json to search.formats:

search:
formats:
- html
- json

The quick-start Compose file already enables this.

Native pip and uvx installs default to ddgs. The Docker quick start overrides the backend to searxng.

  • ddgs lets the DDGS package select a backend.
  • duckduckgo pins DDGS to DuckDuckGo.
  • searxng uses the configured SearXNG URL. With search_backend_fallback set to true, TinySearch also treats an empty response that lists unresponsive_engines as a backend outage, then falls back to DDGS.
  • auto tries SearXNG and then DDGS.

Check TINYSEARCH_SEARCH_BACKEND, SEARXNG_URL, and your config file if the selected backend is not what you expected.

If BRAVE_SEARCH_API_KEY is set, Brave’s official API is used only after the DDGS or DuckDuckGo attempt errors or returns no results. That includes the fallback path after a SearXNG outage. A genuine empty SearXNG result without unresponsive_engines remains an empty result; it does not trigger a fallback.

Server processes resolve configuration in this order:

  1. Built-in defaults.
  2. The JSON file selected by TINYSEARCH_CONFIG_PATH, or the native per-user config location.
  3. Supported environment overrides such as TINYSEARCH_SEARCH_BACKEND, SEARXNG_URL, TINYSEARCH_EMBEDDING_BACKEND, and TINYSEARCH_EMBEDDING_MODEL.

The public Python API is deliberately isolated from ambient server config. Pass a mapping or TinySearchConfig to search(..., config=...), scrape_urls(..., config=...), or research(..., config=...).

PUT /config is disabled by default. To enable it, set both:

TINYSEARCH_CONFIG_WRITABLE=1
TINYSEARCH_CONFIG_PATH=/an/explicit/writable/tinysearch_config.json

Do not enable runtime writes against a read-only Compose mount.

The research pipeline requires dense embeddings. Keep both dense weights greater than zero:

{
"search_dense_weight": 0.5,
"chunk_dense_weight": 0.5
}

TinySearch rejects:

  • non-HTTP schemes
  • URLs with embedded credentials
  • private, loopback, link-local, multicast, reserved, or unspecified IP addresses
  • domains listed in blocked_domains

The safety check applies to the initial URL and the final URL reported after redirects.