Skip to main content
Glama

baumarkt-mcp

An MCP server that lets an LLM search four German hardware-store (Baumarkt) retailers — Hornbach, BAUHAUS, Globus Baumarkt and OBI — through one tool surface. Built for self-hosting: one container, no API keys, no per-query credits. The point is the single compare_price call: it fans out over all four retailers and answers "who sells this cheapest, and can I collect it locally today" — every result carries a store_pickup field scoped to the local (Braunschweig-area) branches.

Companion to jnslmk/geizhals-mcp (new retail goods) and jnslmk/kleinanzeigen-mcp (second-hand classifieds).

Tools

Tool

What it does

search_products

Keyword search of one retailer or all four, with an optional client-side max_price filter. Returns product summaries.

get_product

Full detail for one product id at one retailer.

compare_price

Fan-out over all four retailers, sorted cheapest first — the "who sells this cheapest, can I collect it locally today" answer in one call.

The intended flow is compare_price (or search_products with retailer="all") → pick interesting ids → get_product. Every result is one normalised product with the same shape: retailer, id, name, brand, gtin, price, currency, availability, url, image, store_pickup.

search_products

search_products(query, retailer="all", store=None, max_price=None, max_results=20)
  • retailer"all" (default) fans out over hornbach, bauhaus, globus and obi; or exactly one of those four names.

  • store — optional branch/store identifier; None uses each retailer's default (Braunschweig) branch. Per-retailer semantics in "The retailers".

  • max_price — maximum price in EUR (≥ 0), typed str | int because LLMs routinely send "600" as a string and the schema accepts it. Filters results client-side after fetching, so a product with no listed price (price: null) cannot be filtered and is kept.

  • max_results — per-retailer cap, also str | int, hard-capped at BM_MAX_RESULTS (default 50).

Returns {query, returned, results, errors}results is the flat list of matching products across the requested retailers, errors the per-retailer failures.

get_product

get_product(retailer, product_id, store=None)

retailer must be exactly one of the four names; product_id is the retailer's own id/sku as returned by a search. Returns {retailer, product, error}product is null when the id does not resolve, or when the retailer fails. A retailer failure is additionally reported in error (a non-null message) so it is distinguishable from id-not-found, where error is null.

compare_price

compare_price(query, store=None, max_results=20)

Always fans out over all four retailers and sorts the merged results by price ascending — products with no listed price sort last. Returns {query, returned, results, errors}.

Failure handling

A failing retailer never fails the call. Each retailer runs independently and a failure becomes an entry in the tool's errors list while the other retailers' results are still returned. "Blocked" is never collapsed into "no results": a bot-wall timeout reads as blocked by a challenge: ... (or blocked by a CAPTCHA: ..., browser pool exhausted: ...), which is a different answer from an empty result list.

Related MCP server: Swissgroceries MCP

The bauhaus limitation

bauhaus.info sits behind a Cloudflare Turnstile challenge. The adapter deliberately does not automate interaction with the challenge widget — presenting a real browser and being passively let through is one thing, scripting the widget itself is a different thing this project does not do. So when bauhaus serves the interactive checkbox variant (or a hard or slow challenge), searches through this server return no bauhaus results and the tool reports a clear blocked-by-challenge error instead: blocked by a challenge: bauhaus served an interactive Cloudflare Turnstile challenge (the checkbox variant, ...) — this adapter deliberately does not attempt to clear it.

This is intended behaviour — the deliberate cost of not defeating the bot-control. bauhaus is the least reliable of the four retailers and may routinely fail; the other three are unaffected, and every tool output makes it explicit which retailer(s) failed rather than pretending bauhaus had nothing.

The retailers

Retailer

Approach

Bot wall

Local store

Hornbach

JSON-LD only (search ItemList, detail Product), headed Chromium

F5/Fastly "Client Challenge"; headless escalates to an image CAPTCHA

Braunschweig, branchCode 615

BAUHAUS

Captures the page's own /api/products responses, never replayed; JSON-LD fallback

Cloudflare Turnstile (two variants — see above)

Braunschweig, storeId 607

Globus

FactFinder-rendered listing, DOM-parsed; JSON-LD on the detail page

none observed (defensive check only)

Braunschweig, store id 212

OBI

Plain HTTP (httpx), no browser

none

none — online pricing only, never pickup

Hornbach

