Offer Discovery MCP Server
README.md
# Offer Discovery MCP Server
> A **Model Context Protocol (MCP) server** that connects AI agents to a live offers API and returns structured product offers in real time.
---
## Table of Contents
- [What is MCP?](#what-is-mcp)
- [Architecture Overview](#architecture-overview)
- [Project Structure](#project-structure)
- [Data Flow](#data-flow)
- [Transport Modes](#transport-modes)
- [Getting Started](#getting-started)
- [Available Scripts](#available-scripts)
- [Environment Variables](#environment-variables)
---
## What is MCP?
**Model Context Protocol (MCP)** is an open standard that lets AI models (like ChatGPT) call external tools and fetch live data — designed specifically for LLM tool use.
```
ChatGPT ──────── MCP Protocol ────────► MCP Server ────► Offers API
"call get_offers tool" (this repo) (live, 520+ offers)
◄────────────────────────────── ◄──────────
Returns structured JSON
```
When a user asks ChatGPT *"What home improvement deals are available?"*, ChatGPT automatically:
1. Recognizes it needs real data
2. Calls our `get_offers` tool via MCP
3. Receives a structured JSON list of **live offers**
4. Summarizes and presents them to the user
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ │
│ ChatGPT / AI Agent / MCP Inspector │
│ (Sends JSON-RPC tool call requests) │
└───────────────────────────┬─────────────────────────────────────────┘
│ MCP Protocol (JSON-RPC 2.0)
│
┌─────────────▼──────────────┐
│ TRANSPORT LAYER │
│ │
│ stdio (local/dev) │ ← src/index.ts
│ HTTP + SSE (remote/ngrok) │ ← src/server.ts
└─────────────┬──────────────┘
│
┌─────────────▼──────────────────────────┐
│ McpServer (SDK v1.x high-level API) │
│ │
│ registerTool("get_offers", { │
│ inputSchema: GetOffersInputZodShape, │ ← offerSchema.ts
│ description: "...", │
│ }, handler) │
│ │
│ • Serves tools/list automatically │
│ • Validates args via Zod automatically │
│ • Routes tools/call to handler │
└─────────────┬───────────────────────────┘
│ pre-validated GetOffersInput
┌─────────────▼──────────────┐
│ API CLIENT LAYER │
│ │
│ fetchOffers() │ ← src/api/offersClient.ts
│ Live API + mock fallback │
└─────────────┬──────────────┘
│ axios.get()
┌─────────────▼──────────────┐
│ OFFERS API │
│ api.example.com/offers │
│ /offers?campaignMappingId │
│ =ALL (live offers) │
└────────────────────────────┘
```
---
## Project Structure
```
offer-discovery-mcp/
│
├── src/
│ ├── index.ts # Entry point: stdio transport (local dev & MCP Inspector)
│ ├── server.ts # Entry point: HTTP/SSE transport (ngrok & remote clients)
│ │
│ ├── schemas/
│ │ └── offerSchema.ts # Zod schemas: input args + offer output shape
│ │
│ ├── api/
│ │ └── offersClient.ts # Live API client: calls the offers API, falls back to mock
│ │
│ └── tools/
│ └── getOffers.ts # Tool handler: validate → fetch → filter → format → respond
│
├── package.json # Dependencies + npm scripts
├── tsconfig.json # TypeScript: ES2022, NodeNext, strict mode
├── .gitignore
├── README.md # ← You are here
└── TESTING.md # Step-by-step testing guide
```
---
## Data Flow
Exact journey of a single tool call from ChatGPT to a response:
```
1. ChatGPT sends:
{ "method": "tools/call", "params": { "name": "get_offers", "arguments": { "category": "furniture", "featured": true } } }
2. src/index.ts (or server.ts) — McpServer receives the tool call
└── SDK validates args against GetOffersInputZodShape (Zod)
├── FAIL → SDK returns validation error to ChatGPT (handler not called)
└── PASS → calls the registered handler with typed GetOffersInput args
3. src/tools/getOffers.ts :: handleGetOffers(args: GetOffersInput)
└── calls fetchOffers(args)
4. src/api/offersClient.ts :: fetchOffers()
├── axios.get("https://api.example.com/offers?campaignMappingId=ALL")
│ ├── SUCCESS → live offers returned
│ └── FAIL → falls back to MOCK_OFFERS (server stays functional)
└── Applies in-process filters:
industry → category (legacy) → offerType → region → network → brand → featured → pagination
└── Returns: Offer[]
5. src/tools/getOffers.ts :: formatOfferForChatGPT()
└── Strips raw image URLs + internal IDs
└── Surfaces: brand, offerType, links, keywords, expiryMsg, disclosure
└── Wraps in envelope: { totalOffers, appliedFilters, offers: [...] }
6. ChatGPT receives the JSON and presents live offers to the user.
```
---
## Transport Modes
| Mode | File | Command | Use When |
|------|------|---------|----------|
| **stdio** | `src/index.ts` | `npm run dev` | Local MCP Inspector, Claude Desktop |
| **HTTP/SSE** | `src/server.ts` | `npm run dev:http` | Remote access via ngrok, ChatGPT Agents SDK |
### Endpoints (HTTP mode)
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `POST /mcp` | Streamable HTTP | OpenAI Responses API (recommended) |
| `GET /mcp` | Streamable HTTP | SSE streaming for long responses |
| `GET /sse` | SSE (legacy) | MCP Inspector |
| `POST /messages` | SSE (legacy) | MCP Inspector message routing |
| `GET /health` | — | Health check |
---
## OpenAI Integration
> **Source:** [OpenAI Apps SDK — Build your MCP server](https://developers.openai.com/apps-sdk/build/mcp-server) · [MCP concept overview](https://developers.openai.com/apps-sdk/concepts/mcp-server/)
### Recommended Transport: Streamable HTTP
Per official OpenAI docs, **Streamable HTTP is the recommended transport** for production.
| Transport | Status | Use When |
|-----------|--------|----------|
| `stdio` | ✅ Active | Local MCP Inspector, Claude Desktop |
| `SSE` | ⚠️ Legacy | Remote testing with MCP Inspector |
| `Streamable HTTP` | ✅ Recommended | Production (ChatGPT, OpenAI Responses API) |
Both transports are implemented in this project. `POST /mcp` uses Streamable HTTP; `GET /sse` uses legacy SSE.
### Tool Annotations (Required for ChatGPT App Store)
```typescript
server.registerTool("get_offers", {
description: "...",
inputSchema: GetOffersInputZodShape,
annotations: {
readOnlyHint: true, // ✅ reads data only, never writes
openWorldHint: false, // ✅ scoped to the offers domain only
destructiveHint: false, // ✅ no deletes or irreversible actions
},
}, handler);
```
### Official References
| Resource | Link |
|----------|------|
| OpenAI Apps SDK: Build MCP server | [developers.openai.com/apps-sdk/build/mcp-server](https://developers.openai.com/apps-sdk/build/mcp-server) |
| MCP concept overview | [developers.openai.com/apps-sdk/concepts/mcp-server](https://developers.openai.com/apps-sdk/concepts/mcp-server/) |
| TypeScript SDK | [github.com/modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) |
| MCP Specification | [spec.modelcontextprotocol.io](https://spec.modelcontextprotocol.io) |
| MCP Inspector | [modelcontextprotocol.io/docs/tools/inspector](https://modelcontextprotocol.io/docs/tools/inspector) |
---
## Getting Started
### Prerequisites
- Node.js v18+
- npm v9+
- [ngrok](https://ngrok.com) (only for remote/HTTP mode)
### Installation
```bash
git clone https://github.com/siddharthkoundal/chatgpt-marketplace-app.git
cd offer-discovery-mcp
npm install
```
### Running Locally (stdio — for MCP Inspector)
```bash
npm run dev
```
### Running for Remote Access (HTTP — for ChatGPT / ngrok)
```bash
# Terminal 1: Start HTTP server
npm run dev:http
# → 🚀 offer-discovery-mcp v1.0.0 running on port 3000
# → [offer-discovery-mcp] Offers API working! returned live offers.
# Terminal 2: Expose via ngrok
ngrok http 3000
# → Forwarding: https://abc123.ngrok-free.app → localhost:3000
```
See [TESTING.md](./TESTING.md) for detailed testing steps.
---
## Available Scripts
| Command | Description |
|---------|-------------|
| `npm run dev` | Start server with **stdio** transport (local MCP Inspector) |
| `npm run dev:http` | Start server with **HTTP/SSE** transport (ngrok / remote) |
| `npm run build` | Compile TypeScript to `dist/` |
| `npm start` | Run compiled JS from `dist/` |
---
## Environment Variables
Create a `.env` file in the project root (already listed in `.gitignore` — never commit it):
```env
# Offers API
OFFERS_API_URL=https://api.example.com/offers
OFFERS_API_KEY=your-api-key-here
# Server
PORT=3000
```
`tsx` (used by `npm run dev` and `npm run dev:http`) loads `.env` automatically — no extra packages needed.
> If `OFFERS_API_KEY` is missing or empty, the server falls back to the `MOCK_OFFERS` dataset automatically.
TDQS
A4/5.0
Scored across 1 tool
Disambiguation5/5
With only one tool, there is no possibility of ambiguity between tools. The tool's purpose is clearly defined.
Naming Consistency5/5
The single tool uses a clear verb_noun pattern (get_offers), which is consistent with common MCP conventions. No naming conflicts exist.
Tool Count3/5
A single tool feels thin for a server named 'Offer Discovery MCP Server', which might reasonably include multiple tools (e.g., get_offer_details, search_offers). However, as a prototype, it is borderline acceptable.
Completeness2/5
The server only provides a single fetch operation with filters, lacking essential discovery features such as retrieving a specific offer by ID, pagination, or detail views. This leaves significant gaps for typical offer discovery workflows.
Maintenance
ActivityStale
ResponsivenessNo issues