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.
Quick start: MCP over stdio
Section titled “Quick start: MCP over stdio”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:
uvx --from "tinysuite-search[server]" tinysearch setupCheck an existing installation without downloading anything:
uvx --from "tinysuite-search[server]" tinysearch doctorChoose an installation
Section titled “Choose an installation”| 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 |
Docker with SearXNG
Section titled “Docker with SearXNG”Run the published TinySearch image with its own SearXNG instance:
docker compose -f "https://github.com/TinySuiteHQ/TinySearch.git#main:compose.quickstart.yaml" up -dConnect 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 port8000.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:
docker compose -f "https://github.com/TinySuiteHQ/TinySearch.git#main:compose.quickstart.yaml" downPython library
Section titled “Python library”Install the core package:
pip install tinysuite-searchimport asynciofrom 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.
Run from source
Section titled “Run from source”Use source mode when you want to inspect or modify TinySearch:
git clone https://github.com/TinySuiteHQ/TinySearchcd TinySearch
python -m venv .venvsource .venv/bin/activatepip install -e ".[server]"tinysearch setupRun tinysearch or tinysearch mcp for stdio, and tinysearch serve for Streamable HTTP on port 8000.
MCP tools
Section titled “MCP tools”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.
get_current_datetime()
Section titled “get_current_datetime()”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>search(query)
Section titled “search(query)”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.
scrape_urls(items)
Section titled “scrape_urls(items)”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.
Related links
Section titled “Related links”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 (
queryomitted 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 anotherscrape_urlscall. - Document scrapes (PDF, DOCX) do not produce related links;
related_linksis empty for those items.
research(query) (deprecated)
Section titled “research(query) (deprecated)”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.
Output formats
Section titled “Output formats”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.
Configuration
Section titled “Configuration”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)Docker override
Section titled “Docker override”Mount an explicit file when you want to customize the container:
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:latestThe 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.
MCP budgets
Section titled “MCP budgets”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.
Environment variables
Section titled “Environment variables”Config and models
Section titled “Config and models”| 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.
MCP server
Section titled “MCP server”| 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.
FastAPI runtime config
Section titled “FastAPI runtime config”The optional FastAPI adapter exposes GET /config. PUT /config remains read-only unless both conditions are met:
TINYSEARCH_CONFIG_WRITABLE=1TINYSEARCH_CONFIG_PATH=/an/explicit/writable/tinysearch_config.jsonThis prevents accidental writes to implicit or read-only configuration.
Search backend
Section titled “Search backend”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:
ddgsuses the DDGS package’s automatic backend selection.duckduckgoforces DDGS to use its DuckDuckGo backend.searxngqueriessearch_backend_url. When fallback is enabled, request failures and empty responses with SearXNGunresponsive_enginesfall back to DDGS; ifBRAVE_SEARCH_API_KEYis set, Brave is the final fallback when DDGS also fails or returns no results.autotries 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.
External browser over CDP
Section titled “External browser over CDP”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.
Crawling and extraction
Section titled “Crawling and extraction”| 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. |
Chunk ranking
Section titled “Chunk ranking”| 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. |
Embeddings and token counting
Section titled “Embeddings and token counting”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.
Troubleshooting
Section titled “Troubleshooting”Check the installation
Section titled “Check the installation”Run the readiness check before debugging individual requests:
tinysearch doctorIt 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:
tinysearch setupOn Linux, include OS-level Chromium dependencies when needed:
tinysearch setup --with-system-depsServer dependencies are missing
Section titled “Server dependencies are missing”The Python library has a smaller core install. MCP, FastAPI, and Uvicorn are in the server extra:
pip install "tinysuite-search[server]"First scrape is slow
Section titled “First scrape is slow”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.
MCP client cannot connect
Section titled “MCP client cannot connect”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/mcpStreamable HTTP is served on /mcp; legacy SSE is served on /mcp/sse.
SearXNG returns HTML
Section titled “SearXNG returns HTML”TinySearch needs SearXNG JSON output. Add json to search.formats:
search: formats: - html - jsonThe quick-start Compose file already enables this.
Search backend behavior is unexpected
Section titled “Search backend behavior is unexpected”Native pip and uvx installs default to ddgs. The Docker quick start overrides the backend to searxng.
ddgslets the DDGS package select a backend.duckduckgopins DDGS to DuckDuckGo.searxnguses the configured SearXNG URL. Withsearch_backend_fallbackset totrue, TinySearch also treats an empty response that listsunresponsive_enginesas a backend outage, then falls back to DDGS.autotries 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.
Config changes are ignored
Section titled “Config changes are ignored”Server processes resolve configuration in this order:
- Built-in defaults.
- The JSON file selected by
TINYSEARCH_CONFIG_PATH, or the native per-user config location. - Supported environment overrides such as
TINYSEARCH_SEARCH_BACKEND,SEARXNG_URL,TINYSEARCH_EMBEDDING_BACKEND, andTINYSEARCH_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=...).
FastAPI config updates return 403
Section titled “FastAPI config updates return 403”PUT /config is disabled by default. To enable it, set both:
TINYSEARCH_CONFIG_WRITABLE=1TINYSEARCH_CONFIG_PATH=/an/explicit/writable/tinysearch_config.jsonDo not enable runtime writes against a read-only Compose mount.
Dense weight errors
Section titled “Dense weight errors”The research pipeline requires dense embeddings. Keep both dense weights greater than zero:
{ "search_dense_weight": 0.5, "chunk_dense_weight": 0.5}URLs are rejected before crawling
Section titled “URLs are rejected before crawling”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.