---
name: gentic-reviews
description: "Give your AI agent your product reviews. Pull Junip reviews newest-first with incremental-since polling, rating and media filters, and cursor pagination — all through the Model Context Protocol. Pay per call, no subscriptions."
license: MIT
metadata:
  author: gentic
  version: "1.0.0"
---

# Gentic Reviews

Connect any AI agent to your product reviews. Pull your latest Junip reviews — newest-first, incremental since last check — through the Model Context Protocol. Filter by rating, media, or product, and poll for new reviews on a schedule without re-reading the whole backlog.

## When to apply

- User wants to read their latest product or store reviews.
- User wants to poll for new reviews on a schedule without re-reading old ones.
- User wants to filter reviews by an exact star rating (1–5).
- User wants only reviews that include photos or videos.
- User wants reviews for specific products (by store-platform product IDs).
- User wants to feed recent reviews into sentiment analysis, support triage, or ad copy.

## Tools

| Tool | Description | Cost |
|------|-------------|------|
| `import_amazon_reviews_csv` | Import a scraped Amazon customer-reviews CSV export into this organization's vendor-neutral `reviews` table (the same table junip_get_reviews / okendo_get_reviews write to; `source='amazon'`, `review_type='product'`). Use this for INDIVIDUAL Amazon reviews (reviewer, title, body, star rating, date) — these come from scraped CSV exports because Amazon's SP-API does not expose individual reviews. (The Amazon MCP's sync_amazon_reviews is different: it stores Amazon's AGGREGATED review-topic insights in a separate `amazon_review_topics` table.) Provide the CSV via EITHER `csv_url` (an https URL — a presigned-S3 link, a public Google Sheets URL, or a Google Drive share link; auto-converted to the direct CSV and fetched server-side) OR inline `csv_content` — exactly one. Expected columns (case-insensitive, extra columns preserved verbatim in `raw`): Date, Date Only, Reviewer Name, Verified, Title, Body, Rating, URL, Review ID, Images, Videos, Product Code, Variation / ASIN, Style. Optionally add a `Product Title` (or `Product Name`) column — it's mapped to `product_title` and folded into the embedding so reviews can be found by product name (not just ASIN). Each review is upserted deduped on its Amazon Review ID (re-importing the same export is idempotent; mutable fields refresh) and its title+body embedded so it's searchable via `search_reviews` and aggregatable with `query_data` over the `reviews` table. Returns `{ imported, skipped, total, latest_created_at, persisted, embedded, source, table }`. Billed 1¢ per review imported (covers the per-row embedding). | 1¢ / result (min 1¢) |
| `junip_get_reviews` | Fetch reviews from Junip (v1 Display API), newest-first, with cursor pagination and incremental-since support. Sources the Junip store key from the org's connected integration; pass `store_key` only to override. Every fetched review is also upserted into the org's `reviews` table in MotherDuck (deduped on the Junip review id; mutable fields like response/votes/featured/rating are refreshed on re-pull), and the tool returns the persisted rows for this pull — including `review_created_at` / `review_updated_at` and `ingested_at` — newest-first. For scheduled polling, persist the returned `latest_id` and pass it back as `since_id` on the next run to fetch only newer reviews (Junip has NO date-range filter, so this high-water-mark pattern is the supported way to get 'reviews since last check'). For manual paging, pass the returned `next_cursor` back as `page_after`. Respects Junip's 2 req/sec rate limit. Returns `{ reviews, count, latest_id, next_cursor, has_more, persisted, table }`; if the MotherDuck write fails the call still returns the freshly-fetched reviews with `persisted: false`. $0.05 per call. | 5¢ / call |
| `okendo_get_reviews` | Fetch product & store reviews from Okendo into this organization's vendor-neutral `reviews` table (the same table junip_get_reviews writes to; `source='okendo'`). Uses the org's connected Okendo integration (dashboard → Integrations; the Okendo User ID is configured there once, like an API key — it is NOT a tool input). Walks NEWEST-FIRST with cursor pagination — ONE walk covers both: incremental polling (pass the previous run's `latest_created_at` as `since` to fetch only newer reviews — Okendo has NO date-range filter, so this high-water-mark pattern is the supported 'reviews since last check') and full historical backfill (omit `since`, then keep passing the returned `next_cursor` back as `cursor` until `has_more` is false). Optionally restrict to one product via `product_id` (numeric Shopify product id). Each review is upserted deduped on the Okendo reviewId (mutable fields refreshed on re-pull) and its text embedded so it's searchable via `search_reviews`; aggregate with `query_data` over the `reviews` table. Returns `{ reviews, count, latest_created_at, next_cursor, has_more, persisted, embedded, table }`; if the DB write fails the call still returns the freshly-fetched reviews with `persisted:false`. $0.05 per call. | 5¢ / call |
| `search_reviews` | Search this organization's product & store reviews by natural language — the right tool for 'what do customers love about <product>?', 'what are people unhappy about?', 'find reviews mentioning battery life'. Searches the `reviews` table (populated by okendo_get_reviews / junip_get_reviews, across all connected review sources) semantically over each review's product+title+body text, returning full review rows ranked by relevance. Supports an optional SQL `filters` clause over the structured columns to narrow results — e.g. rating, source, product_title, review_created_at (TIMESTAMP), verified_buyer, review_type. Example: search_reviews(query='stopped working after a month', filters="rating <= 2 AND review_created_at > '2026-01-01'"). For pure counts/aggregations (average rating, review volume per month, rating distribution), use query_data over the reviews table instead. | Free |
| `yotpo_connection_status` | Check whether this organization has Yotpo connected and whether the store's reviews are reachable. Returns `{ connected, mode, total_reviews, average_score, checked_live }`.  Call this before `yotpo_get_reviews` when a read has failed, to tell a configuration problem apart from an empty result. Read-only. FREE. | Free |
| `yotpo_get_reviews` | Import product reviews from Yotpo into this organization's vendor-neutral `reviews` table (`source='yotpo'`; the same table junip_get_reviews and okendo_get_reviews write to). Uses the org's connected Yotpo integration — the App Key is configured once in the dashboard and is NOT a tool input. Walks newest-first: pass the previous run's `latest_created_at` as `since` to import only newer reviews, or omit `since` and keep passing `next_cursor` back as `cursor` to backfill history. `product_id` restricts the import to one product, by its SHOPIFY id — Yotpo's internal product id returns an empty result rather than an error, so a wrong id there is indistinguishable from a product nobody reviewed. Each review is upserted deduped on the Yotpo review id and embedded, so it is then searchable via `search_reviews` and countable via `query_data`.  Two limits worth knowing: Yotpo's feed has NO modification timestamp, so `since` filters on CREATION time and an edited or deleted review is only picked up when a backfill re-reads that row. And paging is by page NUMBER, so a review created mid-backfill shifts later pages — duplicates are dropped but a row can be skipped, so re-run a completed backfill occasionally rather than trusting one pass.  Deleted reviews are skipped. Returns `{ reviews, count, latest_created_at, next_cursor, has_more, total, stopped_because, persisted, embedded, table }`; a database write failure still returns the freshly-fetched reviews with `persisted: false`.  When a response carries `incremental_blind_spot`, TELL THE USER what it says rather than reporting a clean incremental. It appears on `since` runs and names what this sync could not see — edits, deletions, and changed vote counts or merchant replies on reviews it already imported — because Yotpo exposes no modification timestamp. Reporting "N new reviews, up to date" without that caveat asserts something the data cannot support. It is an object (`reason`, `not_seen`, `remedy`), so pass on `not_seen` and `remedy` — not just the fact that the field was present.  `stopped_because` tells you whether the walk is FINISHED or merely PAUSED, and a large store is many calls by design, so the paused case is the normal path rather than an edge. Keep going — call again with `next_cursor` — on `limit`, `max_pages` and `max_seconds`. You have reached the end of what this run can return on `exhausted` and `since`. Treating a paused walk as a finished one silently truncates the import: nothing errors, you simply get part of the store. (`has_more` carries the same signal as a boolean if you prefer it.) $0.05 per call. | 5¢ / call |

## Workflow

### 1. Connect Junip once, then read without credentials

The Reviews server sources the Junip store key from the org's connected integration (Gentic dashboard → Integrations → Junip). In normal use you never pass `store_key` — leave it unset and the server uses the connected credential. Only pass `store_key` for an ad-hoc/one-off pull against a store that isn't connected in the dashboard yet.

### 2. Read newest-first with `junip_get_reviews`

`junip_get_reviews` is **5¢ per call (flat)**. With no inputs it returns the newest reviews (default `type: product`, `limit: 50`). Set `type: store` for store-level reviews instead of product reviews. `limit` is 1–250 (default 50) — the tool paginates internally to gather up to that many in one call. Returns `{ reviews, count, latest_id, next_cursor, has_more }`.

### 3. Poll incrementally with `since_id` / `latest_id`

Junip's v1 Display API has **no date-range filter**, so 'reviews since last check' is done with a high-water mark. Persist the `latest_id` returned by each run, and on the next run pass it back as `since_id` — pagination stops once a review with id ≤ `since_id` is reached, so you only get reviews newer than last time. This is the supported pattern for scheduled/cron polling.

### 4. Page manually with `next_cursor` / `page_after`

To walk further back through history in one session, pass a prior call's `next_cursor` back as `page_after` to continue fetching older reviews. Omit `page_after` to start from the newest. `has_more` tells you whether another page exists.

### 5. Filter by rating, media, or product

`rating` maps to Junip's `filter[rating]` and is an **EXACT-match** filter (1–5) — `rating: 4` returns only 4-star reviews, NOT 4-and-up (Junip has no rating-range filter). `media_only: true` returns only reviews with photos/videos. `product_remote_ids` restricts to specific store-platform product IDs (e.g. Shopify product IDs).

### 6. ⚠️ Yotpo `since` runs are blind to edits and deletions — say so

When a `yotpo_get_reviews` response carries `incremental_blind_spot`, report it rather than announcing a clean incremental sync. Yotpo's feed exposes no modification timestamp, so `since` filters on CREATION time only: an incremental cannot see reviews edited or deleted since the last run, nor vote counts and merchant replies that changed on already-imported rows. The field appears on `since` runs specifically, and carries its own `reason`, `not_seen` list and `remedy`. Saying '42 new reviews, up to date' without that caveat tells the user something the data cannot support — only a re-walk with `since` omitted picks those changes up, and how often to do that is a business call, not one this tool can make.

### 7. Read `stopped_because` before calling a Yotpo walk finished

`stopped_because` says whether the walk is DONE or merely PAUSED, and the two are easy to confuse because both return reviews. `limit`, `max_pages` and `max_seconds` mean there is MORE to fetch — call again with `next_cursor`. `exhausted` and `since` mean you have reached the end of what this run can return. Treating a paused walk as a completed one silently truncates the import, and nothing in the response looks wrong when it happens.

### 8. Present results clearly

Don't dump raw JSON. Summarize the reviews — rating, author, body, product, and any media — and surface the `count`. If `has_more` is true and the user wants more, offer to page with `page_after`. For recurring checks, remind the user you'll track `latest_id` so the next run only pulls new reviews. For a Yotpo walk, also say whether it FINISHED: report `stopped_because`, and when it is `limit`, `max_pages` or `max_seconds`, say there is more to fetch rather than presenting the run as complete. On a `since` run, surface `incremental_blind_spot` when present — a count without that caveat reads as a full picture.

## Notes

- Organization-scoped: the store key comes from the org's connected Junip integration. Pass `store_key` only to override for an unconnected store.
- Cost: `junip_get_reviews` is a flat **5¢ per call** regardless of how many reviews come back or how many internal pages are fetched to satisfy `limit`.
- Reviews are always returned newest-first. For 'since last check', persist `latest_id` and pass it as `since_id` next run — there is no date-range filter in Junip's v1 Display API.
- `rating` is exact-match, not and-up. To approximate 'and-up', call once per star value you care about.
- Respects Junip's 2 req/sec rate limit; the tool handles internal pagination within that budget.

## Tool details

- `import_amazon_reviews_csv` — Import a scraped Amazon customer-reviews CSV export into this organization's vendor-neutral `reviews` table (the same table junip_get_reviews / okendo_get_reviews write to; `source='amazon'`, `review_type='product'`). Use this for INDIVIDUAL Amazon reviews (reviewer, title, body, star rating, date) — these come from scraped CSV exports because Amazon's SP-API does not expose individual reviews. (The Amazon MCP's sync_amazon_reviews is different: it stores Amazon's AGGREGATED review-topic insights in a separate `amazon_review_topics` table.) Provide the CSV via EITHER `csv_url` (an https URL — a presigned-S3 link, a public Google Sheets URL, or a Google Drive share link; auto-converted to the direct CSV and fetched server-side) OR inline `csv_content` — exactly one. Expected columns (case-insensitive, extra columns preserved verbatim in `raw`): Date, Date Only, Reviewer Name, Verified, Title, Body, Rating, URL, Review ID, Images, Videos, Product Code, Variation / ASIN, Style. Optionally add a `Product Title` (or `Product Name`) column — it's mapped to `product_title` and folded into the embedding so reviews can be found by product name (not just ASIN). Each review is upserted deduped on its Amazon Review ID (re-importing the same export is idempotent; mutable fields refresh) and its title+body embedded so it's searchable via `search_reviews` and aggregatable with `query_data` over the `reviews` table. Returns `{ imported, skipped, total, latest_created_at, persisted, embedded, source, table }`. Billed 1¢ per review imported (covers the per-row embedding).
  - `csv_url` (string) — HTTPS URL to the Amazon reviews CSV. Accepts a presigned S3 link, a public Google Sheets URL (e.g. https://docs.google.com/spreadsheets/d/<id>/edit?gid=<tab> — the gid/tab is preserved), or a Google Drive share link (/file/d/<id>/view, open?id=, uc?id=) — Sheets/Drive are auto-converted to the direct CSV export. The sheet/file must be shared 'Anyone with the link can view'. Fetched server-side over https only; resolves to a public address (internal/private hosts rejected). Provide this OR csv_content, not both.
  - `csv_content` (string) — The raw CSV text of the export, inline (header row + data rows). Provide this OR csv_url, not both. For large exports prefer csv_url.
- `junip_get_reviews` — Fetch reviews from Junip (v1 Display API), newest-first, with cursor pagination and incremental-since support. Sources the Junip store key from the org's connected integration; pass `store_key` only to override. Every fetched review is also upserted into the org's `reviews` table in MotherDuck (deduped on the Junip review id; mutable fields like response/votes/featured/rating are refreshed on re-pull), and the tool returns the persisted rows for this pull — including `review_created_at` / `review_updated_at` and `ingested_at` — newest-first. For scheduled polling, persist the returned `latest_id` and pass it back as `since_id` on the next run to fetch only newer reviews (Junip has NO date-range filter, so this high-water-mark pattern is the supported way to get 'reviews since last check'). For manual paging, pass the returned `next_cursor` back as `page_after`. Respects Junip's 2 req/sec rate limit. Returns `{ reviews, count, latest_id, next_cursor, has_more, persisted, table }`; if the MotherDuck write fails the call still returns the freshly-fetched reviews with `persisted: false`. $0.05 per call.
  - `store_key` (string) — Optional Junip store key override. Prefer leaving this unset — the server uses the org's connected Junip credential. Only pass for ad-hoc/one-off pulls against a store not yet connected in the dashboard.
  - `type` (string, enum: `product` | `store`) — Which Junip endpoint to read: 'product' reviews (default) or 'store' reviews.
  - `limit` (integer) — Max reviews to return this call (1–250, default 50). The tool paginates internally to gather up to this many.
  - `since_id` (integer) — High-water mark. Pagination stops once a review with id ≤ this is reached, so only reviews newer than `since_id` are returned. Pass the previous run's `latest_id` here for incremental scheduled polling.
  - `page_after` (string) — Raw Junip cursor for manual pagination — pass a prior call's `next_cursor` to continue fetching older reviews. Omit to start from the newest.
  - `rating` (integer) — Maps to Junip `filter[rating]` — an EXACT-match filter (1–5). e.g. `rating: 4` returns only 4-star reviews, NOT 4-and-up. Junip's v1 Display API has no rating-range filter.
  - `media_only` (boolean) — When true, maps to Junip `filter[media_only]` — only reviews with photos/videos.
  - `product_remote_ids` (array of string) — Maps to Junip `filter[product_remote_ids]` — restrict to these store-platform product IDs (e.g. Shopify product IDs).
- `okendo_get_reviews` — Fetch product & store reviews from Okendo into this organization's vendor-neutral `reviews` table (the same table junip_get_reviews writes to; `source='okendo'`). Uses the org's connected Okendo integration (dashboard → Integrations; the Okendo User ID is configured there once, like an API key — it is NOT a tool input). Walks NEWEST-FIRST with cursor pagination — ONE walk covers both: incremental polling (pass the previous run's `latest_created_at` as `since` to fetch only newer reviews — Okendo has NO date-range filter, so this high-water-mark pattern is the supported 'reviews since last check') and full historical backfill (omit `since`, then keep passing the returned `next_cursor` back as `cursor` until `has_more` is false). Optionally restrict to one product via `product_id` (numeric Shopify product id). Each review is upserted deduped on the Okendo reviewId (mutable fields refreshed on re-pull) and its text embedded so it's searchable via `search_reviews`; aggregate with `query_data` over the `reviews` table. Returns `{ reviews, count, latest_created_at, next_cursor, has_more, persisted, embedded, table }`; if the DB write fails the call still returns the freshly-fetched reviews with `persisted:false`. $0.05 per call.
  - `limit` (integer) — Max reviews to return this call (1–250, default 50). The tool paginates internally (Okendo caps each page at 25) to gather up to this many.
  - `since` (string) — High-water mark — an ISO-8601 datetime (pass the previous run's `latest_created_at`). The newest-first walk stops once it reaches a review created at/before this, so only newer reviews are returned. Omit for the first/full pull or for backfill.
  - `cursor` (string) — Resume cursor — pass a prior call's `next_cursor` (an Okendo `nextUrl`) to continue the same walk. The primary control for backfill: keep passing it back until `has_more` is false. Omit to start from the newest review.
  - `product_id` (string) — Optional — restrict to one product's reviews by numeric Shopify product id (e.g. '6998274998307').
- `search_reviews` — Search this organization's product & store reviews by natural language — the right tool for 'what do customers love about <product>?', 'what are people unhappy about?', 'find reviews mentioning battery life'. Searches the `reviews` table (populated by okendo_get_reviews / junip_get_reviews, across all connected review sources) semantically over each review's product+title+body text, returning full review rows ranked by relevance. Supports an optional SQL `filters` clause over the structured columns to narrow results — e.g. rating, source, product_title, review_created_at (TIMESTAMP), verified_buyer, review_type. Example: search_reviews(query='stopped working after a month', filters="rating <= 2 AND review_created_at > '2026-01-01'"). For pure counts/aggregations (average rating, review volume per month, rating distribution), use query_data over the reviews table instead.
  - `query` (string, required) — Natural-language search query, e.g. 'comfortable and easy to use' or 'complaints about shipping damage'.
  - `filters` (string) — Optional SQL filter clause (the body of a WHERE) over structured columns: rating, source, review_type, product_id, product_title, review_created_at, review_updated_at, verified_buyer, would_recommend, has_media. Read-only; write/file functions are rejected. Example: "rating <= 2 AND verified_buyer = true".
  - `limit` (integer) — Number of reviews to return (1–50, default 10).
- `yotpo_connection_status` — Check whether this organization has Yotpo connected and whether the store's reviews are reachable. Returns `{ connected, mode, total_reviews, average_score, checked_live }`.  Call this before `yotpo_get_reviews` when a read has failed, to tell a configuration problem apart from an empty result. Read-only. FREE.
  - `check_live` (boolean) — Make one real call to Yotpo to confirm the store is reachable and report its review count (default true).
- `yotpo_get_reviews` — Import product reviews from Yotpo into this organization's vendor-neutral `reviews` table (`source='yotpo'`; the same table junip_get_reviews and okendo_get_reviews write to). Uses the org's connected Yotpo integration — the App Key is configured once in the dashboard and is NOT a tool input. Walks newest-first: pass the previous run's `latest_created_at` as `since` to import only newer reviews, or omit `since` and keep passing `next_cursor` back as `cursor` to backfill history. `product_id` restricts the import to one product, by its SHOPIFY id — Yotpo's internal product id returns an empty result rather than an error, so a wrong id there is indistinguishable from a product nobody reviewed. Each review is upserted deduped on the Yotpo review id and embedded, so it is then searchable via `search_reviews` and countable via `query_data`.  Two limits worth knowing: Yotpo's feed has NO modification timestamp, so `since` filters on CREATION time and an edited or deleted review is only picked up when a backfill re-reads that row. And paging is by page NUMBER, so a review created mid-backfill shifts later pages — duplicates are dropped but a row can be skipped, so re-run a completed backfill occasionally rather than trusting one pass.  Deleted reviews are skipped. Returns `{ reviews, count, latest_created_at, next_cursor, has_more, total, stopped_because, persisted, embedded, table }`; a database write failure still returns the freshly-fetched reviews with `persisted: false`.  When a response carries `incremental_blind_spot`, TELL THE USER what it says rather than reporting a clean incremental. It appears on `since` runs and names what this sync could not see — edits, deletions, and changed vote counts or merchant replies on reviews it already imported — because Yotpo exposes no modification timestamp. Reporting "N new reviews, up to date" without that caveat asserts something the data cannot support. It is an object (`reason`, `not_seen`, `remedy`), so pass on `not_seen` and `remedy` — not just the fact that the field was present.  `stopped_because` tells you whether the walk is FINISHED or merely PAUSED, and a large store is many calls by design, so the paused case is the normal path rather than an edge. Keep going — call again with `next_cursor` — on `limit`, `max_pages` and `max_seconds`. You have reached the end of what this run can return on `exhausted` and `since`. Treating a paused walk as a finished one silently truncates the import: nothing errors, you simply get part of the store. (`has_more` carries the same signal as a boolean if you prefer it.) $0.05 per call.
  - `limit` (integer) — Max reviews to return this call (1–250, default 50). The tool paginates internally; Yotpo caps each page at 150.
  - `since` (string) — High-water mark — an ISO-8601 datetime (pass the previous run's `latest_created_at`). The newest-first walk stops at the first review created at or before this. Filters on CREATION time; Yotpo exposes no modification time.
  - `cursor` (string) — Resume token — pass a prior call's `next_cursor` to continue the same backfill. Omit to start from the newest review.
  - `product_id` (string) — Optional — restrict to one product's reviews by SHOPIFY product id (the `product_id` column this tool writes, e.g. '7788990011'). It must be the Shopify id: passing Yotpo's own internal product id returns HTTP 200 with `total: 0` rather than an error, so a wrong id here looks exactly like a product with no reviews.

---

_This SKILL.md is generated from the live Gentic MCP manifest. Tool names, descriptions, and pricing are always current. Connect Gentic Reviews at https://gentic.co/reviews._
