search.

API

JSON by default, markdown with Accept: text/markdown. Everything returned was crawled, extracted and ranked by this engine; nothing is proxied from another one. No key is needed for a modest budget per client.

Endpoints

GET/v1/search

q (required, operators allowed), n (1–50, default 10), offset (0–200),fields (snippet default, text, markdown; several, comma separated), site, type, ecosystem, lang, since,until as parameters instead of operators, live=0 to answer from the index only.

GET/v1/page

url (required), fields (markdown default, text). One stored page with its citation. 404 not_indexed when the index has none. WithAccept: text/markdown the body is the page itself, title and address first.

POST/v1/fetch

Body { "url": "…", "fields": ["markdown"] } (or GET ?url=). Fetches that address right now through the same extractor, under the same manners, and returns it like a hit. Pages markednoindex are returned to you and never kept.

GET/v1/similar

url of an indexed page, or POST { "text": "…" }. Pages like it, as a search result.

POST/mcp

Streamable HTTP MCP server with the tools search, fetch and similar.

Examples

curl -s "https://search.caioricciuti.com/v1/search?q=react+router&n=3&fields=markdown"

curl -s -H "Accept: text/markdown" "https://search.caioricciuti.com/v1/search?q=site:docs.python.org+asyncio"

curl -s "https://search.caioricciuti.com/v1/page?url=https://ch-ui.com/&fields=markdown"

curl -s -X POST "https://search.caioricciuti.com/v1/fetch" -d '{"url":"https://www.rfc-editor.org/rfc/rfc9309.html","fields":["text"]}'

curl -s "https://search.caioricciuti.com/v1/similar?url=https://pypi.org/project/fastapi"

Query operators

The same operators as the search box work in q:

operatormeaning
site:docs.python.orgthat host or its subdomains
type:docdoc, package, code, article, reference, other
ecosystem:npmnpm, pypi, go, cargo
package:react-routerexactly that package
lang:entwo-letter language
since:2026-01-01 until:…published or fetched in that window
"exact phrase"the text must contain it
-wordthe text must not contain it
intitle:word inurl:wordword in the title, or in the address
sort:newnewest first among the matches

Operators alone, with no words, list the newest pages that match them; those browse queries never trigger a live lookup. The response echoes what was understood in parsed.

What comes back

{
  "q": "react router",
  "terms": ["react", "router"],
  "parsed": { "terms": ["react", "router"], "filters": {}, "ops": [] },
  "coverage": "full",            // full | partial | none
  "total_candidates": 47,
  "took_ms": 31,
  "offset": 0,
  "live": false,                 // true: the index was thin, pages were fetched right now
  "discovered": 0,               // how many that live lookup indexed
  "hits": [
    {
      "url": "https://…", "title": "…", "description": "…",
      "host": "…", "lang": "en",
      "type": "doc",               // doc | package | code | article | reference | other
      "ecosystem": "npm", "package": "react-router",
      "fetched_at": "2026-10-25 20:25:47",
      "published_at": null,        // or a timestamp
      "content_hash": "…",         // sha256 of the extracted text
      "word_count": 1190,
      "score": 51.4,
      "snippet": "…",              // fields=snippet (default)
      "text": "…",                 // fields=text
      "markdown": "…"              // fields=markdown
    }
  ]
}

The same query against the same index gives the same order. coverage is full when every term matched, partial when the engine fell back to all terms but one (three or more terms only),none when the index has nothing. Errors are { "error": { "code", "message" } }with a stable code: missing_q, missing_url, not_indexed,rate_limited, not_found, internal.

When the index is thin

Fewer than three hits and the engine looks right then, within a few seconds, at sources with structure rather than at another search engine: the query as an address, a host or a package name; its own frontier; registry search APIs. What it finds is fetched, indexed and answered from the index, so the second ask is instant. The response says so with live: true and discovered. Pass live=0 to skip it.

Limits

Anonymous clients get 30 searches a minute, counted per address; past that the answer is429 rate_limited. q is cut at 500 characters, n at 50,offset at 200. Keys with a larger budget are coming; write tohello@caioricciuti.com if you need one now.

MCP

{
  "mcpServers": {
    "search": { "type": "http", "url": "https://search.caioricciuti.com/mcp" }
  }
}

Claude Code:

claude mcp add --transport http search https://search.caioricciuti.com/mcp

The tools take the same parameters as the endpoints and return the same JSON. Ask forfields: ["text"] or ["markdown"] on search so one call finds and reads.