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
|
|---|---|
| GET | /v1/page
|
| POST | /v1/fetchBody |
| GET | /v1/similar
|
| POST | /mcpStreamable HTTP MCP server with the tools |
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:
| operator | meaning |
|---|---|
site:docs.python.org | that host or its subdomains |
type:doc | doc, package, code, article, reference, other |
ecosystem:npm | npm, pypi, go, cargo |
package:react-router | exactly that package |
lang:en | two-letter language |
since:2026-01-01 until:… | published or fetched in that window |
"exact phrase" | the text must contain it |
-word | the text must not contain it |
intitle:word inurl:word | word in the title, or in the address |
sort:new | newest 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/mcpThe 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.