Electronic Markets TR
README.md
# Electronic Markets TR — MCP Server (Rust)
A lightweight [Model Context Protocol](https://modelcontextprotocol.io) server that searches
Turkish electronics e-commerce sites in parallel and returns normalized product, price (TRY)
and stock data for AI agents.
## Purpose
Give LLM agents one tool to compare prices and stock across Turkish electronics markets,
without loading pages or scraping HTML themselves. The server returns raw page-one search
hits; ranking and relevance judgment stay with the agent.
## Supported markets
| Market | Method |
| :--- | :--- |
| Robotistan | Server-rendered search page (`/arama?q=`) |
| Robocombo | Ticimax storefront JSON API |
| Direnc.net | T-Soft v5 search page (`data-toggle` attributes) |
| Robo90 | T-Soft AJAX product loader |
| Ceriyan | WooCommerce product search (`/?s=&post_type=product`) |
## Architecture
```
src/
domain/ Product, Price/Money (kurus-backed integer), StockStatus, Market, SearchQuery
parsing.rs Turkish price/stock text parsing (pure functions, no I/O)
http.rs Shared reqwest client: pooling, timeouts, bounded retry
scrapers/ One independent module per market (fetch + site-specific parser)
search.rs Concurrent orchestration: all 5 markets in parallel, per-market timeout,
partial results on failure
pipeline.rs Stock filter + URL-canonicalization dedup (pure functions)
mcp_service.rs rmcp tool server + shared search execution
main.rs stdio entry point
```
Design notes:
- **Money**: `kurus` (1/100 TRY) stored as `i64`; no floating point anywhere near prices.
A missing price is `Price::Unavailable`, distinct from zero.
- **Concurrency**: all markets are queried concurrently (bounded by the static market
count). A 20 s per-market timeout turns a slow site into a per-market error without
affecting other markets.
- **Errors**: typed (`HttpError`, `ScrapeError`); market failures are reported in the
response payload (`marketErrors`) instead of failing the whole search.
## Setup
Requires Rust 1.85+ (edition 2024).
```bash
cargo build --release
```
## Configuration
Environment variables:
- `RUST_LOG` — log filter (default: no logs on stdout; logs go to stderr, e.g. `RUST_LOG=warn`)
HTTP behavior (timeouts, retries, pool size) is configured in
`src/http.rs::HttpClientConfig` with sensible defaults: 5 s connect timeout, 15 s request
timeout, 1 retry for transient failures (5xx/timeout/network) only.
## MCP usage
Register in an MCP client (e.g. Claude Desktop):
```json
{
"mcpServers": {
"electronic-markets-tr": {
"command": "/absolute/path/to/mcp-electronicmarkets-tr/target/release/mcp-electronicmarkets-tr"
}
}
}
```
Tool: `search_products`
| Argument | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `query` | string | — | Product name or keywords (1–128 chars) |
| `only_in_stock` | boolean | `true` | Return only products currently in stock |
Response (JSON):
```json
{
"query": "esp32",
"onlyInStock": true,
"resultCount": 5,
"products": [
{
"market": "robotistan",
"name": "ESP32-CAM ...",
"price": { "available": { "kurus": 92320 } },
"stockStatus": "in_stock",
"url": "https://www.robotistan.com/..."
}
],
"marketErrors": []
}
```
## Development
```bash
cargo check # fast type check
cargo test # unit + fixture + MCP protocol tests (offline)
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
```
## Testing
- **Unit tests** (`src/`): Turkish price parsing, stock text parsing, URL building,
filtering, deduplication, query validation.
- **Fixture tests** (`tests/scraper_fixtures.rs`, `tests/robocombo_fixture.rs`): real
HTML/JSON captured from the live sites; parser output verified against live-observed
counts (e.g. Ceriyan `esp32` -> 10 products, 5 in stock / 5 out of stock). Includes
malformed-HTML and missing-field safety tests.
- **MCP protocol tests** (`tests/mcp_protocol.rs`): full rmcp handshake over an in-memory
duplex transport — tool registration, input schema shape, invalid-parameter rejection.
## Live smoke testing
Hits the real sites; ignored by default so `cargo test` stays offline:
```bash
cargo test --test live_smoke -- --ignored --nocapture
```
Verifies all five markets with three queries: `arduino uno`, `esp32`, `220 ohm resistor`.
Each market is reported independently; one failing market does not hide others' results.
Performance comparison (concurrent vs sequential search) can be run with:
```bash
cargo run --release --example perf_search
```
Measured on a residential connection (2026-09-10): concurrent 2.4 s vs sequential
7.4 s for `esp32` (~3.1x speedup); results vary with per-site latency. Working set
during a search: ~14 MB.
## Release build
```bash
cargo build --release
# binary: target/release/mcp-electronicmarkets-tr
```
## Adding a new scraper
1. Add a variant to `Market` (`src/domain/market.rs`) and include it in `Market::ALL`.
2. Create `src/scrapers/<market>.rs` with `pub async fn search(http: &HttpClient, query: &SearchQuery)`
plus a pure `pub fn parse_search_page(&str) -> Vec<Product>`.
3. Wire the variant in `search.rs::scrape_market`.
4. Capture a real fixture (e.g. `curl -A "Mozilla/..." <search-url> > tests/fixtures/<market>_search.html`)
and add a fixture test.
5. Verify with live smoke tests before shipping.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues