Groupon MCP Server
by javierlnmn
README.md
# Groupon MCP Server
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes a
Groupon‑style deals catalogue to LLM clients — both **consumer deal discovery**
and **merchant deal intelligence** — as MCP tools and resources, served over
Streamable HTTP behind OAuth 2.1.
## 1. What is this about
The server turns a deals dataset (deals, options, merchants, categories,
locations, reviews) into capabilities an MCP‑aware model can call:
- **Customers** discover deals — search with filters, inspect a deal, compare
several side by side.
- **Merchants** get everything customers can, plus **intelligence** — category
benchmarking, market‑gap analysis, and price positioning against peers.
A caller's **role is fixed by who they log in as**. The role is read from the
OAuth access token at session start, so a session only ever sees the tools its
role is allowed to use — customers never even see the merchant tools in
`tools/list`.
## 2. Approach
**Layered by concern**, one organizing principle throughout `src/`:
| Folder | Responsibility |
| ---------------- | ------------------------------------------------------------------------------ |
| `models/` | Zod domain models — the validated shapes everything else speaks in |
| `repositories/` | **All** SQL lives here; repositories return Zod‑validated domain models |
| `db/` | Infrastructure — in‑memory SQLite client, schema, fixtures, seed |
| `mcp/` | The MCP delivery layer (see below) |
| `utils/` | Stateless helpers (console output, password hashing) |
Inside `mcp/`:
- `app.ts` — builds the Express host: OAuth routes, login, the Bearer‑guarded `/mcp` endpoint.
- `server.ts` — `buildMcpServer(role)` registers the tools/resources the role may use.
- `controllers.ts` — Streamable HTTP transport + per‑session lifecycle.
- `auth-provider.ts` — the OAuth 2.1 authorization‑server logic.
- `tools.ts` — the tool‑name registry and role rules; `results.ts` — tool‑result builders.
**Key design choices**
- **Streamable HTTP as transport.** The features offered by this MCP server
may be consumed by external applications or sevices. That is why, rather than
sticking to `StdioServerTransport` for development purposes, I chose
`StreamableHTTPServerTransport` to offer a more production-like implementation.
- **SQLite, on purpose.** An in‑memory SQLite database keeps the project
dependency‑free and trivial to run, so the focus stays on the part that
matters — the MCP tools and the deal‑intelligence logic — instead of standing
up external infrastructure. It still exercises a *real, structured* data layer,
so the architecture is representative rather than a throwaway in‑memory array.
That database is rebuilt + seeded on every launch — great for a deterministic demo,
but note every restart invalidates issued OAuth tokens (you re‑authenticate
after a restart)
- **Per‑session, role‑scoped servers.** Each session gets a fresh MCP server;
the role comes from the access token, so the tool set is decided once and
fixed for the session. Merchants are a superset of customers.
- **Repositories own persistence.** Tools never touch SQL — they call a
repository that returns a fully‑assembled, validated domain model.
- **Full‑text search.** `search_deals` keyword matching uses SQLite **FTS5**
(relevance‑ranked), not `LIKE`.
- **Self‑contained auth.** The server doubles as its own OAuth 2.1 authorization
server (PKCE), verifying credentials against the seeded `users` table; the
user's role is carried on the token.
**Tech stack:** TypeScript · Express 5 · `@modelcontextprotocol/sdk` ·
`better-sqlite3` (in‑memory) · Zod.
## 3. MCP tools
### Tools
| Tool | Role | Input | Returns |
| -------------------- | --------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `search_deals` | customer¹ | `query?`, `category?`, `location?`, `max_price?`, `min_discount?`, `limit?` | Active deals matching the filters (FTS keyword + slug/price/discount). |
| `get_deal` | customer¹ | `deal_id` | Full detail for one deal: options, merchant, location, rating, fine print. |
| `compare_deals` | customer¹ | `deal_ids` (2+) | Side‑by‑side: full deals, an aligned summary row each, and cheapest / biggest‑discount / highest‑rated picks. |
| `list_all_deals` | **merchant** | — | Every deal including inactive ones (management view). |
| `category_insights` | **merchant** | `category`, `location?` | Deal count, avg/median discount, price range, top merchants. |
| `find_market_gaps` | **merchant** | `location` | Categories under‑supplied in that location vs other markets, ranked by opportunity (`gap_score`). |
| `price_positioning` | **merchant** | `deal_id` | A deal's headline price/discount vs its category peers (peer medians + share it beats). |
¹ Customer tools are also available to merchants.
### Resources
Reference data for grounding the `category` / `location` filter values:
| Resource | URI | Contents |
| ------------------ | ------------------ | --------------------------------- |
| Categories | `categories://all` | Every category (slug + name). |
| Locations | `locations://all` | Every location (slug + region). |
`category` / `location` filters take **slugs** — e.g. categories
`restaurants`, `wellness-beauty`, `fitness`; locations `madrid`, `barcelona`,
`valencia`.
## 4. How to run
**Prerequisites:** Node.js 24 and npm. (`better-sqlite3` is a native module — if
you switch Node versions, run `npm rebuild better-sqlite3`.)
```bash
npm install
# Development (no build step):
npm run dev # tsx src/index.ts
# Production build:
npm run build # tsc && tsc-alias → dist/
npm start # node dist/index.js
```
The server listens on **http://localhost:3000**:
- MCP endpoint — `http://localhost:3000/mcp`
- Health check — `http://localhost:3000/health`
### Trying it with the MCP Inspector
```bash
npx @modelcontextprotocol/inspector
```
In the Inspector UI:
1. **Transport Type:** `Streamable HTTP`
2. **URL:** `http://localhost:3000/mcp`
3. Click **Connect** → run the **Guided OAuth flow** and log in with a seeded user.
The `/mcp` endpoint is OAuth‑protected, so you log in to get a token; the role
you log in as determines which tools you see.
**Seeded logins** (all use password `password123`):
| Role | Email | Sees |
| -------- | -------------------- | ----------------------------------------------------- |
| merchant | `javier@example.com` | Everything — all 7 tools + both resources |
| customer | `laura@example.com` | Discovery only — `search_deals`, `get_deal`, `compare_deals` + resources |
> Because the database is in‑memory, **restarting the server invalidates all
> tokens** — clear the Inspector's stored auth and log in again after a restart.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues