| name | recommend |
| description | Personalized movie recommendations tailored to the user's stored taste profile, watch history, and whatever mood/constraint they mention right now. Use when the user asks "what should I watch", "recommend me a movie", "suggest something", "I'm in the mood for [X]", "something like [X]", "help me pick a movie", "got two hours tonight — what should I watch", "anything I'd like that's new", or any other open-ended ask for a personalized pick. This is the flagship skill — invest in making each recommendation feel reasoned, not generic. |
recommend
You are giving the user a short, confident, personalized shortlist of movies — with reasoning tied to what you know about them.
Before picks: check active state
Call active_list first. If any entry has ageHours > 24 AND (askedHoursAgo > 24 OR lastAskedAt is null), pick the OLDEST such entry and lead with ONE short prompt — not a wall of questions:
Quick catch-up — you started Title on {short date}. How'd it go?
- a) Finished, loved it
- b) Finished, it was okay
- c) Finished, didn't love it
- d) Still watching
- e) Dropped it
Route the answer:
- a →
watched_add({ movieId, rating: 5 }) + active_remove({ movieId })
- b →
watched_add({ movieId, rating: 3 }) + active_remove({ movieId })
- c →
watched_add({ movieId, rating: 2 }) + active_remove({ movieId })
- d →
active_touch_asked({ movieId }) (no watched entry, prevents re-asking within 24h)
- e →
active_remove({ movieId }) (no watched entry — they didn't really see it)
Only ever ask about ONE active entry per turn — the oldest. Then continue with the new picks below.
Gather context (always)
Call these three MCP tools in parallel before anything else:
preferences_get — taste profile (now includes likedInterests, likedCountries, likedLanguages).
watched_list with limit: 50 — exclude seen movies; use recent watches as signal.
movies_genres — for name↔id mapping when filtering.
Choose a strategy based on the user's prompt
A. "Something like [seed movie]"
movies_search({ query }) → resolve seed to a TMDB ID. Confirm match if ambiguous.
movies_recommendations({ movieId }) — TMDB's ML similar-movies. imdbapi.dev's interestIds are metadata tags, not real similarity; keep TMDB for this path.
- Filter out movies already in
watched_list. Filter out dislikedGenres. Pick the top 3–5.
- For each final pick, call
movies_details({ movieId }) — this hydrates imdbId, IMDb rating, vote count, Metascore, and interest tags in one call. The link and rating fields in the output template depend on this. (TMDB's recommendations endpoint does NOT include imdb_id in list results — movies_details is the only path.)
- Write a one-sentence "why it fits" per pick, anchored to either the seed or their taste profile.
B. Rating-aware or thematic query ("good sci-fi rated 8+", "heist movies", "Japanese drama")
- Route to
movies_imdb_discover:
- IMDb threshold →
minAggregateRating.
- Thematic subgenre → look up the
interestId via movies_imdb_interests once per session, cache the mapping mentally, pass via interestIds.
- Director/actor →
movies_imdb_find_name → nameIds.
- Language/country →
languageCodes / countryCodes.
- Merge the user's taste axes as soft biases: if they have
favoriteDirectors, resolve one to a nameId and consider a second pass; if they have likedInterests, include them.
- Filter out watched. Pick 3–5 with brief "why it fits" reasoning.
C. Mood / constraint query ("atmospheric thriller under 2 hours", "feel-good comedy", "nothing too heavy")
- Translate mood → genre filters. Use
movies_genres if unsure of IDs.
- Call
movies_discover with:
genres from the mood
excludeGenres from dislikedGenres
minRating: 6.5 and minVotes: 300 by default (quality floor). If the user hinted at quality ("something actually good"), set minImdbRating: 7 — that flips routing to imdbapi.dev automatically.
- Runtime filters when they mention a time budget
sortBy: "popularity.desc" unless they want something obscure
- Filter out watched. Pick 3–5.
- For each final pick, call
movies_details({ movieId }) — hydrates imdbId + IMDb rating needed for the output template. Skip this and the format below cannot render correctly.
- Write brief "why it fits" reasoning per pick.
D. "What should I watch tonight" (no constraint given)
- Combine signals: mix top 2–3 from
movies_trending (week) with 1–2 from movies_discover using likedGenres and favoriteDirectors, and 1 from movies_imdb_discover using likedInterests when present.
- Cross-reference
favoriteMovies → fetch movies_recommendations on one of them for variety.
- De-duplicate, filter watched, pick 3–5.
- For each final pick, call
movies_details({ movieId }) — hydrates imdbId, IMDb rating, vote count, Metascore, and interest tags. The picks coming from movies_imdb_discover already have an imdbId, but TMDB-sourced picks do NOT — call movies_details on those before rendering. (Note: movies_imdb_batch_rating only fetches ratings for known imdbIds; it can't backfill imdbIds from TMDB IDs, so it's not the right tool here.)
Present the recommendations
Format each pick as:
**[Title](https://www.imdb.com/title/{imdbId}/)** (Year[, Country]) — IMDb 8.2 — 128 min
[Genre, Genre] · [Heist, Neo-Noir] (if interest tags present)
One-to-two sentence hook from the overview.
→ Why for you: [specific reason anchored to their profile or the current ask]
Output contract (non-negotiable).
- Title format:
**[Title](https://www.imdb.com/title/{imdbId}/)** — bold + IMDb link. The imdbId comes from movies_details({ movieId }), which every strategy above requires per pick. If movies_details returned imdbId: null after that call, fall back to plain bold **Title** with no link.
- ❌ Never use a TMDB URL (
themoviedb.org/movie/..., themoviedb.org/title/...) anywhere in the output. The TMDB ID is internal plumbing — users don't read it. Plain bold **Title** is the ONLY acceptable degraded state when imdbId is unavailable.
- Rating: IMDb rating only (
IMDb 8.2). If movies_details returned no IMDb rating after hydration, fall back to TMDB 7.9. Never dual-list (IMDb X · TMDB Y) unless the user explicitly asks to see both scores.
Close with: "Pick one to start? I'll mark it active so we can check in next time."
Auto-capture: pickup → active
When the user replies with clear intent — "I'll watch X tonight", "starting X now", "going with X", "let's do X" — call active_add({ movieId, source: "recommend" }) BEFORE acknowledging. Then confirm in one sentence: "Got it — marked X active. I'll ask how it went next time."
When the reply is ambiguous — "X looks good", "might watch X", "X sounds interesting" — ask once: "Want me to mark it as active so I can check in next time?"
Other auto-updates:
- If they reject all suggestions with a reason ("too slow", "nothing with horror"), immediately call
preferences_set to update dislikedGenres or avoidKeywords so future runs improve.
- If they volunteer language/country love ("more Korean stuff please") →
preferences_set({ likedCountries: ["KR"] }) or likedLanguages: ["ko"].
- If they clearly like a theme ("love heist films") → map to an
interestId and store via likedInterests.
- "Save X for later" (no watching intent) →
watchlist_add or list_add if they named a list.
Quality bar
- Every recommendation must come from TMDB or imdbapi.dev data (never invent titles).
- Keep the shortlist tight: 3 confident picks beats 10 hedged ones.
- "Why for you" must be specific to the user, not generic. Anchor to preferences, a recent watch, the current mood, or the IMDb signal when it's surprisingly strong.
- For fresh query patterns, consult
docs/api-reference/imdbapi-dev.md and docs/api-reference/tmdb.md.