Rovyn

Rovyn lets your AI assistant curate what you listen to.

Community: Submitted by a user or imported; check the owner before granting accessOnlineNo sign-inGlobalFreeCan modify data

What it can do

  • Resolve Content Batch: Validate a BATCH of candidates you found yourself (web search, memory, a show's site) in one call — up to 25 refs. This is the normal way to ground a list: resolve first, then b
  • Resolve Content: Ground ONE piece of content you found yourself. Have more than one? Use resolve_content_batch — it takes up to 25 refs in a single call and is the normal path; this tool is the single
  • Import Feed: Import a WHOLE show into the library from a feed URL, an Apple Podcasts show link, or a show title, then search or list within it. This is the only call that pulls an entire back catalog

What data it sees

Do you need an account

No: the server works without sign-in

Rovyn lets your AI assistant curate what you listen to. Ask for an hour on the chip war for the commute, one skeptical take included — the assistant searches the web, picks, and writes a one-line why for each episode; Rovyn grounds every pick so only genuinely playable episodes make the cut, then hands back one tap-to-play link. The plan opens in the Rovyn player typeset like a magazine issue: durations totalled against your available time, streaming direct from the publisher.

The loop closes on the way back. After you listen, your assistant can read compact consumption receipts: what you finished, what you dropped, and anything you typed in your own words, carried back exactly as you wrote it — the server never summarizes or scores it. The next plan starts from what the last one actually did.

The exchange is scoped by design: the assistant sends a brief (goal, time, level), Rovyn returns receipts. Each assistant connects with its own revocable grant, speaking Cueback, an open MCP convention.

Server tool list (11)

Raw names from tools/list. Only developers need these.

resolve_content_batchValidate a BATCH of candidates you found yourself (web search, memory, a show's site) in one call — up to 25 refs. This is the normal way to ground a list: resolve first, then build a plan from what came back playable. Each ref carries a content_type; this app implements podcast_episode only, and needs at least one of episode_id / guid / feed_url / episode_url — title, show_title, published_at are hints that improve matching but are not a valid ref on their own (for a title-only finding, import_feed the show, then search_episodes within it). Results are returned in input order with input_index, and a per-item verdict: 'playable' with grounded content (episode_id ready for create_plan), or 'unavailable' with a machine-readable reason (subscription_only, private_feed_required, spotify_exclusive, apple_subscription, removed_by_publisher, no_public_feed, audio_unreachable, unsupported_content_type, resolution_failed — that last one is the server's own failure, retry or re-ref it) you can tell the user in plain language. One bad ref never fails the batch. Each ref imports its own episode plus that feed's 50 most recent items, not the show's whole back catalog. Found a whole SHOW rather than specific episodes? Use import_feed instead. Single ref? resolve_content is the same thing for one. When it is due the response also carries standing_feedback: what the user has written about their listening, verbatim, with notes about the shows you just named sorted to the front and the newest after them. Read it before you build the list — one listen is never a permanent preference, but a note about a show you are weighing is the most relevant thing in this payload. It is unfiltered by topic on purpose: deciding which note bears on this build is your job, not the server's.
resolve_contentGround ONE piece of content you found yourself. Have more than one? Use resolve_content_batch — it takes up to 25 refs in a single call and is the normal path; this tool is the single-candidate convenience form. Takes a ContentRef; this app implements the podcast_episode profile only (episode_id, guid, feed_url, or an episode page URL, plus title / show_title / published_at hints) and imports it just in time from Podcast Index and the publisher's canonical RSS feed when the library lacks it — that episode plus the feed's 50 most recent items, NOT the whole back catalog; use import_feed when the user wants the show itself. Only this server can declare content playable — never tell the user something is playable because you saw it on the web. Unavailable is a normal, successful answer, with a machine-readable reason (subscription_only, private_feed_required, spotify_exclusive, apple_subscription, removed_by_publisher, no_public_feed, audio_unreachable, unsupported_content_type for a medium this app cannot play, or resolution_failed when the resolver itself failed and a retry may succeed) you can relay plainly and route around, e.g. by offering an external link. Whole show rather than one episode? Use import_feed. Like the batch form, when it is due this carries standing_feedback: what the user has written about their listening, verbatim, notes about this show placed first, unfiltered by topic because that judgement is yours.
import_feedImport a WHOLE show into the library from a feed URL, an Apple Podcasts show link, or a show title, then search or list within it. This is the only call that pulls an entire back catalog — resolving an episode writes just that episode plus the feed's 50 most recent items — so run it on a show resolution already touched to COMPLETE it. Prefer this over resolve_content_batch when your web search turned up a show rather than specific episodes ('the user should follow Latent Space'), when you want several episodes from one show, or when you want to browse a back catalog you cannot enumerate from the web. Also prefer it when a feed is awkward to fetch yourself — the server's fetcher handles gzip, redirects, feed quirks, and Podcasting 2.0 tags, so hand it the URL rather than parsing XML in-context. Pass exactly one of feed_url / apple_url / show_title. Returns show_id, the canonical feed_url, episode_count, and latest_published_at; the show is then visible to list_shows and search_episodes. No matching feed → show_not_found.
list_showsList the shows in the library — the fastest way to learn what is actually here before you search. Call this FIRST when the user says something like 'anything new in my shows?' or when a search_episodes call comes back thin: it tells you, in one call, which shows exist, how many episodes each has, how many carry publisher transcripts, and when each last published. It is a library inventory, not a catalog of all podcasts — a show missing here simply has not been imported yet, which import_feed fixes. Ordered by title; optional limit (default 50, max 200). When it is due this also carries standing_feedback — what the user has written about their listening, verbatim, each note naming its show, so you can tell an old favourite from a show they abandoned before you start proposing anything.
search_episodesLook inside the USER'S LIBRARY — the shows already imported, the episodes resolved for them plus those shows' recent siblings, and their private feeds. This is NOT a search of the open podcast universe and NOT topic discovery: for that, use your own web search and ground the findings with resolve_content_batch. It is Postgres keyword full-text search over metadata (episode/show title, description, show notes, categories) — transcript text is not indexed in V1, so every result's match_basis disclosure currently reads 'metadata'; do not promise transcript-level matches. Query style: 2–4 distinctive keywords, not a sentence — all terms are tried together first, and if that finds nothing the server automatically retries with ANY term and ranks the results. The 'matched' field tells you which happened: 'all_terms' (tight match), 'any_terms' (loose — skim before recommending), 'none' (the library really does not have it; go web-search, then resolve_content_batch or import_feed). Empty results mean the library lacks it, never that the episode does not exist. When it is due the response carries standing_feedback — what the user has written about their listening, verbatim, notes about these shows placed first; already_listened still arrives on resolve verdicts, and get_recent_receipts with a content_id returns any one receipt whole. Use list_shows first if you do not know what is here. Note that a show which arrived by RESOLUTION is present but not complete — resolving writes that episode plus the feed's 50 most recent items, so if a deep back catalog comes up short, import_feed on its feed_url completes the show and the search is worth repeating.
create_planCreate a consumption plan (an 'edition') from a scoped Brief and ordered picks, and get back a player_link. PRESENTING THE LINK: the edition is ALREADY on the user's Shelf when this call returns — delivery is server-side, the link delivers nothing. It opens the Rovyn app straight to this edition (and is the install path on a phone without the app), so offer it as the shortcut ('it's on your Shelf — this opens it directly'), never as the thing that transfers the edition, and never imply the edition is lost if the link is. ENTRY DISCIPLINE, in order: (1) check the user's LIBRARY first — search_episodes and list_shows are grounded, instant, and carry the user's own standing notes; most requests can be filled entirely from there. (2) Go to the open web only for what the library lacks — and a ref you send must be REAL: an episode_url/feed_url/guid you actually retrieved THIS session, from a live web result or from these tools. Never reconstruct a URL, guid, or feed from memory — remembered refs resolve as unavailable and can zero out the plan (no_playable_content). (3) If you cannot search the web and the library has nothing, say so and ask — never guess refs to fill a slot. Recommendations do NOT have to be catalog ids. Each is either { content_id, why } or { ref: { content_type: 'podcast_episode', episode_url | feed_url | guid, title?, show_title?, published_at? }, why } — real web findings are batch-resolved right here, no pre-resolve step. V1 plays podcast episodes only: a ref of any other content_type is rejected outright, never silently dropped. Picks that cannot be grounded do not fail the call: they come back in 'unresolved' with input_index and a reason (subscription_only, spotify_exclusive, apple_subscription, no_public_feed, audio_unreachable…) — always tell the user what dropped out and why, then substitute via update_plan. Zero playable recommendations is the one hard error (no_playable_content). An edition should usually draw from MORE THAN ONE show: different voices on the same question is most of what a curated edition is for, and nothing here narrows your search corpus — single-show is right only when the goal names a show or series, or the user asked for a course from one voice. When every playable pick in a call comes from one show, the response says so in 'note' — an observation to act on or explain to the user, never an error and never a rejected plan. The response also carries total_duration_seconds so you never have to sum durations yourself; brief.available_minutes is advisory guidance for YOU, not a server-enforced cap — the server never trims the list, you are the editor. Three of the strings you send are read by the USER, not by a log: 'title' names the edition in the app's chrome (a short noun phrase); brief.goal is the HEADLINE, set in large serif on the shelf card — one scannable line, aim ≤ ~60 chars, because a paragraph there truncates mid-sentence and the card stops being readable (put coverage, rationale, and constraints in brief.preferences or a pick's 'why' instead); and every 'why' is shown beside its episode, so write it for them. This call carries standing_feedback whenever the server can read it — what the user has written about their listening, verbatim, notes about these picks' shows first and unfiltered by topic because it is the build and that judgement is yours — and 'note' says so. It arrives after the edition is written but before the user has seen it: if anything there cuts against a pick or the 'why' you wrote for it, revise with update_plan before you hand over the player_link.
update_planEdit an existing plan with a batch of ops: add / remove / reorder / replace. Use it to substitute the picks create_plan reported as unresolved, to drop something the user disliked, or to reorder for the time they actually have. Added and replaced recommendations take a raw ref (content_type 'podcast_episode' plus episode_url / feed_url / guid and hints) just like create_plan, not only catalog ids. The batch is atomic: one rejected op leaves the plan untouched, and unlike create_plan an unresolvable add fails the whole batch (content_unavailable) — resolve_content_batch first if you are unsure. The user is the final editor: recommendations they locked in the app cannot be removed, replaced, or reordered ('locked_recommendation'), and lock/unlock are user-only ops. Returns the updated ConsumptionPlan plus total_duration_seconds.
get_planFetch one plan: its brief, its recommendations in order with your original 'why' rationale, and a per-recommendation consumption_state (not_started | in_progress | completed | removed) — this is how you check mid-week whether the user got to it and what they dropped. Also returns total_duration_seconds (playable runtime, removed recommendations excluded), so you never have to sum durations to answer 'how much is left?'. Works across assistants: with the user's permission you can read a plan a different assistant created, which is how someone switches assistants without losing their queue. For how something LANDED (progress, the user's own words) use get_recent_receipts instead.
create_player_linkMint a fresh universal link for an existing plan. The user taps it and the Rovyn app opens (or installs) straight to that plan. The plan is already on the user's Shelf regardless — a link never delivers, it only opens — so present it as the shortcut, not the handoff. create_plan ALREADY returns a player_link — do not call this right after creating a plan. Use it to re-share later, replace a lost link, or link a plan that predates links. Each call mints a new token; previously minted links stay valid until claimed. The token is the credential, so give the link only to the user in chat.
get_recent_receiptsFind out how your last recommendations actually landed, so the next edition is better. Each receipt has two layers. 'consumption' is what the app observed — outcome (completed | partial | skipped | removed), progress_percent, time_spent_minutes: behavior, not opinion, and never raw playback logs, which never leave the server. 'user_feedback' is the user's own words, verbatim and chronological, exactly as they typed or said them; read them as written and let them outrank both the structured_feedback chips and every number in 'consumption'. Filter with since, plan_id, limit. Read the numbers carefully: unfinished may mean the commute ended, a completion may be autoplay, a forward skip may be an ad, and one session is not a permanent preference. Receipts are the USER'S, so they include content from plans other assistants created — which is why this tool is gated on receipts:read. This is the full-fidelity path: the standing_feedback block that rides the other tools is excerpts, cut at 500 characters and capped at 40 notes; this tool returns receipts whole. Pass content_id to get one specific truncated note back in full. It cannot search text, cannot filter to receipts that carry notes, and cannot page past its own newest 100 — so it redeems a named receipt, not an old tail.
record_feedbackRecord what the user ACTUALLY SAID about a piece of content in conversation. Prefer user_feedback: their own sentence, quoted, not your paraphrase ('the second one was too basic, I wanted the operator view') — it is stored verbatim and comes back verbatim on the receipt, for you or for the next assistant. 'structured' is the optional one-tap convenience beside it: value (worth_my_time | not_worth_my_time), difficulty, length, flags (too_repetitive | too_promotional), free-form tags. Send at least one of the two, both when the user gave you both. If the user said it EARLIER (not this moment), set occurred_at to when they said it; if they are reacting to a specific pick, pass its recommendation_id so the receipt attributes the judgment. It lands in the same store the app's own feedback screen writes to and flows into the content's receipts immediately, where it outweighs passive consumption behavior. Only record what the user expressed — never synthesize feedback from your own reading of their history, and never rewrite their words into something tidier. Repeat submissions are fine; the store is append-only and the user may refine. Requires feedback:write.