verifind-mcp
verifind-mcp
An MCP server with one tool: find real things matching whatever shape you ask for, and never invent one. Products, job postings, event tickets, opportunities, apartments — you give it a plain-language query and a JSON Schema for what one result should look like. It runs a real web search, has a model restructure only what that search actually returned into your shape, then checks every result's source URL in code against the literal search results. If a claim can't be traced back to a real fetched page, that item is dropped — not guessed at, not "corrected," dropped.
Search → restructure into your schema → verify the source in code → return only what survives.
Built entirely on free-tier APIs — Tavily for search, any OpenAI-compatible endpoint (Groq by default) for extraction. No credit card required for either.
The problem, in one exchange
Ask a raw LLM to "find" something and it will, whether or not that thing actually exists:
You: Find me 3 real wireless earbuds under $50 with good bass. Raw LLM (illustrative — not a real call): 1. AudioMax Pro X200 — $42.99, deep bass, 30hr battery. 2. SoundWave Elite S3 — $38.50...
Confident, specific, and — unless you happen to already know this market — completely unverifiable. Neither product may exist.
Here's verified_search on the same question, actual output from examples/smoke.ts against live Tavily + Groq, unedited:
{
"items": [
{ "name": "Treblab WX8", "highlight": "Listed under “Best Bass Wireless Earbuds under $50”", "sourceUrl": "https://headphonesaddict.com/best-bluetooth-earbuds-under-50" },
{ "name": "TOZO NC2", "price": "$40", "highlight": "Described as having heavy bass and priced at $40", "sourceUrl": "https://www.soundguys.com/the-best-wireless-earbuds-under-50-156056" }
],
"itemsExtracted": 4,
"itemsDropped": 0
}Every sourceUrl above is a real page this tool actually fetched — click one, it's there. That's the entire point of the project.
Why this exists
This started as one piece of a different project (a CRM that finds paid photography gigs): a pattern for asking an LLM to "find real things" without it confidently hallucinating half of them. That pattern turned out to have nothing to do with photography — it generalizes to anything an agent might need to look up on the live web with a straight-faced answer instead of a plausible-sounding guess.
The core problem: an LLM asked to "find X" will happily invent X. The fix is splitting retrieval from extraction: a real search API fetches real pages, and the model is only ever allowed to restructure those specific results — never asked to know things directly. See
src/verifiedSearch.ts.The guarantee is enforced in code, not by asking nicely. Every result schema gets a
sourceUrlfield injected automatically (even if the caller's schema didn't include one), and after the model responds, that field is checked against the literal set of URLs the search actually returned — in aSet, not by trusting the model's word. A result citing an unlisted or fabricated URL is dropped, unconditionally, no exceptions.Fully generic, not a fixed set of "modes." The result shape is a JSON Schema the caller supplies per call — there's no hardcoded
ProductorJobPostingtype anywhere in this codebase. The same tool that found the earbuds above can find event tickets or apartment listings, because the shape of "a result" is an input, not a design decision baked into the server.Honest about where the guarantee is strong vs. weak. A
sourceUrlis either genuinely one of the fetched pages or it isn't — that's a hard, checkable fact, enforced by aSet.has()call, not a vibe. Apriceornamefield is only as good as the model's transcription of the source text; nothing stops it from misreading a number. This README says that outright instead of implying every field carries the same guarantee.
Architecture
flowchart LR
Q["query + JSON Schema"] --> S["Tavily search"]
S -->|"real search results"| E["LLM extraction"]
E -->|"structured JSON"| V{"Validate"}
S -->|"actual URLs fetched"| V
V -->|"schema-valid AND source verified"| KEEP["Kept items"]
V -->|"fails either check"| DROP["Dropped, reason recorded"]The LLM never searches the web itself — it only ever sees the exact search results verifiedSearch already fetched, wrapped as clearly-labeled untrusted data (see src/security.ts, this project's prompt-injection defense against hostile page content trying to redirect the model's behavior).
Tech stack
Piece | Choice |
Language | TypeScript |
Protocol |
|
Search | Tavily — free tier, no card |
Extraction | Any OpenAI-compatible chat completions endpoint — defaults to Groq, free tier, no card |
Schema validation |
|
Testing | Vitest — unit tests mock Tavily/the LLM; |
Repository layout
src/
index.ts MCP server entrypoint — registers the verified_search tool
verifiedSearch.ts The whole pipeline: search -> extract -> validate -> verify
tavily.ts Real web search client
security.ts Wraps untrusted web content for safe LLM inclusion
json.ts Tolerant JSON parsing for model output
env.ts Env validation
logger.ts stderr-only logger (stdout is the MCP protocol channel — never log there)
examples/
smoke.ts Live end-to-end call against real APIs, no MCP involved
mcp-smoke.ts Live call through the actual MCP client/server stdio protocolQuick start
git clone https://github.com/<you>/verifind-mcp.git
cd verifind-mcp
npm install
cp .env.example .env # fill in TAVILY_API_KEY and LLM_API_KEY (both free, no card)
npm run build
npx dotenv -e .env -- npx tsx examples/smoke.tsThat last command hits real APIs and prints real, sourced results — no MCP client required to see it work.
Prerequisites
Node.js 20+
A free Tavily API key
A free Groq API key (or any other OpenAI-compatible endpoint you'd rather point at)
Using it as an MCP server
Register it with any MCP-compatible client — e.g. Claude Desktop's claude_desktop_config.json:
{
"mcpServers": {
"verifind": {
"command": "node",
"args": ["/path/to/verifind-mcp/dist/index.js"],
"env": {
"TAVILY_API_KEY": "...",
"LLM_API_KEY": "..."
}
}
}
}Then ask the client something like: "Use verified_search to find me three real budget mechanical keyboards under $60, with name, price, and sourceUrl."
To try it without any MCP client:
npx dotenv -e .env -- npx tsx examples/smoke.ts # calls verifiedSearch() directly
npx dotenv -e .env -- npx tsx examples/mcp-smoke.ts # calls it through the real stdio protocolTesting
npm testUnit tests mock Tavily and the LLM — no network calls, no API keys needed. The examples/ scripts are the real, live check: they hit actual Tavily and Groq endpoints and print real results, the same way the earbuds output above was generated.
Limitations, honestly
Free-tier rate limits (Tavily's request quota, Groq's per-minute token budget) apply — this isn't built for high-volume production traffic without upgrading those.
The
sourceUrlguarantee is real and code-enforced. Every other field is the model's best-effort transcription of the source text — good, but not independently verified the way the URL is.Search quality depends entirely on Tavily's index for the query given; a query with no good public results returns few or no items rather than a forced answer, which is the intended behavior, not a bug.
Contributing
This is deliberately domain-agnostic — if you use it for something (job boards, real estate, event tickets, anything), an example schema under examples/ showing that use case is a genuinely useful PR. Issues and schema-design questions welcome too.
License
MIT — see LICENSE.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hemanth-hexo/groundtruth_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server