baumarkt-mcp
Provides tools for searching products, retrieving product details, and comparing prices at Globus Baumarkt, including local store pickup availability.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@baumarkt-mcpcompare prices for a cordless drill"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| Keyword search of one retailer or all four, with an optional client-side |
| Full detail for one product id at one retailer. |
| 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;Noneuses each retailer's default (Braunschweig) branch. Per-retailer semantics in "The retailers".max_price— maximum price in EUR (≥ 0), typedstr | intbecause 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, alsostr | int, hard-capped atBM_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 | F5/Fastly "Client Challenge"; headless escalates to an image CAPTCHA | Braunschweig, branchCode |
BAUHAUS | Captures the page's own | Cloudflare Turnstile (two variants — see above) | Braunschweig, storeId |
Globus | FactFinder-rendered listing, DOM-parsed; JSON-LD on the detail page | none observed (defensive check only) | Braunschweig, store id |
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-mcpStreamable 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 |
|
|
|
|
| Bind address |
|
| Bind port |
|
| MCP endpoint path |
|
| Browser contexts to pool — pool size and concurrency limit in one |
|
| Hard cap on products per retailer per search |
|
|
|
|
| How long to wait for a bot-wall challenge to clear |
| (none) | Egress proxy URL for the browser, e.g. |
| (none) | Optional proxy auth |
|
| Xvfb display number (entrypoint) |
|
| Xvfb screen geometry (entrypoint) |
|
| 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/mcpDevelopment
uv venv && uv pip install -e .
patchright install chromium
BM_HEADLESS=1 MCP_TRANSPORT=stdio uv run python -m baumarkt_mcpHeadless 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.globusIt 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 |
| Newest build of |
| A specific commit |
| 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.
This server cannot be installed
Maintenance
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
- Alicense-qualityDmaintenanceAn 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 updated3MIT
- AlicenseAqualityBmaintenanceA 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 updated74426AGPL 3.0
- Flicense-qualityCmaintenanceMCP 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
- Alicense-qualityCmaintenanceAn MCP server that lets an LLM search Geizhals, a leading German/DACH price-comparison site, for the cheapest offers on new products.Last updatedMIT
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
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/jnslmk/baumarkt-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server