hornbach.de sits behind an F5/Fastly "Client Challenge" bot wall. Plain HTTP gets served a ~3 KB challenge body with HTTP 200 — indistinguishable from success by status code alone. Headless Chromium escalates to an image CAPTCHA; headed Chromium clears the challenge effectively immediately. There is no DOM scraping: the search page (/s/<query>) carries a schema.org ItemList JSON-LD block, and the detail page (/p/<slug>/<sku>/) a Product block with sku and gtin13. The page carries at most one pickup offer — for the visitor's default branch (Braunschweig, branchCode 615); the store parameter filters among whatever pickup offer the page carries but cannot fetch another branch's real availability.

BAUHAUS

bauhaus.info is behind a Cloudflare managed challenge (Turnstile) plus its own branded WAF block page. The search results API (/api/products?productIds=...&filter=...) returns clean JSON — but re-requesting that exact URL from inside an already-cleared page gets the branded 403 page back, never JSON. So the adapter never constructs or replays that URL: it registers a page.on("response", ...) handler before navigating and reads whatever the page's own JavaScript requests, dict-merging the results as they arrive (the search page's ItemList JSON-LD is the fallback). A detail page carries no price in its JSON-LD either — the price is pulled out of the embedded server-rendered payload, and captured /api/purchasability responses provide availability and a real per-store pickup signal. The selectedStore cookie pins the storefront to Braunschweig (storeId 607). Search results have no confirmed per-store signal, so their store_pickup is None rather than a guess.

Globus Baumarkt

globus-baumarkt.de is Shopware 6 with a FactFinder search integration; the results listing is client-rendered and carries no JSON-LD, so search drives a headed browser and parses the rendered cards from the DOM (/search/result?query=..., paginated with &p=<n>, at most 5 pages). The detail page carries a schema.org Product JSON-LD block (name, sku, gtin13, brand, offers) plus a server-rendered "In diesem Markt" widget that gives a real store_pickup reading. get_product looks an article number up by searching for it — a unique hit redirects straight to the product detail page. A fresh browser context resolves to the Braunschweig branch (store id 212); the store parameter is accepted for signature parity but is not wired to store-switching and logs a warning when passed.

OBI

obi.de has no bot wall (verified: a plain-HTTP request with a browser-shaped User-Agent returns real search results), so this adapter is plain httpx — no browser, no Xvfb, and it never touches the shared browser pool. search parses the window.__INITIAL_STATE__ JSON blob embedded in the search page (/search/<query>/); get_product parses the Product JSON-LD on the detail page (/p/<id>). OBI has no Braunschweig-area branch, so it is an online-only price source here: every result has store_pickup: null and store is accepted but ignored — an OBI price is never a pickup price.

How it works

Three of the four retailers sit behind bot walls, and headless Chromium does not clear them: on hornbach.de it gets served an image CAPTCHA, on bauhaus.info a hard 403. The container therefore runs headed Chromium under Xvfb, driven by patchright (a patched, undetected Playwright fork with the same async API). One BrowserManager owns the browser and a small pool of reusable contexts (BM_MAX_CONCURRENT, default 2): a context that has already cleared a wall keeps its cookies, so reusing it is more valuable than starting cold again. wait_for_challenge_clear polls the page every 250 ms and recognises both wall shapes — hornbach's F5/Fastly challenge (raising CaptchaRequired on the unrecoverable image-CAPTCHA variant) and bauhaus's Turnstile/WAF pages — so a wall that never clears ends as an explicit blocked by a challenge error, never as an empty result. obi is excluded from the pool entirely.

Running it

docker build -t baumarkt-mcp .
docker run --rm -p 8000:8000 baumarkt-mcp

Streamable HTTP at http://localhost:8000/mcp, plain GET /healthz for healthchecks (the image ships a matching Docker HEALTHCHECK). Give the container at least 1.5 GB of memory — Chromium under Xvfb is the heavy part.

The entrypoint starts Xvfb (display BM_DISPLAY, default 99, screen BM_SCREEN, default 1366x900x24), waits for the X socket to appear, then execs the server as PID 1. It deliberately does not use xvfb-run: its signal handshake races as PID 1 in a container and can leave Xvfb up but the server never binding its port.

Configuration

Variable

Default

Purpose

MCP_TRANSPORT

http

http (streamable HTTP) or stdio

MCP_HOST

0.0.0.0

Bind address

MCP_PORT

8000

Bind port

MCP_PATH

/mcp

MCP endpoint path

BM_MAX_CONCURRENT

2

Browser contexts to pool — pool size and concurrency limit in one

BM_MAX_RESULTS

50

Hard cap on products per retailer per search

BM_HEADLESS

0

1 runs headless — local development only, will not clear the hornbach/bauhaus walls

BM_CHALLENGE_TIMEOUT_MS

25000

How long to wait for a bot-wall challenge to clear

BM_PROXY

(none)

Egress proxy URL for the browser, e.g. http://10.0.0.5:8888

BM_PROXY_USERNAME / BM_PROXY_PASSWORD

(none)

Optional proxy auth

BM_DISPLAY

99

Xvfb display number (entrypoint)

BM_SCREEN

1366x900x24

Xvfb screen geometry (entrypoint)

LOG_LEVEL

INFO

Python log level

The BM_* defaults are tuned for a chat agent making one request at a time. Raise BM_MAX_CONCURRENT for throughput, at the cost of memory and a greater chance of tripping these sites' bot detection.

LibreChat

mcpServers:
  baumarkt:
    type: streamable-http
    url: "http://baumarkt-mcp:8000/mcp"
    timeout: 180000
    chatMenu: true

mcpSettings:
  allowedAddresses:
    - "baumarkt-mcp:8000"

allowedAddresses is required — LibreChat's SSRF guard blocks MCP URLs that resolve to private addresses, which a sibling container always does. The timeout is generous because a cold call starts a browser and may wait out a bot-wall challenge.

Claude Code

claude mcp add --transport http baumarkt http://localhost:8000/mcp

Development

uv venv && uv pip install -e .
patchright install chromium
BM_HEADLESS=1 MCP_TRANSPORT=stdio uv run python -m baumarkt_mcp

Headless is fine for a quick local check; the container default (headed under Xvfb) is what actually clears the bot walls. MCP_TRANSPORT=stdio runs the server as a local stdio server instead of HTTP.

Lint with ruff check and ruff format --check. The globus adapter ships a dependency-free regression check runnable directly:

python -m baumarkt_mcp.retailers.globus

It feeds synthetic listing HTML — including the discounted-card shape that once produced a wrong-but-plausible price — through the real parsing path and asserts the correct sale price comes out. No network involved.

Images

ghcr.io/jnslmk/baumarkt-mcp — multi-arch (linux/amd64, linux/arm64), built by GitHub Actions on native runners for each architecture.

Tag

Meaning

latest

Newest build of main

sha-<full-sha>

A specific commit

v0.1.0, v0.1

Release tags

Caveats

None of these retailers has a public API, so this server scrapes their sites with a real browser (or, for obi, plain HTTP). It can break whenever they change their markup or tighten their bot walls, and heavy or parallel use may trip bot detection. Scraping is also at odds with the retailers' terms of service. Keep it to personal-scale use — the conservative concurrency defaults exist for exactly this reason.

License

MIT — see LICENSE.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    -
    quality
    D
    maintenance
    An MCP server that enables real-time product, price, and stock searching across major Turkish electronic component retailers including Robotistan, Robocombo, Direnc.net, and Robo90. It allows AI models to compare local market data to find the best deals and available components.
    Last updated
    3
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    A Model Context Protocol server for real-time Swiss grocery shopping that searches and compares products across 8 major Swiss retailers (Migros, Coop, Aldi, Denner, Lidl, Farmy, Volgshop, Otto’s), normalizes per-unit prices, surfaces promotions, computes optimal multi-store shopping plans, and works with any MCP-compatible client without API keys or accounts.
    Last updated
    7
    44
    26
    AGPL 3.0
  • F
    license
    -
    quality
    C
    maintenance
    MCP server that turns Wildberries marketplace into a toolkit for LLM agents, enabling product search, detailed card inspection, price history, reviews, and cross-product comparison.
    Last updated
  • A
    license
    -
    quality
    C
    maintenance
    An MCP server that lets an LLM search Geizhals, a leading German/DACH price-comparison site, for the cheapest offers on new products.
    Last updated
    MIT

View all related MCP servers

Related MCP Connectors

  • Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.

  • MCP server for generating rough-draft project plans from natural-language prompts.

  • MCP server for AI dialogue using various LLM models via AceDataCloud

View all MCP Connectors

Latest Blog Posts

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/jnslmk/baumarkt-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server