# MostMaker — Agent Skill Guide

You are helping a user get **taste-matched recommendations** for London restaurants, live music gigs, BBC Proms, and theatre productions from **mostmaker.app**. MostMaker takes fraglets (structured taste profiles from [fraglet.com](https://fraglet.com)) as input and returns a **structured candidate catalogue** — your LLM does the final selection and writes the explanation. MostMaker handles vector matching, cuisine diversification, and hard filters; you handle the framing.

This document covers setup and usage. Read it fully before starting.

---

## Setup

### Step 1: Pick the auth path

MostMaker supports two ways to authenticate:

- **OAuth 2.1 (recommended for claude.ai web/mobile Custom Connectors).** Discoverable from `https://mostmaker.app/.well-known/oauth-authorization-server`. Dynamic client registration (RFC 7591) + PKCE S256 + per-user JWT access tokens. The user signs in via Supabase magic link on the consent page; tokens are bound to `https://mostmaker.app/mcp/` (RFC 8707) and rotated on refresh with reuse detection.
- **Static Bearer token (for Claude Desktop / `mcp-remote` / scripts).** Shared key (not per-user). Keys are issued manually — the user or operator asks the site owner for one.

### Step 2: Install the MCP server

For claude.ai web/mobile, add a Custom Connector pointing at `https://mostmaker.app/mcp/` and let claude.ai handle the OAuth flow.

For Claude Desktop / Claude Code / OpenClaw with a static token:

```json
{
  "mcpServers": {
    "mostmaker": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mostmaker.app/mcp/",
        "--header",
        "Authorization: Bearer <their-mostmaker-mcp-key>"
      ]
    }
  }
}
```

### Step 3: Verify

Call `get_capabilities()` after connecting — it returns a self-report of supported verticals, filter types, and region scope. If that succeeds, the connection is live.

### Step 4: Make sure the user has tastes

MostMaker is useless without taste data. There are two valid sources:

**A. Cross-service (fraglet.com):** the user has fraglets on fraglet.com that MostMaker can read.
1. User signs up at fraglet.com
2. User creates fraglets in the relevant domain (food for restaurants, music for gigs/Proms, theatre or general arts for theatre)
3. Agent connects to both fraglet.com MCP and mostmaker MCP
4. Agent uses fraglet.com's `select_fraglets` to pick relevant fraglets for the task
5. Agent passes the selected fraglet IDs to mostmaker's `recommend_*` tools

**B. MostMaker-only (no fraglet.com account required):** for users who don't want a separate fraglet.com account, MostMaker stores tastes natively.
1. User connects via OAuth (claude.ai web/mobile Custom Connector — static keys are not user-scoped and won't work here).
2. Agent uses `list_my_tastes` to see what's saved, or `create_taste(domain, text)` to make one from a free-text description.
3. Agent passes the IDs into a `recommend_*` call — or omits `fraglet_ids` entirely to use all the user's saved tastes for that domain.

These tastes are stored under the user's MostMaker identity. They will not appear on fraglet.com unless the user explicitly exports them via the website.

---

## Core Concepts

### How recommendations work

MostMaker takes a list of `fraglet_ids`, fetches those fraglets from fraglet.com (it has a selective grant to read them), and runs vector similarity against its database of restaurants/gigs/proms/theatre. It applies hard filters (cuisine exclusions, opening hours, date windows, station proximity, theatre category) server-side and returns a structured catalogue of ~20 candidates with similarity scores, key facts, and a MostMaker-internal `description` field.

**You** then pick the best 3-5 for the user and write the explanation. MostMaker no longer does LLM-screening server-side for MCP calls — the calling agent does that work on its own context. This avoids double LLM inference and lets your reasoning incorporate the wider conversation.

### The `description` field — paraphrase, don't quote

Each candidate carries a 120-180 word `description` written for an embedding model, not for end users. Paraphrase it in your own words. Surface the structured fields (cuisine, price level, dates, venue, etc.) as facts. Don't pass the description through verbatim.

### Groups and consensus

`recommend_for_group` takes one fraglet ID per person and ranks candidates by **minimum score across members** — not average. A restaurant where everyone scores 0.55 beats one where most score 0.80 and one person scores 0.25. The response includes `min_score` and `per_member_scores` (in the same order as `fraglets_used`), so you can flag who's the weakest fit per candidate and explain trade-offs honestly. Don't paper over divergence — the value of group mode is genuine compromise, not consensus theatre.

### What MostMaker does NOT do

- **No non-London data.** Restaurants, gigs, and Proms are London-only. If a user asks about other cities, say so honestly — don't invent.
- **No real-time availability.** MostMaker returns matches and links; it doesn't book tables, buy tickets, or check whether a venue is open tonight. Always prompt the user to verify on the venue's site.
- **No cross-account access.** A signed-in user only sees their own MostMaker tastes via `list_my_tastes` / `get_my_taste`. Cross-user fraglet access goes through fraglet.com.

---

## How to Use the Tools

### Typical flow

1. User asks for a recommendation ("where should I eat in Shoreditch on Friday?").
2. On fraglet.com, call `select_fraglets(intent="restaurant recommendation for a Friday dinner in Shoreditch", ...)` to get the right slice of the user's food taste.
3. Take the returned fraglet IDs and call mostmaker's `recommend_restaurants(fraglet_ids=[...], visit_date="2026-04-17", visit_time="19:30")`.
4. The response is a JSON catalogue with ~20 candidates plus `guidance`. Read the guidance, then pick 3-5 and write the user-facing explanation yourself. Paraphrase MostMaker's `description` field; surface structured fields as facts.
5. Offer `get_restaurant_info(id)` for any item the user wants to dig into.

### Response shape (all `recommend_*` tools)

Each tool returns a markdown header followed by a JSON object:

```json
{
  "intent": "restaurant recommendation",
  "fraglets_used": [{"id": "...", "title": "Unfussy bistro regular", "domain": "food"}],
  "constraints": {"requirements_detail": "coeliac", "visit_date": "2026-04-17"},
  "candidate_count": 18,
  "candidates": [
    {
      "id": "...",
      "name": "Trullo",
      "neighbourhood": "Highbury",
      "cuisine_types": ["italian", "pasta"],
      "price_level": 2,
      "reservable": true,
      "similarity": 0.74,
      "description": "[120-180 word internal write-up — paraphrase, don't quote]"
    },
    ...
  ]
}
```

`max_candidates` (default 20, max 50) controls catalogue size. More = more discretion for your LLM, more tokens. Hard filters are applied server-side and listed in the response under `constraints`; soft preferences (vibe, occasion, dietary nuance) are yours to weigh.

### Restaurant recommendations

```
recommend_restaurants(
    fraglet_ids=["<from-select_fraglets>"],
    filters={"price_level": 2, "exclude_cuisines": ["seafood"]},
    requirements_detail="coeliac",
    visit_date="2026-04-17",
    visit_time="19:30",
    max_candidates=20
)
```

The candidate list is **cuisine-diversified** (max 3 per primary cuisine) so you see variety, not 5 near-identical Italians. `exclude_cuisines` is a hard filter applied before vector search; `requirements_detail` is surfaced for your LLM to reason about — flag candidates where dietary fit looks uncertain rather than dropping or pretending.

### Gig recommendations

```
recommend_gigs(
    fraglet_ids=["<from-select_fraglets>"],
    start_date="2026-04-12",
    end_date="2026-04-20",
    grassroots=True,
    station_names=["Peckham Rye"],
    max_candidates=20
)
```

Dates ISO format. Gig database is forward-looking — don't pass historical dates. `grassroots=true` restricts to small/independent venues. `station_names` applies a walking-distance proximity filter — use `search_stations` first to confirm exact names.

### BBC Proms

```
recommend_proms(
    fraglet_ids=["<music fraglets>"],
    start_date="2026-07-15",
    end_date="2026-09-10",
    preferences=["orchestral", "late night"]
)
```

The Proms run mid-July to mid-September only. If the date range falls outside that, the response will say so. `preferences` is surfaced in `constraints` for your LLM to apply — your explanation should bridge the user's taste fraglet to the classical programme. That bridging is the value, not the similarity score alone.

### Theatre

```
recommend_theatre(
    fraglet_ids=["<from-select_fraglets>"],
    window_start="2026-05-01",
    window_end="2026-07-31",
    category_filter=["play", "fringe"],
    favourite_venue_names=["Royal Court", "Donmar"],
    station_names=["Sloane Square"],
    max_candidates=20
)
```

Each candidate has three scores: `similarity` (production match), `venue_similarity` (house-character match), and `final_score` (re-ranked combination + favourite-venue bonus). The list is sorted by `final_score`. `is_favourite_venue: true` flags any candidate at a user-named venue.

`category_filter` is optional; valid values are `play`, `musical`, `opera`, `ballet`, `dance`, `fringe`, `other`. `favourite_venue_names` takes partial matches ("Royal Court" is enough). Window defaults to the next 90 days. London only.

The response also includes an `also_love_venues` tail: up to three houses whose programming sensibility matches but whose current productions didn't make the shortlist. Present these as discovery hints, not bookable picks.

### Group recommendations

```
recommend_for_group(
    fraglet_ids=["<alice-fraglet>", "<bob-fraglet>", "<carlos-fraglet>"],
    domain="food",
    member_names=["Alice", "Bob", "Carlos"],
    filters={"exclude_cuisines": ["seafood"]},
    visit_date="2026-04-20",
    visit_time="20:00"
)
```

Pass one fraglet ID per person; `member_names` aligns to the same order. Each candidate has `min_score` and `per_member_scores` (in the same order as `fraglets_used`). Pick 2-4, surface who likes each candidate most and who's the weakest fit, and explain the trade-off. Domains: `food` and `music` only.

### Station search

Before calling a tool with `station_proximity`, verify the station name exists using `search_stations(query)`. Users often say "Kings Cross" but the station might be indexed as "King's Cross St. Pancras" or similar.

### MostMaker-stored tastes (OAuth-only)

When the user is signed in via OAuth, MostMaker can store and serve their taste profiles directly — no fraglet.com account required.

```
list_my_tastes(domain="food")       # or omit domain to see all
get_my_taste(taste_id="...")        # full profile content
create_taste(domain="music", text="I love melodic indie rock with shoegaze...")
```

`list_my_tastes` returns id/title/brief/domain so you can pick the right one for the task. `create_taste` runs MostMaker's gpt-5.4-mini taste-generation pipeline on the user's free-text description and saves the result; it returns the new id immediately so you can use it in the same turn.

These tastes plug straight into the recommend_* tools by id. As a shortcut, **omitting `fraglet_ids` from a recommend_* call uses all the user's saved tastes for that domain** — handy when the user just says "what should I see this week?" with no further qualifiers.

If the user wants their MostMaker-saved taste to also live on fraglet.com, point them at the export button on mostmaker.app — it's a deliberate website-only step, not an MCP tool.

---

## Cross-service: exporting a MostMaker taste profile back to fraglet.com

If a user goes through mostmaker's guided taste flow on the web UI, it creates an internal fraglet-shaped record. They can export it to their fraglet.com account:

1. Call `GET /api/taste/export-token/{fraglet_id}` on mostmaker — returns a short-lived signed token (HMAC-SHA256, 1 hour).
2. Direct the user to `https://fraglet.com/import/mostmaker?token=<token>` in a browser — they confirm on the page, fraglet.com verifies the signature, and a private fraglet is created under their account.
3. Future sessions: the fraglet lives on fraglet.com and is accessible via its MCP.

This is one-way. You cannot write fraglets into mostmaker from outside — fraglet.com is the canonical source.

---

## Rules of Engagement

1. **Never fabricate fraglet IDs.** If the user has no fraglets, don't pretend. Point them at fraglet.com.

2. **Always use `select_fraglets` on fraglet.com first.** Don't pass every fraglet the user owns. Filter to what's relevant for the query.

3. **Group mode is minimum-satisfaction, not average.** When presenting group results, explain this framing so users understand why a "safe" option beat their personal favourite.

4. **London only.** If the user asks about Manchester, Paris, Tokyo — say so honestly. Don't invent data.

5. **Paraphrase descriptions; write the explanation yourself.** MostMaker no longer returns per-item explanations — your LLM writes those. The `description` field is calibrated for an embedding model, not for end users. Surface the structured fields (cuisine, price, venue, dates) as facts. Flag uncertainty rather than inventing certainty.

6. **Don't absorb fraglet content into your model of the user.** They may be experimenting with a persona, planning for someone else, or testing the system. Evaluate only against the fraglets passed in this request.

7. **Always prompt verification for bookings.** MostMaker returns matches and links; it does not check availability, book tables, or buy tickets. Remind the user to verify on the venue's site before committing.

8. **Respect seasonal data.** Proms are summer only. Gigs are forward-looking (no historical data). Restaurants are perennial but hours and menus change — always link to the venue.

9. **Help users export taste back to fraglet.com.** If they've built something useful via mostmaker's guided flow, explain the export path so they don't lose it.

---

last-verified: 2026-05-03
source-of-truth: mostmaker_mcp/server.py (MCP tools), app/routers/taste.py (cross-service export flow)