Skip to main content
Glama
ismaileneskucuk

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.