MCP server by isaamthalhath07
opensooq-mcp
A comprehensive, read-only MCP server that gives any LLM agent live access to OpenSooq — the largest classifieds marketplace in Kuwait (and the wider Gulf/ Levant). It turns natural-language questions like "what's the going rate for a used iPhone 15 Pro in Hawally, and who are the trusted sellers?" into real marketplace data: listings, prices, deals, categories, and seller reputation.
OpenSooq publishes no official API, so this server reverse-engineers and
replicates the web client's private serp/search/v2 API — including
its anonymous device-registration + JWT-signing handshake — and exposes 15
well-documented tools through the Model Context Protocol.
Drop the folder anywhere and
run it with uv (no Python install, no venv, no
config); see Quick start.
Table of contents
- Features at a glance
- Quick start
- Integrating with an MCP client
- See it in action
- How the MCP works
- Tool reference
- Use cases & applications
- Price-drop watcher (cron-ready)
- Data-quality handling
- Limitations & responsible use
- Development & testing
- Troubleshooting
Features at a glance
| Area | What you get | |------|--------------| | Search | Full-text search with server-side sorting (relevant / newest / cheapest / priciest), category + city scoping, and client-side refinements (price range, media, seller quality). Bilingual — pass an Arabic term for much better recall. | | Listing detail | Any listing by id, enriched with the full untruncated description, attribute rows, and a rich seller profile. | | Categories & cities | The full category taxonomy and city list, plus category-first browsing and per-query category breakdowns. | | Pricing & deals | Market price summaries (min/max/avg/median/quartiles), price histograms, a below-market deal finder, and side-by-side price comparison across variants. | | Sellers & reputation | Seller leaderboards, a seller's full inventory, a complete reputation profile, and a trusted-seller finder to reduce scam risk. | | Robustness | Outputs are defended against OpenSooq's noisy data (accessories, troll prices, loosely-related fallback results). |
Every tool is read-only (it never posts, edits, or deletes), returns either human-readable markdown or structured JSON, and has rich docstrings the agent uses to pick the right tool automatically.
See it in action
The whole point of the MCP is turning a vague, training-data answer into live marketplace facts. Same three questions, asked of the model without the MCP vs. with it (real OpenSooq Kuwait data, captured 2026-06-13):
"Cheapest Samsung Galaxy S26 Ultra?"
| Without the MCP | With the MCP | |---|---| | "I can't see live listings. A new flagship Ultra in the Gulf is maybe 350–450 KWD — but I can't confirm any actual price, seller, or that it's even listed." | 310 KWD (256 GB) — real listings: Almohands Shop 🏪 ★3.65 in Kuwait City (282305058) and a sealed one in Farwaniya (281723370). It also caught two traps a naive price-sort puts first: a "200 KWD 1TB" that's actually a social-media-account listing in disguise, and a "280 KWD" that's an iPhone offered in exchange — and warned that only 20/150 scanned were genuine S26 Ultras. |
"Going rate for a used iPhone 15 Pro?"
| Without the MCP | With the MCP | |---|---| | "Roughly $600–800 used → very loosely ~190–250 KWD. A guess — not Kuwait-specific, not current, no idea of the spread." | From 55 real priced listings: median 200 KWD, average 196.6, range 100–275, p25–p75 = 155–230 KWD. So "fair" is ~200, and under ~155 is a genuine deal (or a red flag). |
"Which sellers are trustworthy for an iPhone 15 Pro?"
| Without the MCP | With the MCP | |---|---| | "Unanswerable — I don't know individual sellers. Generic advice only: prefer verified sellers, meet in public…" | Named verified sellers ≥★4: Hamza ✓ ★5.0 at 210 KWD (282253000), LaRushka ✓ ★5.0 at 250 KWD (281670200) — with member ids, ratings, and links. |
The difference in one line: stale, generic, global guesses → live, specific, Kuwait-current listings, prices, sellers, and scam-filtering.
Quick start
The only requirement is uv — one tool that
handles everything else. You do not install Python or make a virtualenv; uv
fetches the pinned Python (3.12, from the bundled .python-version) and the deps
(mcp, httpx, pydantic) automatically. Works the same on Windows, macOS, Linux.
1. Install uv (once per machine):
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Restart your terminal so uv is on PATH.
2. Run the installer — from this folder:
python install.py
That one command does everything: it provisions Python + deps, verifies the server loads, then auto-writes the correct config into every MCP client it finds on your machine (Claude Desktop, Cursor, Antigravity, Windsurf) — backing up each file first. Fully restart your client, then ask "search OpenSooq for iphone 15 pro". Done.
python install.py --print-only— just print the config, change nothing.- Moved the folder, or merged it elsewhere? Re-run
python install.pyand it rewrites every client config with the new path. (It's how you stay portable.)
Prefer to wire it up by hand, or want the per-client file locations? See Integrating with an MCP client. No
uv? See Plan B.
Integrating with an MCP client
python install.py (above) does this for you. This section is for doing it by
hand, or just to see where each client keeps its config.
All MCP clients use the same idea: they spawn the server over stdio. With uv the
block is identical on every OS and client — you only change the path to the
opensooq-mcp folder:
{
"mcpServers": {
"opensooq": {
"command": "uv",
"args": [
"run",
"--directory", "/ABSOLUTE/PATH/TO/opensooq-mcp",
"server.py"
],
"env": { "PYTHONIOENCODING": "utf-8" }
}
}
}
That single block is all you need below. uv reads the deps + pinned Python (3.12,
from the bundled .python-version) and provisions them automatically — no
pip install, no venv, no version conflicts, no GitHub download for a newer Python.
If
uvisn't found by your client: GUI apps (e.g. Claude Desktop) sometimes don't inherit your terminal'sPATH. Put the absolute path touvincommand(which uv/where.exe uv). The installer already does this.
Legacy: pointing at a Python interpreter directly (Plan B)
If you used Plan B (your own venv), set "command" to that
venv's python and "args" to the absolute path of server.py:
{
"mcpServers": {
"opensooq": {
"command": "/ABSOLUTE/PATH/TO/opensooq-mcp/.venv/bin/python",
"args": ["/ABSOLUTE/PATH/TO/opensooq-mcp/server.py"],
"env": { "PYTHONIOENCODING": "utf-8" }
}
}
}
On Windows use .venv\Scripts\python.exe; if you didn't make a venv, use "command": "python" (macOS/Linux) or
"command": "py" (Windows) — just make sure that interpreter has the deps.
Claude Desktop
Edit claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Paste the config block above (merge into any existing mcpServers), save, and
fully restart Claude Desktop. You'll see a 🔌/tools icon; ask "search
OpenSooq for iphone 15 pro" to confirm it's live.
Claude Code (CLI)
The fastest path is the CLI helper — it writes the config for you:
claude mcp add opensooq -e PYTHONIOENCODING=utf-8 -- uv run --directory /abs/path/opensooq-mcp server.py
Then claude mcp list to verify, and /mcp inside a session to inspect the
tools. (You can also commit a project-scoped .mcp.json with the same
mcpServers block so teammates get it automatically.)
Cursor
Create .cursor/mcp.json in your project (or ~/.cursor/mcp.json for a
global server) with the config block above. Then open Cursor Settings → MCP,
confirm opensooq is listed and toggled on (green). In Composer/Agent, the model
can now call the tools; you may be asked to approve tool use the first time.
Google Antigravity
Antigravity (Gemini) reads MCP servers from ~/.gemini/config/mcp_config.json
(on Windows: C:\Users\<you>\.gemini\config\mcp_config.json).
- Create/open that file and paste the reusable
mcpServersblock from above (samecommand/args/env— Antigravity uses the standard schema), or just runpython install.py, which writes this file for you. - Save and fully restart Antigravity. Ask "find the cheapest verified-seller iPhone 15 on OpenSooq" to test.
Earlier builds hit
dns error: No such host is known→calling "initialize": EOFhere, becauseuvtried to download a newer Python (3.14) from GitHub. The bundled.python-version(3.12) now prevents that —uvreuses the cached interpreter and starts immediately.
Windsurf / other MCP clients
- Windsurf:
~/.codeium/windsurf/mcp_config.json, samemcpServersblock. - Any stdio MCP client (Zed, Cline, custom hosts, the MCP Inspector): they
all accept a
command+args(+ optionalenv). Use the block above. - Debug with the MCP Inspector:
npx @modelcontextprotocol/inspector uv run --directory /abs/path/opensooq-mcp server.py— a browser UI to list/call the tools directly, great for verifying the server in isolation.
Programmatic use (no LLM)
You don't need an LLM to use the data layer. opensooq_client.py has no MCP/
Pydantic dependency and can be imported anywhere httpx is installed:
import asyncio, opensooq_client as oc
async def main():
# one page of normalized listings
listings, meta = await oc.search_page("iphone 15 pro", sort=oc.SORT_CODES["cheapest"])
print(meta["count"], "total;", listings[0]["price"], listings[0]["currency"])
# a specific seller's whole inventory
items, m = await oc.fetch_member_listings("42899294", page=1)
# one listing by id
one = await oc.fetch_listing_by_id("281910976")
asyncio.run(main())
This is how you'd embed OpenSooq data into a backend, a cron job, or another app without going through the MCP layer.
Plan B — without uv
Prefer to manage Python yourself? You need Python 3.10+, then:
cd opensooq-mcp
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt # Windows: .venv\Scripts\python
Then point your client at that venv's interpreter (the Legacy config block in
Integrating). requirements.txt:
mcp>=1.2.0
httpx>=0.27.0
pydantic>=2.0.0
Note: only the MCP server (server.py) needs Python 3.10+. The standalone
watch.py and opensooq_client.py need only
httpx and run on Python ≥3.9.
How the MCP works
Architecture
┌─────────────────────┐ stdio (MCP) ┌──────────────────────────────────────┐
│ MCP client │ ◄────────────► │ server.py (FastMCP tool layer) │
│ Claude / Cursor / │ tools/call │ • 15 @mcp.tool functions │
│ Antigravity / … │ │ • Pydantic input models (validation) │
└─────────────────────┘ │ • markdown / JSON formatting │
│ • relevance + price-band defenses │
└───────────────┬──────────────────────┘
│ imports
┌───────────────▼──────────────────────┐
│ opensooq_client.py (pure client) │
│ • device registration + JWT signing │
│ • raw_search / post_ids / member_ids │
│ • HTML/RSC scraping for extras │
│ • parsing → stable listing schema │
└───────────────┬──────────────────────┘
│ HTTPS
┌───────────────▼──────────────────────┐
│ kw.opensooq.com (private web API) │
└──────────────────────────────────────┘
The split is deliberate: server.py is the MCP/agent surface (tools,
validation, formatting, data-quality logic), while opensooq_client.py is a
dependency-light API client that's independently testable and reusable.
The OpenSooq handshake (the hard part)
OpenSooq's search API rejects unauthenticated requests, so the client replicates exactly what the browser does:
- Register an anonymous device —
POST /api/register-devicereturns a secret triple(deviceUUID, t, k). This is cached and reused for ~4 minutes. - Mint a short-lived HS256 JWT — the signing key is not
kdirectly; the browser derives it with a two-stage HMAC-SHA256 chain (HMAC(k, "desktop..rnd.t")→ base64url → that becomes the HMAC key over the token body)._generate_jwt()reproduces this precisely. - Send the exact cookie + header set the SERP endpoint expects (Kuwait
locale, KWD currency,
desktopsource,x-tracking-uuid, release version…). - POST the query, then normalize the noisy raw payload into one stable listing schema (see below). If a request 401s (stale registration), the client re-registers once and retries automatically.
What one normalized listing looks like
{
"id": "281910976",
"title": "Apple iPhone 15 Pro Max 256 GB in Hawally",
"description": "…", // masked/preview from search
"price": 210.0, "currency": "KWD", // 0 = "price on request"
"postedDate": "2026-05-31T…Z", "insertedDate": "2026-05-31", "expiresAt": "30-06-2026",
"category": "Mobiles", "categoryPath": ["Mobile - Tablet", "Mobiles"],
"location": { "city": "Hawally", "neighborhood": "Hawally" },
"status": "posted", "isActive": true,
"media": { "imageUrl": "…", "gallery": ["…"], "imageCount": 8, "hasVideo": false, "has360": false },
"tags": ["Delivery by seller"],
"seller": {
"member_id": "42899294", "display_name": "…", "username": "…",
"badges": ["shop"], "is_shop": true, "rating_avg": 3.2, "rating_count": 274,
"is_verified": false, "verification_level": 0,
"phone_masked": "555471XX", "has_phone": true
},
"url": "https://kw.opensooq.com/…"
}
Endpoints discovered (beyond plain search)
Several capabilities aren't in the obvious API and were recovered by inspecting the site's server-rendered payloads:
- Per-seller listings → the search body accepts a top-level
member_idsarray (not insidefilters). Returns a seller's whole inventory, paginated, with their authoritative total count. (Found by reading the seller shop page's embedded__NEXT_DATA__, which itself issues amember_ids-filtered search.) - Single listing by id → a top-level
post_idsarray returns exactly that listing. (rel_typemust not be sent alongside these — it triggers a 500.) - Rich detail (full description, attribute rows, member-since / response-time
/ total-listings / profile picture) → scraped best-effort from the listing
page's App-Router RSC stream (
self.__next_fchunks), because the JSON/api/listing/{id}endpoint requires a login (401). If the page format changes, these extras degrade gracefully to the API-level data.
Verified API capabilities & limits
| Capability | Status |
|------------|--------|
| Full-text search, pagination, total counts | ✅ server-side |
| Sort: relevant / newest / cheapest / priciest | ✅ server-side (sort_code) |
| Category filter (cat_ids) + city filter (city_ids) | ✅ server-side |
| Per-query category facets | ✅ server-side |
| Per-seller listings (member_ids) + single listing (post_ids) | ✅ server-side, top-level body fields |
| Price-range / media filters | ⚙️ applied client-side (not honored in the search body) |
| Full description / attributes / rich seller profile | ⚙️ scraped best-effort from the listing page |
| "No exact match" signal | ❌ none — OpenSooq returns loosely-related fallbacks; handled client-side (relevance_warning / strict_match) |
| Phone unmasking | ⛔ requires a logged-in account (anonymous token → 401); intentionally not built — see Limitations |
Tool reference
All 15 tools are read-only. query_ar (Arabic) is optional but strongly
recommended — the Kuwait catalog is predominantly Arabic, so Arabic terms
return far more results (e.g. Rolex: 36 → 1358). Most tools take
response_format: "markdown" | "json".
Search & discovery
| Tool | Key parameters | Returns |
|------|----------------|---------|
| opensooq_search_listings | query, query_ar, sort (relevant/newest/cheapest/priciest), category, city, page, limit, min_price, max_price, has_video, has_images, shops_only, verified_only, min_seller_rating, strict_match, response_format | A page of listings (+ total, + a relevance_warning if the page looks like a fallback). |
| opensooq_get_listing | listing_id, include_details (default true), response_format | One listing + (best-effort) full description, attributes, and rich seller profile. |
| opensooq_latest_listings | query, query_ar, category, city, limit | Newest-first listings — "what just got posted". |
| opensooq_category_breakdown | query, query_ar | Which categories a term appears in, with counts. |
Categories & cities
| Tool | Key parameters | Returns |
|------|----------------|---------|
| opensooq_list_categories | (none) | The top-level category taxonomy (id, EN/AR name, url slug). |
| opensooq_list_cities | (none) | The Kuwait cities/governorates usable as city. |
| opensooq_browse_category | category (required), query, city, sort, page, limit | Category-first browsing, optionally narrowed. |
Pricing & deals
| Tool | Key parameters | Returns |
|------|----------------|---------|
| opensooq_market_summary | query, query_ar, category, pages (1-5) | min/max/avg/median/p25/p75 + cheapest & priciest links. |
| opensooq_price_distribution | query, query_ar, category, pages, buckets (2-12) | A price histogram. |
| opensooq_find_deals | query, query_ar, category, pages, discount_pct (default 30), verified_only, limit | Listings ≥N% below the median (outlier-trimmed). |
| opensooq_compare_prices | queries (2-5 terms), pages | Side-by-side median/avg per term. |
Sellers & reputation
| Tool | Key parameters | Returns |
|------|----------------|---------|
| opensooq_top_sellers | query, query_ar, category, pages, limit | Sellers ranked by listing volume + rating. |
| opensooq_seller_listings | member_id (required), sort, page (1-20), limit | A seller's full inventory, paginated, with their total count. |
| opensooq_seller_profile | member_id (required), max_pages (1-5), include_sample | Reputation + inventory stats + category breakdown + scraped profile. No query needed. |
| opensooq_find_trusted_sellers | query, query_ar, category, pages, min_rating (default 4.0), require_shop | Verified / highly-rated sellers, to reduce scam risk. |
Use cases & applications
Ask it directly (in any connected client)
- "What's the going rate for a used PlayStation 5 on OpenSooq?" →
market_summary - "Find iPhone 15 Pro deals at least 40% below market from verified sellers." →
find_deals(verified-only) - "Show me the cheapest verified-shop iPhone 15 listings in Hawally." →
search_listings(sort=cheapest, shops_only, verified_only, city) - "Is 128 or 256 GB cheaper right now — iPhone 14 vs 15 vs 16?" →
compare_prices - "Who are the biggest Toyota Land Cruiser dealers, and which are reputable?" →
top_sellers+find_trusted_sellers - "Pull everything seller 42899294 is selling, newest first." →
seller_listings - "Give me the full details and seller history for listing 281910976." →
get_listing+seller_profile
Apps you can build on top
For buyers
- Deal-alert agent — schedule
find_deals(verified-only) over a watchlist and notify on a hit. (Pairs well with a scheduled/cron agent.) - "Fair price" assistant — paste any listing; it runs
market_summary+price_distributionand says whether it's over/under market and which percentile it sits in. - Negotiation helper —
compare_pricesacross variants + the cheapest verified comps, to back up a lowball offer with evidence.
For sellers / dealers
- Pricing copilot — before listing,
market_summary+top_sellersshow the going rate and what competitors charge, suggesting an optimal price. - Competitor monitor — track a rival's
seller_listings/seller_profileover time to watch their inventory and pricing moves.
For trust & safety
- Scam-risk scorer — combine
find_trusted_sellers, the relevance signal, and "too-good-to-be-true" deal detection to flag likely scams before a buyer makes contact.
For resale / arbitrage
- Cross-platform arbitrage scanner — compare OpenSooq P2P prices against retail or other marketplaces to surface buy-low/sell-high gaps.
- Depreciation dashboard — periodically snapshot
price_distribution/market_summaryfor popular models to chart how prices move over time.
Price-drop watcher (cron-ready)
The simplest "useful app" ships in this folder: watch.py — a tiny
script that alerts you when a listing drops below a target price. It reuses
the exact same relevance + price-band filtering as the MCP server (so it won't
ping you about a Galaxy A52s or a 10 KWD installment-teaser), and it remembers
what it has already seen so you only hear about new matches.
It needs only httpx (no MCP/Pydantic), so it runs on any Python ≥3.9 — or
zero-setup via uv run --directory /abs/path/opensooq-mcp watch.py ….
Try it once
# real Galaxy S26 Ultra under 320 KWD
python watch.py --query "samsung galaxy s26 ultra" \
--query-ar "سامسونج جالاكسي اس 26 الترا" --max-price 320
# iPhone 15 Pro under 150 KWD, verified sellers only, ping a Discord/Slack/Telegram webhook
python watch.py --query "iphone 15 pro" --query-ar "ايفون 15 برو" \
--max-price 150 --verified-only \
--webhook "https://discord.com/api/webhooks/XXX/YYY"
Sample output (first run):
[2026-06-13 10:25Z] 3 NEW 'samsung galaxy s26 ultra' listing(s) <= 320 KWD:
310 KWD | Samsung Galaxy S26 Ultra 256 GB in Kuwait City | Sharq, Kuwait City | Almohands. Shop 🏪 ★3.6(325) | https://kw.opensooq.com/search/282305058
310 KWD | SAMSUNG S26 ULTRA 256 gb | West Abdullah Al-Mubarak, Farwaniya | khaled ★3.7(3) | https://kw.opensooq.com/search/281723370
320 KWD | Samsung Galaxy S26 Ultra 512 GB in Hawally | Maidan Hawally, Hawally | Ahmed ★3.7(3) | https://kw.opensooq.com/search/282363402
Each run prints new hits, appends them to watch-<query>.log, optionally POSTs
to your webhook, and saves seen ids to watch-<query>.json. Flags: --city,
--shops-only, --verified-only, --pages, --state, --log, --webhook.
Schedule it
Linux/macOS (cron) — check every 15 minutes:
*/15 * * * * cd /abs/path/opensooq-mcp && /abs/path/.venv/bin/python watch.py \
--query "iphone 15 pro" --query-ar "ايفون 15 برو" --max-price 150 \
--webhook "https://discord.com/api/webhooks/XXX/YYY" >> watch.cron.log 2>&1
Or with uv (no venv needed): replace the python call with
uv run --directory /abs/path/opensooq-mcp watch.py ….
Windows (Task Scheduler) — register a task that runs every 15 minutes:
$py = "C:\path\opensooq-mcp\.venv\Scripts\python.exe"
$args = 'watch.py --query "iphone 15 pro" --query-ar "ايفون 15 برو" --max-price 150'
$act = New-ScheduledTaskAction -Execute $py -Argument $args -WorkingDirectory "C:\path\opensooq-mcp"
$trg = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes 15)
Register-ScheduledTask -TaskName "OpenSooq iPhone15Pro watch" -Action $act -Trigger $trg
Set PYTHONIOENCODING=utf-8 in the environment (cron line / task) so Arabic
titles print cleanly. The webhook payload works as-is with Discord (content)
and Slack/Telegram-style (text) incoming webhooks.
Data-quality handling
OpenSooq search results are noisy: accessories priced at ~1 KWD, troll listings at "100 billion", and loosely-related fallback results for weak queries (OpenSooq returns something rather than "no match"). The pricing, deals, and seller tools defend against this with:
- Query-relevance filtering — drop listings whose title/description don't
contain a query token. Search surfaces a
relevance_warningwhen a page looks like a fallback, andstrict_match: trueremoves that noise entirely. - A median price-band — when computing medians, distributions, deals, and seller stats, ignore listings below 20% or above 5× the median, so a 1 KWD phone case can't masquerade as a "99% off" iPhone.
These invariants are asserted by troubleshoot.py,
which exercises every tool across many detailed Arabic + English product names.
Limitations & responsible use
- Read-only, single-market. The server only reads public Kuwait (
kw) classifieds. It never posts, edits, or deletes, and currency is KWD. - Unofficial API. OpenSooq can change its endpoints or signing scheme at any time; the scraped "rich detail" extras are best-effort and degrade gracefully. Be a good citizen: keep request volume modest (the tools already cap pages 1-5) and don't hammer the API.
- Seller phone numbers stay masked. Each listing exposes a masked phone
(
555471XX) and a reveal key. Unmasking requires a logged-in OpenSooq account (the anonymous token gets a 401), so it is intentionally not implemented. Revealing a single number to contact a seller mirrors the site's "show phone" button and is legitimate; bulk-harvesting phone numbers is out of scope and not supported.
Development & testing
Layout:
install.py— the one-command installer: findsuv, provisions- verifies, and writes the config into every detected MCP client.
.python-version— pins Python3.12souvnever tries to download a newer one (the cause of the GitHub/DNS startup failures).opensooq_client.py— the OpenSooq API client (handshake, search,post_ids/member_idslookups, scraping, parsing). No MCP/Pydantic deps → unit-testable on any Python withhttpx.server.py— the MCP tool layer (FastMCP + Pydantic input models + formatting + data-quality logic).watch.py— the standalone, cron-ready price-drop watcher (see above); reuses the client's relevance + price-band filters, needs onlyhttpx.troubleshoot.py— a live diagnostic harness. It loads the tools via an AST shim (so it runs on Python 3.9 without themcpSDK) and exercises all 15 across detailed Arabic + English products, asserting sort/filter/relevance/deal invariants.
# with uv (run from this folder; .python-version pins 3.12):
PYTHONIOENCODING=utf-8 uv run --with httpx --with pydantic troubleshoot.py # ~40 live calls, pass/fail summary
# quick syntax check:
uv run --with httpx --with pydantic python -m py_compile server.py opensooq_client.py watch.py
(Or, with a Plan-B venv, swap uv run … for .venv/bin/python.)
Troubleshooting
| Symptom | Likely cause / fix |
|---------|--------------------|
| Failed to download …python-build-standalone… / dns error: No such host is known / calling "initialize": EOF | uv tried to fetch a newer Python from GitHub and the network/DNS is blocked. The bundled .python-version (3.12) prevents this — make sure your --directory points at this folder (which contains it), or re-run python install.py. |
| Client log: uv: command not found | GUI clients may not inherit your shell PATH. Use the absolute path to uv in command (which uv / where.exe uv) — install.py does this automatically. |
| could not attach to MCP server / server disconnected | A wrong --directory (it must point at the folder that contains server.py and .python-version), or the download error above. Re-run python install.py and check the client's MCP log. |
| First request hangs a few seconds | Normal — uv is provisioning Python + deps that one time. Instant afterward. |
| Client shows the server but no tools | Wrong --directory path (must point at the folder containing server.py). |
| Garbled/? Arabic text | Set "env": { "PYTHONIOENCODING": "utf-8" } in the MCP config. |
| Empty results for a real product | OpenSooq may have returned fallbacks — add query_ar (Arabic) and/or set strict_match: true. |
| Error: … 401 | A stale device registration; the client auto-retries once. If it persists, OpenSooq may have changed the signing scheme. |
| Error: … 429 | Rate-limited. Slow down / reduce pages. |
| Verify the server in isolation | npx @modelcontextprotocol/inspector uv run --directory /abs/path/opensooq-mcp server.py |