{
  "markdown": "<div align=\"center\">\n\n# search\n\n**A self-hosted MCP server for web search that reports how much of each answer to believe**\n\n[![MCP registry](https://img.shields.io/badge/MCP_registry-com.ag--bureau%2Fsearch-2ea44f?style=for-the-badge)](https://registry.modelcontextprotocol.io)\n[![License](https://img.shields.io/badge/License-AGPL--3.0-E23A50?style=for-the-badge)](LICENSE)\n[![Python](https://img.shields.io/badge/Python-3.13-3776AB?logo=python&logoColor=white&style=for-the-badge)](adapter/Dockerfile)\n[![Self-hosted](https://img.shields.io/badge/Self--hosted-Docker_Compose-2496ED?logo=docker&logoColor=white&style=for-the-badge)](#-install)\n[![ag-mcp-search MCP server — quality and maintenance score on Glama](https://glama.ai/mcp/servers/AG-Bureau/mcp-search/badges/score.svg)](https://glama.ai/mcp/servers/AG-Bureau/mcp-search)\n\n</div>\n\n---\n\nA search tool fails in ways that look exactly like success. An engine answers with\nsomebody else's subject. A page returns text that is an anti-bot shield. Sixteen\nsources turn out to be two engines counted eight times. None of that raises an\nerror, and the model on the other end builds on it.\n\n**This server's job is to make those cases distinguishable, in fields you can\nbranch on.** It runs on your machine, over your own metasearch instance, with your\nown model key — or none at all.\n\n\n## 🔧 Tools\n\n| Tool | What it does | Required | Notable options |\n|---|---|---|---|\n| `web_search` | Finds pages **and reads the top ones** — one call, links with their text | `query` | `read: false` for links only · `read_top` how many to read · `min_engines` to force breadth · `corroborate` |\n| `web_read` | Reads pages by address: text, PDF, or a scan recognised by a vision model | `urls` | `mode: browser` for JS-rendered pages · `expect` to assert what must be there · `offset` to continue |\n| `web_image_search` | Finds images: the address of the FILE and, separately, of the page it sits on | `query` | `max_results`, `page` |\n| `web_screenshot` | A PNG of a page **plus its text from the same visit**, so the two can be cross-checked | `url` | `max_chars` for how much text · `full_page` · `expect` |\n| `web_deep_search` | Composes its own queries, reads in waves, and answers from several sources — saying what it could not confirm | `question` | `waves` |\n\nFull argument reference, response shapes and failure modes: **[HOWTO-CALL.md](HOWTO-CALL.md)**.\n\n## 📦 Install\n\nFrom an open repository page to a working answer. Nothing is assumed to be on\nyour disk already:\n\n```bash\ngit clone https://github.com/AG-Bureau/mcp-search\ncd mcp-search\ncp .env.example .env\necho \"SEARXNG_SECRET=$(openssl rand -hex 32)\" >> .env\ndocker compose -f docker-compose.yml -f wiring/expose-localhost.yml up -d --build\ncurl -s http://127.0.0.1:8081/healthz\n```\n\nThe fourth line is not decoration. Without a value in `SEARXNG_SECRET` the very\nnext command refuses — and that refusal is deliberate: with no key of its own the\nmetasearch does not fail, it comes up with a publicly known one from its image\ntemplate, silently.\n\nThe overlay publishes the port **on loopback only**. A published container port\ndoes not go through the host firewall's usual chain, so exposing it more widely\nis a separate, deliberate step — see [Deployment](#-deployment-and-exposure).\n\n### Two transports\n\nMCP has two, and they answer different questions. **HTTP** — the commands above —\nis for a server that is already running somewhere. **stdio** is the protocol's\ndefault: the client starts the server as a process and talks to it through the\npipes, which is how most desktop clients and wrappers work.\n\n```bash\npython adapter/server.py --stdio        # or MCP_TRANSPORT=stdio\n```\n\nOne JSON-RPC object per line in, one answer per line out. The mode is chosen\nexplicitly and never guessed from whether a terminal is attached — that sign\nmerely sits next to the subject, and one day it answers for a case nobody meant.\n\nIn stdio mode **stdout is the protocol**: answers and nothing else, with the log\non stderr. One stray line of anything else breaks the client reading it.\n\nThe sidecars do not depend on the choice. Started by a client with no compose\nproject around it, the module still works and names what is missing instead of\npretending: the browser path reports `not_wired_up`, and the engine pool comes\nback as `pool_source: seed`.\n\n## ⚙️ Configuration\n\n| Variable | Required | What it is |\n|---|---|---|\n| `SEARXNG_SECRET` | **yes** | Session key for the metasearch. Any long random string that is not from somebody's history. |\n| `LLM_API_KEY` | no | Key for any OpenAI-compatible endpoint. **Secret.** |\n| `LLM_API_BASE` | no | Base URL of that endpoint. Take it from your provider's documentation, not by analogy — the obvious guess can answer `429: Insufficient balance` because the subscription lives on a different path of the same domain. |\n| `LLM_MODEL_TEXT` | no | Model that plans queries and composes answers. No default is shipped: a default would silently ask your provider for a model it may not have. |\n| `LLM_MODEL_VISION` | no | Model that reads scanned PDFs. Unset, such documents return an explicit refusal naming the reason. |\n| `LLM_DISABLE_THINKING` | no | Set for providers whose reasoning budget swallows the answer, leaving it empty with `finish_reason: length`. |\n| `READ_CONTACT` | no | Contact placed in the `User-Agent` when fetching pages. Defaults to this repository; set your own if you run this at scale. |\n| `READ_LANGUAGES` | no | `Accept-Language` when reading. Unset by default — the language of the pages you read is not ours to choose. |\n\nPacing, pool size and read limits have their own variables with measured\ndefaults; see [`.env.example`](.env.example), which explains each one where you\nset it.\n\n**A model key is optional.** Search, reading, image search and screenshots are\nHTTP requests and spend no model tokens. A model is called in exactly two places,\nand both are named in the answer: `web_deep_search`, and recognising a PDF with no\ntext layer — which happens only when you ask to read such a document, never behind\nyour back in a search.\n\n## 🎯 What a bundled search tool does not do\n\n**Cost you control.** One argument changes the answer by an order of magnitude:\n\n| call | payload | time | model tokens |\n|---|---|---|---|\n| `read: false`, 6 links | 3.8 KB | 0.6 s | **0** |\n| `read_top: 1`, 3 links | 8.8 KB | 1.6 s | **0** |\n| `read_top: 3`, 6 links | 9.8 KB | 6.3 s | **0** |\n| `web_deep_search` | full account | 36 s | 6 calls |\n\n*Measured on one machine, one query. Take the shape, not the digits.*\n\n**The engine list maintains itself.** A hand-written list goes stale in silence:\nan engine that was the best returns nothing weeks later and says nothing about it.\nOurs was revised three times in a single day — each revision against the previous\none, each correct on its own data. The problem was never the engines: a decision\nfreezes while observation goes on.\n\nSo the list is not written here. A prober asks every known engine, continuously,\nwith questions whose correct answer is known in advance, and the pool is the best\nfew by reference hit share — recomputed on its own. Verified by falsification: a\nplanted bad run took an engine out of the pool **with no code change**, and\nrestoring the run brought it back by itself.\n\nUntil enough observation accumulates, the pool is a seed list and every answer\nsays so in `pool_source`.\n\n**Failure is distinguishable from success.** Four ways an engine can fail, and\nwhat shows each:\n\n| how it fails | what shows it |\n|---|---|\n| answers with a refusal: captcha, rate limit, ban | `unresponsive_engines` |\n| silently returns nothing | the difference between `engines_asked` and `engines_answered` |\n| answers a different question | `engines_irrelevant` — its results are already discarded |\n| substitutes the subject with a better-indexed namesake | `engines_trust`, earned against references |\n\nThe same applies to reading: seven distinct outcomes, and a page that returned a\nshield is `stub`, not empty text.\n\n## 📖 How it works\n\n- **[ALGORITHM.md](ALGORITHM.md)** — what happens, step by step, on each call.\n- **[contracts/](contracts/)** — the call contracts, versioned separately from\n  the code that implements them.\n- **[measures/](measures/)** — dated measurements: which engines were alive, what\n  the load ladder gives, what the transport change bought. Numbers, with what was\n  measured and when.\n\n## 🔒 Deployment and exposure\n\n`wiring/expose-localhost.yml` publishes the adapter on `127.0.0.1` only. Anything\nwider is a separate overlay, and its header says what to check first: Docker\npasses traffic to published ports through `FORWARD` after DNAT, while the\nfirewall's own chain sits before its hooks — so a firewall that says \"closed\" can\nbe open to the internet on a published port.\n\nA search server open to the outside is an open proxy that goes to the network in\nthe machine owner's name.\n\n## ✅ Tests\n\n```bash\nIMAGE=ag-mod-search/adapter:0.2.1 bash tests/in-image.sh\n```\n\nThree suites — the protocol and search against a fake metasearch, reading against\na fake site, the computed pool against a database built in memory. **Not one of\nthem makes a single outbound request**: for reading that matters more than for\nsearch, because a test that went to the internet would spend the very resource\nthe tool protects — the reputation of the one address it calls from.\n\nThey run inside the built image rather than on the machine where the code is\nedited: the PDF parser lives in the image, and a suite run outside would skip\neverything that touches it. The skip is not silent — the check goes red with a\nnote saying where to run it.\n\nWhat these suites cannot check is written down in\n[tests/README.md](tests/README.md).\n\n## 🤝 Contributing\n\nA capability, engine or heuristic is not accepted until its **reference\nattribute** is declared — a property of the correct answer that the thing being\ntested could not have told us itself — and a pool of checked questions is\nattached. See [CONTRIBUTING](https://github.com/AG-Bureau/.github/blob/main/CONTRIBUTING.md).\n\n## 📄 License\n\n[GNU Affero General Public License v3.0](LICENSE). Run it, change it, build on\nit. If you make it available to others OVER A NETWORK, the changes you made go\nback out under the same licence — that is the one obligation, and running a\nservice counts as making it available.\n\nFor whoever cannot live with that clause, a commercial licence is a question to\nask rather than a fork to make.\n",
  "bytes": 10497,
  "sha": "7b936974631ea201c3799165addbaaa4e6161fd3d754c4b6cf74534cdf047c35",
  "repo_slug": "ag-bureau/mcp-search",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_ag_bureau_search_91a65554/readme"
}