x=hub

your toolset

    build deployment
    sign in

    Search that shows its work

    mcpother official build verified scanned
    com.ag-bureau/search · v0.3.5
    deploy just this

    Self-hosted web search that reports how much of each answer to believe

    run it

    on your own machines, one way per deploy method
    verified 2026-10-18 · v0.3.5 · r-20261018T115948Z-bfd6cf950347 deployed 2026-10-18 · 5 tools
    # once, as root:
    docker build -t mcp-local/search:8e781f8e1080206e532c9ae2c8d843f467d4dc16 'https://github.com/AG-Bureau/mcp-search.git#8e781f8e1080206e532c9ae2c8d843f467d4dc16:adapter'
    
    #!/usr/bin/env bash
    # search (com.ag-bureau/search), recipe docker (worker, verified 2026-10-18 on v0.3.5, run r-20261018T115948Z-bfd6cf950347).
    # Generated by Hub for toolset "my-agent"; started by mcp-my-agent-search.service.
    set -euo pipefail
    export HOST=127.0.0.1 PORT=8101
    exec docker run --rm --name mcp-my-agent-search -p 127.0.0.1:${PORT}:8081 mcp-local/search:8e781f8e1080206e532c9ae2c8d843f467d4dc16
    
    [Unit]
    Description=MCP server search (toolset my-agent), generated by Hub
    After=network-online.target docker.service
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=mcp
    Group=mcp
    SupplementaryGroups=docker
    EnvironmentFile=/etc/mcp/my-agent.env
    Environment=HOME=/var/lib/mcp/search
    WorkingDirectory=/var/lib/mcp/search
    ExecStart=/opt/mcp/my-agent/bin/search.sh
    Restart=on-failure
    RestartSec=5
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    PrivateTmp=true
    ReadWritePaths=/var/lib/mcp/search
    
    [Install]
    WantedBy=multi-user.target
    
    verified 2026-10-18 · v0.3.5 · r-20261018T115948Z-bfd6cf950347
    docker build -t search 'https://github.com/AG-Bureau/mcp-search.git#8e781f8e1080206e532c9ae2c8d843f467d4dc16:adapter'
    docker run --rm -p 127.0.0.1:8101:8081 search
    
    verified 2026-10-18 · v0.3.5 · r-20261018T115948Z-bfd6cf950347 deployed 2026-10-18 · 5 tools
    # Generated by Hub for toolset "my-agent" (2026-10-18T22:56:52+00:00).
    # Each package server runs behind a stdio->HTTP bridge and listens on localhost only.
    #   cp .env.example .env    # fill the ${VAR}s
    #   docker compose up -d && bash smoke.sh
    # Remote servers are not here: clients reach them directly (see clients/).
    name: mcp-my-agent
    services:
      search:
        build:
          context: https://github.com/AG-Bureau/mcp-search.git#8e781f8e1080206e532c9ae2c8d843f467d4dc16:adapter
        ports:
        - 127.0.0.1:8101:8081
        restart: unless-stopped
    volumes:
      npm-cache: {}
      uv-cache: {}
    
    verified 2026-10-18 · v0.3.5 · r-20261018T115948Z-bfd6cf950347 deployed 2026-10-18 · 5 tools
    # Generated by Hub for toolset "my-agent" (2026-10-18T22:56:52+00:00).
    #   kubectl create namespace mcp
    #   kubectl -n mcp create secret generic mcp-credentials --from-env-file=.env
    #   kubectl apply -f kubernetes.yaml
    # Services are cluster-internal: <name>.mcp.svc.cluster.local:8000/mcp. Put an ingress
    # with auth in front before exposing any of them.
    apiVersion: v1
    kind: Namespace
    metadata:
      name: mcp
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: search
      namespace: mcp
      labels: &id001
        app: search
    spec:
      replicas: 1
      selector:
        matchLabels: *id001
      template:
        metadata:
          labels: *id001
        spec:
          containers:
          - name: search
            image: CHANGE_ME
            ports:
            - containerPort: 8081
              name: http
            env: []
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: search
      namespace: mcp
    spec:
      selector:
        app: search
      ports:
      - port: 8081
        targetPort: http
        name: http
    
    verified 2026-10-18 · v0.3.5 · r-20261018T115948Z-bfd6cf950347 deployed 2026-10-18 · 5 tools
    # Generated by Hub for toolset "my-agent" (2026-10-18T22:56:52+00:00).
    # Credentials come from the Secret 'mcp-credentials' (kubectl create secret generic ... --from-env-file=.env).
    # Each server under `servers` is one Deployment + Service; the keys after it configure
    # upstream charts a worker verified.
    servers:
      search:
        enabled: true
        replicas: 1
        port: 8081
        path: /mcp
        container:
          name: search
          image: CHANGE_ME
          ports:
          - containerPort: 8081
            name: http
          env: []
    
    # helm install my-agent helm/my-agent -n mcp
    

    Scanned github.com/ag-bureau/mcp-search/browser at 8e781f8e1080 with [email protected][email protected] on 2026-10-18 before the build: 0 dependencies, no known vulnerabilities, nothing in the code that looks malicious.

    in the cloud

    one click: the provider's deploy form opens filled in, in your own account
    koyeb

    Koyeb builds the repository's Dockerfile from branch main, not commit 8e781f8e1080, the one the worker scanned and built (it takes branches only); change the branch in the form if the default one has another name.

    Clients connect to the service's https://…koyeb.app address plus /mcp.

    cloud run

    Cloud Shell clones the repository, builds its Dockerfile and deploys it to Cloud Run.

    search: Cloud Shell clones the default branch, not the commit the worker scanned and built (it takes branches only).

    Cloud Run sends requests to $PORT (8080); the recipe expects :8081, so the server has to read PORT, or set the container port on the service afterwards.

    connect to it

    copy what your client needs
    {
      "mcpServers": {
        "search": {
          "command": "CHANGE_ME",
          "args": []
        }
      }
    }
    #!/usr/bin/env bash
    # Registers every server with Claude Code (run once; `claude mcp list` to check).
    set -euo pipefail
    set -a; [ -f .env ] && source .env; set +a
    claude mcp add search -- CHANGE_ME
    
    {
      "mcpServers": {
        "search": {
          "command": "CHANGE_ME",
          "args": []
        }
      }
    }
    {
      "servers": {
        "search": {
          "type": "stdio",
          "command": "CHANGE_ME",
          "args": []
        }
      }
    }
    name: my-agent
    target: local
    servers:
    - hub: com.ag-bureau/search
    
    • no recipe for target 'local': fill the files by hand

    tools · 5

    • web_readtestedRead a web page by address and return its text. HOW IT DIFFERS FROM web_search, WHICH ALSO READS. That one reads the top three links of its own results at 6000 characters each — enough for an answer. This one takes the addresses YOU name, up to five at a time, reads them in full with a cursor over a long document, can demand the browser and can check for a marker. If you need an answer, search is enough; if you need to work with a document, come here. WHEN TO CALL. You need the text of a specific page whose address is already known — from web_search results or from the user. You need facts from an article rather than a snippet about it. Read a long page in parts: the same call with the offset named at the end of the truncated text. WHEN NOT TO CALL. There is no address yet — use web_search first. You need an office document (DOCX, XLSX) — the tool does not parse those and will say so plainly; PDF, however, IS read. A search-engine result page must not be read: it merges neighbouring results into one text and hands you facts about a namesake. WHAT IT RETURNS. results[] per address: content — the page text, status — what became of it, title, published, lang, final_url (where a redirect led), stub_check — whether this is a block; text_source — HOW the text was obtained. HOW TO READ THE ANSWER — four things that are easy to get wrong. 1. EMPTY CONTENT IS A SUCCESS, not a failure: the page opened and has no text in it. Repeating is pointless, take another source. On a refusal or a failure to open, repeating does make sense. 2. `ok` is about the TOOL, not about the pages: it stays true even if not one page was read. Look at count and failed, and at the status of each address: `read`, `empty`, `stub`, `refused`, `unreachable`, `forbidden`, `not_reached`. They mean different things and call for different next steps — `empty` is not worth repeating, `refused` and `unreachable` are; `not_reached` is news about US (out of time, the per-domain rate limit, the browser busy — its reason says so, retry later — or beyond the batch ceiling) and says nothing about the page. 3. text_source IS REQUIRED READING when it says the text was recognised. That is a scanned PDF with no text layer: the pages were rendered and read by a vision model, and such text MUST NOT be quoted as exact — a measurement recovered 94% of the reference numbers. The details are in the `recognition` field, including the dpi and whether the model's answer was cut off. A text layer means "copied out of the file" and is quotable verbatim. 4. A stub status means we met an anti-bot shield or a paywall: text arrived, but it is not from the page. Do not retell it as the content. A stub_check of "not checked" is NOT "clean". PDF. It is read; `pages` and `pages_read` say how many pages the document has and how many were parsed. An empty result on a PDF means "there are pages and no text" — that is a scan, cured by recognition rather than by repeating. And remember: in a PDF the characters can be extracted correctly while the reading order falls apart, so labels come away from their values. Do not assemble "property: value" pairs out of adjacent PDF lines without checking that they really are adjacent.

      Read a web page by address and return its text. HOW IT DIFFERS FROM web_search, WHICH ALSO READS. That one reads the top three links of its own results at 6000 characters each — enough for an answer. This one takes the addresses YOU name, up to five at a time, reads them in full with a cursor over a long document, can demand the browser and can check for a marker. If you need an answer, search is enough; if you need to work with a document, come here. WHEN TO CALL. You need the text of a specific page whose address is already known — from web_search results or from the user. You need facts from an article rather than a snippet about it. Read a long page in parts: the same call with the offset named at the end of the truncated text. WHEN NOT TO CALL. There is no address yet — use web_search first. You need an office document (DOCX, XLSX) — the tool does not parse those and will say so plainly; PDF, however, IS read. A search-engine result page must not be read: it merges neighbouring results into one text and hands you facts about a namesake. WHAT IT RETURNS. results[] per address: content — the page text, status — what became of it, title, published, lang, final_url (where a redirect led), stub_check — whether this is a block; text_source — HOW the text was obtained. HOW TO READ THE ANSWER — four things that are easy to get wrong. 1. EMPTY CONTENT IS A SUCCESS, not a failure: the page opened and has no text in it. Repeating is pointless, take another source. On a refusal or a failure to open, repeating does make sense. 2. `ok` is about the TOOL, not about the pages: it stays true even if not one page was read. Look at count and failed, and at the status of each address: `read`, `empty`, `stub`, `refused`, `unreachable`, `forbidden`, `not_reached`. They mean different things and call for different next steps — `empty` is not worth repeating, `refused` and `unreachable` are; `not_reached` is news about US (out of time, the per-domain rate limit, the browser busy — its reason says so, retry later — or beyond the batch ceiling) and says nothing about the page. 3. text_source IS REQUIRED READING when it says the text was recognised. That is a scanned PDF with no text layer: the pages were rendered and read by a vision model, and such text MUST NOT be quoted as exact — a measurement recovered 94% of the reference numbers. The details are in the `recognition` field, including the dpi and whether the model's answer was cut off. A text layer means "copied out of the file" and is quotable verbatim. 4. A stub status means we met an anti-bot shield or a paywall: text arrived, but it is not from the page. Do not retell it as the content. A stub_check of "not checked" is NOT "clean". PDF. It is read; `pages` and `pages_read` say how many pages the document has and how many were parsed. An empty result on a PDF means "there are pages and no text" — that is a scan, cured by recognition rather than by repeating. And remember: in a PDF the characters can be extracted correctly while the reading order falls apart, so labels come away from their values. Do not assemble "property: value" pairs out of adjacent PDF lines without checking that they really are adjacent.

      called 2026-10-18 · 42 ms · arguments from the schema
      pin f50710eb55c4ed94fdef079eea6d0f1cc414cd98b2374444b90bc1af81ca6ff8
    • web_screenshotA PNG screenshot of a web page. The fourth tool of the module. WHEN TO CALL. You need to SHOW a page to a person — the layout, the design, what the text does not carry. And you need to CROSS-CHECK: the shot and the text are obtained in one browser visit but by different routes — the pixels are drawn by the layout engine, the text comes from the DOM. A disagreement between them catches what neither route sees alone. WHEN NOT TO CALL. You need the text of the page — that is web_read, many times cheaper. A screenshot costs a browser launch. WHAT IT RETURNS. png_base64 — the shot itself; bytes — its size; page_text — the text of THE SAME visit, up to max_chars; page_text_chars — the length of the whole text, which may be greater; page_text_truncated; browser_version. HOW TO READ THE ANSWER — two things, and they are DIFFERENT. 1. shot_taken — THE SHOT WAS TAKEN: the browser is alive, the page loaded. 2. expected_found — WHAT WE EXPECTED IS ON THE PAGE (when `expect` was given). A shot can be taken flawlessly and show the wrong thing: a stub, a captcha, an error page. Do not confuse these two fields — a page that honestly failed a check and a browser that never opened are different events. A signal worth seeing: there is a shot and page_text_chars is near zero — the page drew, and has nothing to say.

      A PNG screenshot of a web page. The fourth tool of the module. WHEN TO CALL. You need to SHOW a page to a person — the layout, the design, what the text does not carry. And you need to CROSS-CHECK: the shot and the text are obtained in one browser visit but by different routes — the pixels are drawn by the layout engine, the text comes from the DOM. A disagreement between them catches what neither route sees alone. WHEN NOT TO CALL. You need the text of the page — that is web_read, many times cheaper. A screenshot costs a browser launch. WHAT IT RETURNS. png_base64 — the shot itself; bytes — its size; page_text — the text of THE SAME visit, up to max_chars; page_text_chars — the length of the whole text, which may be greater; page_text_truncated; browser_version. HOW TO READ THE ANSWER — two things, and they are DIFFERENT. 1. shot_taken — THE SHOT WAS TAKEN: the browser is alive, the page loaded. 2. expected_found — WHAT WE EXPECTED IS ON THE PAGE (when `expect` was given). A shot can be taken flawlessly and show the wrong thing: a stub, a captcha, an error page. Do not confuse these two fields — a page that honestly failed a check and a browser that never opened are different events. A signal worth seeing: there is a shot and page_text_chars is near zero — the page drew, and has nothing to say.

      needs auth 2026-10-18 · 42 ms · arguments from the schema · the tool asked for credentials
      pin 6028d6b79d7ad3cc867a9d3f6e5f39146aa1ea4a1bb259019f1b94595a3b18e2

    runs · 3

    the latest tests of this entry by the hub and its workers, newest first
    dateworkermethodstatussandboxreason
    2026-10-18 18:18 local-mac-vm baremetal ok —hub-targets —
    2026-10-18 11:59 local-mac-vm dockermcp 2024-11-05 ok5 tools gvisor —
    2026-10-17 13:40 local-mac-vm docker failed gvisor docker: sandbox: docker build: ERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory