Skip to main content
Glama
manamurah

ManaMurah MCP Server

by manamurah
README.md
# manamurah-mcp-server

Remote [Model Context Protocol](https://modelcontextprotocol.io) (MCP)
server for Malaysian PriceCatcher consumer price data. Deployed on
Cloudflare Workers. 15 strongly-typed tools for AI agents.

**Live endpoint:** `https://mcp.manamurah.com/mcp` (POST, JSON-RPC 2.0)

**Data source:** [data.gov.my](https://data.gov.my) PriceCatcher —
~3,800 premises × ~756 items across all 16 Malaysian states/territories,
collected daily and published as weekly averages. 100% public government
data. No credentials needed anywhere in the stack.

---

## Install

The server speaks streamable HTTP at `https://mcp.manamurah.com/mcp`.
Pick the block for your client:

### Claude Desktop

Add to `~/Library/Application Support/Claude/claude_desktop_config.json`
on macOS (or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):

```json
{
  "mcpServers": {
    "manamurah": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.manamurah.com/mcp"]
    }
  }
}
```

Claude Desktop only speaks stdio, so the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
shim is the bridge. Restart the app — you'll see the 15 tools under
the 🔌 icon. Ask *"harga tembikai di Selangor?"* or *"what's the
cheapest chicken in KL this week?"* and Claude will chain
`search_items` → `find_cheapest` automatically.

### Claude Code (CLI)

One command, native streamable HTTP — no shim:

```bash
claude mcp add manamurah --transport http https://mcp.manamurah.com/mcp
```

Confirm with `claude mcp list`. Tools become available the next time
you start a Claude Code session. Add `--scope user` to make it
available across every project.

### Cursor

User-level (every project): create or edit `~/.cursor/mcp.json`.
Project-level: `<project-root>/.cursor/mcp.json`.

```json
{
  "mcpServers": {
    "manamurah": {
      "url": "https://mcp.manamurah.com/mcp"
    }
  }
}
```

Cursor 0.46+ supports remote MCP via `url` directly. Reload the
window (`Cmd-Shift-P` → *Developer: Reload Window*) and the
`manamurah` row appears under Settings → MCP with all 15 tools
listed.

### Cline (VS Code extension)

Open Cline's sidebar → ⚙️ → **MCP Servers** → **Add Server** →
select **Remote** → paste `https://mcp.manamurah.com/mcp` → name it
`manamurah`. Cline writes the config to its VS Code global storage
itself; no JSON editing needed.

For headless setup, the same JSON shape Claude Desktop uses also
works in Cline's `cline_mcp_settings.json`.

### ChatGPT (Custom Connectors)

For ChatGPT Pro / Team / Enterprise: **chatgpt.com/connectors** →
**Add custom connector** → MCP server URL
`https://mcp.manamurah.com/mcp` → no auth required (public read-only).
After approval the connector shows up in the message-input ⊕ menu.

For OpenAI Apps SDK / programmatic use (the `codex_apps.manamurah_*`
tool family in Codex): the server URL field in the app manifest at
**platform.openai.com/apps** must be exactly
`https://mcp.manamurah.com/mcp` — *not* `https://manamurah.com` (which
will fail the streamable transport probe with HTTP 403, since that's
the consumer site, not an MCP endpoint).

### Other MCP clients

Any client that supports streamable-HTTP MCP transport points
directly at `https://mcp.manamurah.com/mcp`. Stdio-only clients shim
via [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) using
the same recipe as Claude Desktop above.

Quick smoke test from any shell:

```bash
curl -sS -X POST https://mcp.manamurah.com/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'
```

You should see fourteen tool names — eleven KPDN PriceCatcher tools
(weekly cadence) plus three FAMA Panduan Harga Harian tools (daily
cadence, three price levels). The richer
[self-describing manifest](https://mcp.manamurah.com/) (full input
schemas + versioning policy) is also a single GET away.

## Tools

All 15 tools are read-only, public, and capped on response size to
keep LLM contexts compact. Every tool carries a detailed description
+ JSON-Schema input so the LLM picks the right one without guessing.

### KPDN PriceCatcher (weekly averages)

Retail prices only. ~3,800 premises × ~756 items, collected daily and
published as weekly averages from the
[data.gov.my PriceCatcher dataset](https://data.gov.my).

| Tool                | Purpose                                                    |
| ------------------- | ---------------------------------------------------------- |
| `search_items`      | Find items by name in Malay / English / Chinese / Tamil    |
| `find_cheapest`     | Up to 10 cheapest premises for one item this week          |
| `price_history`     | Weekly price trend at national/state/district/chain/urban  |
| `nearby_premises`   | Premises within a geographic radius of a coordinate        |
| `compare_prices`    | One item's price across national + state + district + more |
| `list_chains`       | Directory of retail chains + premise counts + coverage     |
| `price_change`      | Delta between current week and 1 / 3 / 6 / 12 months ago   |
| `top_movers`        | Biggest weekly risers + fallers                            |
| `category_trends`   | Per-category movement over a window                        |
| `basket_watch`      | Total cost for a 1–20 item basket over time                |
| `region_gap`        | Semenanjung vs Borneo top-N price gap, both directions     |

### FAMA daily prices (Panduan Harga Harian)

Three price levels — **RUNCIT** (retail), **BORONG** (wholesale),
**LADANG** (farm-gate) — tracked daily and independently. ~46 items
(fresh produce, eggs, poultry). FAMA's catalogue is independent of
PriceCatcher: `item_id` here is FAMA's own 1..46, not the KPDN
`item_code`.

| Tool                | Purpose                                                    |
| ------------------- | ---------------------------------------------------------- |
| `fama_price_history`| Daily series for one item at chosen level + geographic grain |
| `fama_margin`       | LADANG → BORONG → RUNCIT value-chain spread per day        |
| `fama_top_movers`   | Daily movers per level, anchored on latest index date      |

`fama_margin` and `fama_top_movers` reject `grain='daerah'` because
LADANG/BORONG coverage at daerah grain is too sparse for an honest
cut. `fama_price_history` accepts daerah for retail-only callers.

All tools require zero setup beyond the MCP config above. No API
keys. No rate limiting on the client side (upstream has a 12h KV
edge cache so repeat queries are essentially free).

## Example conversation

> **You:** Berapa harga tembikai merah tanpa biji di Selangor minggu ini?
>
> **Claude** *(calls `search_items(query="tembikai")` → gets item_code 21
> → calls `find_cheapest(item_code=21, state="Selangor")`)*:
>
> Tembikai merah tanpa biji di Selangor untuk minggu 2026-04-20:
> hanya ada 1 kedai yang melaporkan data PriceCatcher —
> **Jaya Grocer D'Pulze Cyberjaya** di daerah Sepang pada **RM 5.20/kg**.
> Data lain di KL menunjukkan NSK Seputeh/Cheras/Kepong menjual tembikai
> serupa pada RM 1.69/kg — mungkin berbaloi keluar PJ/KL jika anda di
> Selangor Selatan.

The structured response the LLM worked from:

```json
{
  "item_code": 21,
  "item_name": "TEMBIKAI MERAH TANPA BIJI",
  "weekdate": "2026-04-20",
  "results": [
    {
      "weekdate": "2026-04-20",
      "price": 5.20,
      "premise_name": "JAYA GROCER D`PULZE CYBERJAYA",
      "chain": "JAYA GROCER",
      "chain_type": "SUPERMARKET",
      "state": "Selangor",
      "district": "Sepang"
    }
  ],
  "scope": { "state": "Selangor", "district": null, "chain": null, "chain_type": null },
  "status": "ok",
  "reason": null,
  "warnings": []
}
```

## Architecture

```
 Claude Desktop ─ stdio ─ mcp-remote ─ HTTPS ─ mcp.manamurah.com
                                                    │
                                               (Cloudflare Worker, this repo)
                                                    │
                                                  fetch
                                                    ▼
                                        manamurah.com/api/v2/mcp/*
                                                    │
                                               (SvelteKit site +
                                                Cloudflare KV edge cache,
                                                12h TTL)
                                                    │
                                                    ▼
                                            public price data
```

This Worker is a thin protocol shim — it speaks MCP JSON-RPC on one
side and standard `fetch()` on the other. No business logic, no
credentials, no database binding. Every tool call goes to exactly
one upstream `/api/v2/mcp/<tool>` endpoint and passes the JSON
response through.

Keeping the shim thin means:
- The ~400 lines here are almost all tool-description JSON.
- Query/caching/index logic lives upstream where it's co-located
  with the data layer.
- Updating a tool's behaviour rarely requires a Worker redeploy.

## Self-host / fork

```bash
git clone https://github.com/manamurah/mcp-server
cd mcp-server
pnpm install

# Local dev — hot-reloads on src/ changes, binds to 127.0.0.1:8787
pnpm dev

# Exercise it:
curl -sS -X POST http://127.0.0.1:8787/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'

# Deploy to your own Cloudflare account:
pnpm deploy
```

To point at a staging copy of the upstream (useful when
[manamurah_20240322](https://gitlab.com/agagroup/apps/manamurah_20240322)
ships new `/api/v2/mcp/*` endpoints), override the base URL in
`wrangler.toml`:

```toml
[vars]
MANAMURAH_API_BASE = "https://staging.manamurah.com"
```

## Protocol notes

- **Transport:** HTTP POST at `/mcp`, JSON-RPC 2.0. CORS-open so
  browser-based MCP clients work without a proxy.
- **Methods supported:** `initialize`, `tools/list`, `tools/call`,
  `prompts/list`, `prompts/get`, `resources/list`, `resources/read`,
  `resources/templates/list`, `completion/complete`, `ping`.
- **Tool response shape:** MCP `content: [{ type: "text", text: ... }]`
  blocks with the raw JSON payload also attached as
  `structuredContent` for clients that prefer it.
- **Error handling:** business-level issues (invalid args, no data
  for this week) come back as `{ "status": "no_data", "reason": ... }`
  at HTTP 200 so the LLM can narrate them. Only transport/infra
  failures raise JSON-RPC errors.
- **Upstream errors:** 5xx on `/api/v2/mcp/*` bubbles up as JSON-RPC
  `code: -32603` with the upstream status in `data`. 4xx passes
  through unchanged (client validation error already shaped as a
  JSON envelope on the upstream side).

## Related

- **[manamurah-mcp-2026](https://gitlab.com/agagroup/data/manamurah-mcp-2026)**
  — Python stdio MCP server covering the same 15 tools. Use this
  instead if you want a local-only MCP binary (no Cloudflare, no
  Worker hop).
- **[manamurah_20240322](https://gitlab.com/agagroup/apps/manamurah_20240322)**
  — the SvelteKit site at manamurah.com. Hosts both the public
  website and the `/api/v2/mcp/*` proxy endpoints this Worker calls.
- **[manamurah-data-2026](https://gitlab.com/agagroup/data/manamurah-data-2026)**
  — the Python ETL that populates the indices behind everything
  else. Nightly pipeline, data.gov.my PriceCatcher source of truth.

## License

MIT. See [LICENSE](LICENSE).