anilist_shared_planning
Compare two users' anime or manga planning lists to find overlapping titles and unique entries. See what both plan to watch or read.
Instructions
Find titles on both users' planning lists. Use when two users want to see what they're both planning to watch or read. Shows overlap and unique titles.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| user1 | Yes | First AniList username | |
| user2 | Yes | Second AniList username | |
| type | No | Compare anime or manga planning lists | ANIME |
| limit | No | Max entries to show (default 25, max 50) |
Implementation Reference
- src/tools/social.ts:383-439 (handler)Execute function for anilist_shared_planning tool. Fetches planning lists for two users from AniList, computes overlap (shared titles sorted by score), and returns a formatted summary with unique counts.
execute: async (args) => { try { const [list1, list2] = await Promise.all([ anilistClient.fetchList(args.user1, args.type, "PLANNING"), anilistClient.fetchList(args.user2, args.type, "PLANNING"), ]); const ids1 = new Set(list1.map((e) => e.media.id)); const ids2 = new Set(list2.map((e) => e.media.id)); const entryMap = new Map<number, AniListMediaListEntry>(); for (const e of [...list1, ...list2]) entryMap.set(e.media.id, e); // Shared titles const sharedIds = [...ids1].filter((id) => ids2.has(id)); const shared = sharedIds .map((id) => entryMap.get(id)) .filter((e): e is AniListMediaListEntry => e !== undefined) .sort((a, b) => (b.media.meanScore ?? 0) - (a.media.meanScore ?? 0)); const lines = [ `# Shared Planning: ${args.user1} & ${args.user2}`, `${args.user1}: ${list1.length} | ${args.user2}: ${list2.length} | Overlap: ${shared.length}`, "", ]; if (shared.length === 0) { lines.push("No titles in common on both planning lists."); } else { lines.push("Both planning to watch:"); const show = shared.slice(0, args.limit); for (let i = 0; i < show.length; i++) { const e = show[i]; const title = getTitle(e.media.title); const score = e.media.meanScore ? ` (${(e.media.meanScore / 10).toFixed(1)}/10)` : ""; lines.push(`${i + 1}. ${title}${score}`); } if (shared.length > args.limit) { lines.push(`...and ${shared.length - args.limit} more`); } } // Unique counts const only1 = list1.filter((e) => !ids2.has(e.media.id)).length; const only2 = list2.filter((e) => !ids1.has(e.media.id)).length; if (only1 > 0 || only2 > 0) { lines.push(""); if (only1 > 0) lines.push(`Only ${args.user1}: ${only1} titles`); if (only2 > 0) lines.push(`Only ${args.user2}: ${only2} titles`); } return lines.join("\n"); } catch (error) { return throwToolError(error, "comparing planning lists"); } - src/tools/social.ts:368-441 (registration)Registration of the 'anilist_shared_planning' tool via server.addTool() inside registerSocialTools().
// === Shared Planning === server.addTool({ name: "anilist_shared_planning", description: "Find titles on both users' planning lists. " + "Use when two users want to see what they're both planning to watch or read. " + "Shows overlap and unique titles.", parameters: SharedPlanningInputSchema, annotations: { title: "Shared Planning", readOnlyHint: true, destructiveHint: false, openWorldHint: true, }, execute: async (args) => { try { const [list1, list2] = await Promise.all([ anilistClient.fetchList(args.user1, args.type, "PLANNING"), anilistClient.fetchList(args.user2, args.type, "PLANNING"), ]); const ids1 = new Set(list1.map((e) => e.media.id)); const ids2 = new Set(list2.map((e) => e.media.id)); const entryMap = new Map<number, AniListMediaListEntry>(); for (const e of [...list1, ...list2]) entryMap.set(e.media.id, e); // Shared titles const sharedIds = [...ids1].filter((id) => ids2.has(id)); const shared = sharedIds .map((id) => entryMap.get(id)) .filter((e): e is AniListMediaListEntry => e !== undefined) .sort((a, b) => (b.media.meanScore ?? 0) - (a.media.meanScore ?? 0)); const lines = [ `# Shared Planning: ${args.user1} & ${args.user2}`, `${args.user1}: ${list1.length} | ${args.user2}: ${list2.length} | Overlap: ${shared.length}`, "", ]; if (shared.length === 0) { lines.push("No titles in common on both planning lists."); } else { lines.push("Both planning to watch:"); const show = shared.slice(0, args.limit); for (let i = 0; i < show.length; i++) { const e = show[i]; const title = getTitle(e.media.title); const score = e.media.meanScore ? ` (${(e.media.meanScore / 10).toFixed(1)}/10)` : ""; lines.push(`${i + 1}. ${title}${score}`); } if (shared.length > args.limit) { lines.push(`...and ${shared.length - args.limit} more`); } } // Unique counts const only1 = list1.filter((e) => !ids2.has(e.media.id)).length; const only2 = list2.filter((e) => !ids1.has(e.media.id)).length; if (only1 > 0 || only2 > 0) { lines.push(""); if (only1 > 0) lines.push(`Only ${args.user1}: ${only1} titles`); if (only2 > 0) lines.push(`Only ${args.user2}: ${only2} titles`); } return lines.join("\n"); } catch (error) { return throwToolError(error, "comparing planning lists"); } }, }); - src/schemas.ts:1155-1169 (schema)Zod schema SharedPlanningInputSchema defining inputs: user1, user2, type (ANIME/MANGA default ANIME), limit (1-50 default 25).
export const SharedPlanningInputSchema = z.object({ user1: usernameSchema.describe("First AniList username"), user2: usernameSchema.describe("Second AniList username"), type: z .enum(["ANIME", "MANGA"]) .default("ANIME") .describe("Compare anime or manga planning lists"), limit: z .number() .int() .min(1) .max(50) .default(25) .describe("Max entries to show (default 25, max 50)"), }); - src/index.ts:61-61 (registration)Registration call registerSocialTools(server) invoked from the main server setup.
registerSocialTools(server); - src/tools/social.ts:49-49 (helper)registerSocialTools function declaration that registers this and other social tools.
export function registerSocialTools(server: FastMCP): void {