anilist_watch_order
Find the correct watch order for any anime franchise. Enter a title or ID to get a chronological list of sequels, prequels, and specials from first to last.
Instructions
Suggested viewing order for a franchise. Use when the user asks what order to watch a series, how to start a long franchise, or wants to know the chronological release order of sequels and prequels. Accepts any title in the franchise and traces the full chain. Returns a numbered list from first to last.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | No | AniList media ID of any title in the franchise | |
| title | No | Search by title if no ID is known | |
| includeSpecials | No | Include OVAs, specials, and spin-offs in the watch order |
Implementation Reference
- src/tools/recommend.ts:625-730 (registration)Registration of the anilist_watch_order tool via server.addTool(), including name, description, schema, annotations, and execute handler.
server.addTool({ name: "anilist_watch_order", description: "Suggested viewing order for a franchise. " + "Use when the user asks what order to watch a series, how to start a long franchise, " + "or wants to know the chronological release order of sequels and prequels. " + "Accepts any title in the franchise and traces the full chain. " + "Returns a numbered list from first to last.", parameters: WatchOrderInputSchema, annotations: { title: "Watch Order", readOnlyHint: true, destructiveHint: false, openWorldHint: true, }, execute: async (args) => { try { // Resolve title to ID let mediaId: number; if (args.id) { mediaId = args.id; } else { const data = await anilistClient.query<MediaDetailsResponse>( MEDIA_DETAILS_QUERY, { search: args.title, type: "ANIME" }, { cache: "media" }, ); mediaId = data.Media.id; } // BFS expansion: discover all franchise IDs via batch relation queries const relationsMap = new Map<number, RelationNode>(); let frontier = [mediaId]; const maxRounds = 5; for (let round = 0; round < maxRounds && frontier.length > 0; round++) { const data = await anilistClient.query<BatchRelationsResponse>( BATCH_RELATIONS_QUERY, { ids: frontier }, { cache: "media" }, ); const nextFrontier: number[] = []; for (const media of data.Page.media) { if (relationsMap.has(media.id)) continue; relationsMap.set(media.id, media); // Only follow anime relations to stay within the anime franchise for (const edge of media.relations.edges) { if ( !relationsMap.has(edge.node.id) && edge.node.type === "ANIME" ) { nextFrontier.push(edge.node.id); } } } frontier = nextFrontier; } if (relationsMap.size === 0) { return "Could not find franchise relations for this title."; } const { entries, truncated } = buildWatchOrder( mediaId, relationsMap, args.includeSpecials, ); if (entries.length === 0) { return "No entries found in the watch order. This title may be standalone."; } // Get the franchise name from the root entry const rootTitle = entries[0].title; const lines: string[] = [ `# Watch Order: ${rootTitle} franchise`, `${entries.length} entr${entries.length !== 1 ? "ies" : "y"}${args.includeSpecials ? " (including specials)" : ""}:`, "", ]; for (let i = 0; i < entries.length; i++) { const e = entries[i]; const parts = [e.format ?? "Unknown format"]; if (e.status) parts.push(e.status.replace(/_/g, " ")); if (e.type === "special") parts.push("special"); lines.push(`${i + 1}. ${e.title} (${parts.join(" - ")})`); } if (truncated) { lines.push( "", "Note: This franchise tree was truncated at the depth limit. Some entries may be missing.", ); } return lines.join("\n"); } catch (error) { return throwToolError(error, "building watch order"); } }, }); - src/tools/recommend.ts:640-729 (handler)Execute handler for anilist_watch_order: resolves title to ID, discovers franchise via BFS using BATCH_RELATIONS_QUERY, then calls buildWatchOrder to produce the ordered list.
execute: async (args) => { try { // Resolve title to ID let mediaId: number; if (args.id) { mediaId = args.id; } else { const data = await anilistClient.query<MediaDetailsResponse>( MEDIA_DETAILS_QUERY, { search: args.title, type: "ANIME" }, { cache: "media" }, ); mediaId = data.Media.id; } // BFS expansion: discover all franchise IDs via batch relation queries const relationsMap = new Map<number, RelationNode>(); let frontier = [mediaId]; const maxRounds = 5; for (let round = 0; round < maxRounds && frontier.length > 0; round++) { const data = await anilistClient.query<BatchRelationsResponse>( BATCH_RELATIONS_QUERY, { ids: frontier }, { cache: "media" }, ); const nextFrontier: number[] = []; for (const media of data.Page.media) { if (relationsMap.has(media.id)) continue; relationsMap.set(media.id, media); // Only follow anime relations to stay within the anime franchise for (const edge of media.relations.edges) { if ( !relationsMap.has(edge.node.id) && edge.node.type === "ANIME" ) { nextFrontier.push(edge.node.id); } } } frontier = nextFrontier; } if (relationsMap.size === 0) { return "Could not find franchise relations for this title."; } const { entries, truncated } = buildWatchOrder( mediaId, relationsMap, args.includeSpecials, ); if (entries.length === 0) { return "No entries found in the watch order. This title may be standalone."; } // Get the franchise name from the root entry const rootTitle = entries[0].title; const lines: string[] = [ `# Watch Order: ${rootTitle} franchise`, `${entries.length} entr${entries.length !== 1 ? "ies" : "y"}${args.includeSpecials ? " (including specials)" : ""}:`, "", ]; for (let i = 0; i < entries.length; i++) { const e = entries[i]; const parts = [e.format ?? "Unknown format"]; if (e.status) parts.push(e.status.replace(/_/g, " ")); if (e.type === "special") parts.push("special"); lines.push(`${i + 1}. ${e.title} (${parts.join(" - ")})`); } if (truncated) { lines.push( "", "Note: This franchise tree was truncated at the depth limit. Some entries may be missing.", ); } return lines.join("\n"); } catch (error) { return throwToolError(error, "building watch order"); } }, - src/schemas.ts:297-313 (schema)Zod schema for the anilist_watch_order tool input: optional id, optional title (at least one required), and optional includeSpecials flag.
export const WatchOrderInputSchema = z .object({ id: z .number() .int() .positive() .optional() .describe("AniList media ID of any title in the franchise"), title: z.string().optional().describe("Search by title if no ID is known"), includeSpecials: z .boolean() .default(false) .describe("Include OVAs, specials, and spin-offs in the watch order"), }) .refine((data) => data.id !== undefined || data.title !== undefined, { message: "Provide either an id or a title.", }); - src/engine/franchise.ts:88-148 (helper)buildWatchOrder function: constructs the watch order by traversing sequel and side-story edges from the franchise root, respecting MAX_DEPTH.
export function buildWatchOrder( startId: number, relationsMap: Map<number, RelationNode>, includeSpecials: boolean, ): WatchOrderResult { const rootId = findRoot(startId, relationsMap); const entries: FranchiseEntry[] = []; const visited = new Set<number>(); // BFS through sequel and side-story edges const queue: number[] = [rootId]; let depth = 0; while (queue.length > 0 && depth < MAX_DEPTH) { const id = queue.shift(); if (id === undefined || visited.has(id)) continue; visited.add(id); depth++; const node = relationsMap.get(id); const title = node ? (node.title.english ?? node.title.romaji ?? "Unknown") : "Unknown"; const { format, status } = resolveNodeInfo(id, relationsMap); const isMain = MAIN_FORMATS.has(format ?? ""); if (isMain || includeSpecials) { entries.push({ id, title, format, status, type: isMain ? "main" : "special", }); } if (!node) continue; // Collect sequels and side stories const sequels: number[] = []; const sides: number[] = []; for (const edge of node.relations.edges) { if (visited.has(edge.node.id)) continue; if (edge.relationType === "SEQUEL") { sequels.push(edge.node.id); } else if ( includeSpecials && (edge.relationType === "SIDE_STORY" || edge.relationType === "SPIN_OFF") ) { sides.push(edge.node.id); } } // Side stories appear after their parent, before the next sequel queue.push(...sides, ...sequels); } const truncated = queue.length > 0 && depth >= MAX_DEPTH; return { entries, truncated }; } - src/api/queries.ts:818-843 (helper)BATCH_RELATIONS_QUERY: GraphQL query used to fetch media relations (sequels, prequels, side stories) for franchise discovery.
export const BATCH_RELATIONS_QUERY = ` query BatchRelations($ids: [Int]) { Page(perPage: 50) { media(id_in: $ids) { id title { romaji english } format status relations { edges { relationType node { id title { romaji english } format status type season seasonYear } } } } } } `;