ani-mcp 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Most AniList integrations mirror the API 1:1. ani-mcp adds an intelligence layer
11
11
  - **Compatibility** - compare taste between two users
12
12
  - **Year in review** - your watching/reading stats wrapped up
13
13
 
14
- Plus the essentials: search, details, trending, seasonal browsing, list management, and community recommendations.
14
+ Plus the essentials: search, details, trending, seasonal browsing, list management, and community recommendations. All search and browse tools support pagination for browsing beyond the first page of results.
15
15
 
16
16
  ## Install
17
17
 
@@ -38,8 +38,11 @@ Works with any MCP-compatible client.
38
38
  | Variable | Required | Description |
39
39
  | --- | --- | --- |
40
40
  | `ANILIST_USERNAME` | No | Default username for list and stats tools. Can also pass per-call. |
41
- | `ANILIST_TOKEN` | No | AniList OAuth token. Enables authenticated queries. |
41
+ | `ANILIST_TOKEN` | No | AniList OAuth token. Required for write operations and private lists. |
42
42
  | `DEBUG` | No | Set to `true` for debug logging to stderr. |
43
+ | `MCP_TRANSPORT` | No | Set to `http` for HTTP Stream transport. Default: stdio. |
44
+ | `MCP_PORT` | No | Port for HTTP transport. Default: `3000`. |
45
+ | `MCP_HOST` | No | Host for HTTP transport. Default: `localhost`. |
43
46
 
44
47
  ## Tools
45
48
 
@@ -69,15 +72,37 @@ Works with any MCP-compatible client.
69
72
  | `anilist_pick` | Personalized "what to watch next" based on taste and mood |
70
73
  | `anilist_compare` | Compare taste compatibility between two users |
71
74
  | `anilist_wrapped` | Year-in-review summary |
75
+ | `anilist_explain` | "Why would I like this?" - score a title against your taste profile |
76
+ | `anilist_similar` | Find titles similar to a given anime or manga |
72
77
 
73
78
  ### Info
74
79
 
75
80
  | Tool | Description |
76
81
  | --- | --- |
77
82
  | `anilist_staff` | Staff credits and voice actors for a title |
83
+ | `anilist_staff_search` | Search for a person by name and see all their works |
84
+ | `anilist_studio_search` | Search for a studio and see their productions |
78
85
  | `anilist_schedule` | Airing schedule and next episode countdown |
79
86
  | `anilist_characters` | Search characters by name with appearances and VAs |
80
87
 
88
+ ### Write (requires `ANILIST_TOKEN`)
89
+
90
+ | Tool | Description |
91
+ | --- | --- |
92
+ | `anilist_update_progress` | Update episode or chapter progress |
93
+ | `anilist_add_to_list` | Add a title to your list with a status |
94
+ | `anilist_rate` | Score a title (0-10) |
95
+ | `anilist_delete_from_list` | Remove an entry from your list |
96
+
97
+ ## Docker
98
+
99
+ ```sh
100
+ docker build -t ani-mcp .
101
+ docker run -e ANILIST_USERNAME=your_username ani-mcp
102
+ ```
103
+
104
+ Runs on port 3000 with HTTP Stream transport by default.
105
+
81
106
  ## Build from Source
82
107
 
83
108
  ```sh
@@ -26,5 +26,13 @@ export declare const STAFF_QUERY = "\n query MediaStaff($id: Int, $search: Stri
26
26
  export declare const AIRING_SCHEDULE_QUERY = "\n query AiringSchedule($id: Int, $search: String, $notYetAired: Boolean) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n status\n episodes\n nextAiringEpisode {\n episode\n airingAt\n timeUntilAiring\n }\n airingSchedule(notYetAired: $notYetAired, perPage: 10) {\n nodes {\n episode\n airingAt\n timeUntilAiring\n }\n }\n siteUrl\n }\n }\n";
27
27
  /** Search for characters by name */
28
28
  export declare const CHARACTER_SEARCH_QUERY = "\n query CharacterSearch($search: String!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n characters(search: $search, sort: FAVOURITES_DESC) {\n id\n name { full native alternative }\n image { medium }\n favourites\n siteUrl\n media(sort: POPULARITY_DESC, perPage: 5) {\n edges {\n characterRole\n node {\n id\n title { romaji english }\n format\n type\n siteUrl\n }\n voiceActors(language: JAPANESE) {\n id\n name { full }\n siteUrl\n }\n }\n }\n }\n }\n }\n";
29
+ /** Create or update a list entry */
30
+ export declare const SAVE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation SaveMediaListEntry(\n $mediaId: Int\n $status: MediaListStatus\n $score: Float\n $progress: Int\n $notes: String\n $private: Boolean\n ) {\n SaveMediaListEntry(\n mediaId: $mediaId\n status: $status\n score: $score\n progress: $progress\n notes: $notes\n private: $private\n ) {\n id\n mediaId\n status\n score(format: POINT_10)\n progress\n }\n }\n";
31
+ /** Remove a list entry */
32
+ export declare const DELETE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation DeleteMediaListEntry($id: Int!) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n";
29
33
  /** User's anime/manga list, grouped by status. Omit $status to get all lists. */
30
34
  export declare const USER_LIST_QUERY = "\n query UserMediaList(\n $userName: String!\n $type: MediaType\n $status: MediaListStatus\n $sort: [MediaListSort]\n ) {\n MediaListCollection(\n userName: $userName\n type: $type\n status: $status\n sort: $sort\n ) {\n lists {\n name\n status\n entries {\n id\n score(format: POINT_10) # normalize to 1-10 scale regardless of user's profile setting\n progress\n status\n updatedAt\n startedAt { year month day }\n completedAt { year month day }\n notes\n media {\n ...MediaFields\n }\n }\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
35
+ /** Search for staff by name with their top works */
36
+ export declare const STAFF_SEARCH_QUERY = "\n query StaffSearch($search: String!, $page: Int, $perPage: Int, $mediaPerPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n staff(search: $search, sort: SEARCH_MATCH) {\n id\n name { full native }\n primaryOccupations\n siteUrl\n staffMedia(sort: POPULARITY_DESC, perPage: $mediaPerPage) {\n edges {\n staffRole\n node {\n id\n title { romaji english }\n format\n type\n meanScore\n siteUrl\n }\n }\n }\n }\n }\n }\n";
37
+ /** Search for a studio by name with their productions */
38
+ export declare const STUDIO_SEARCH_QUERY = "\n query StudioSearch($search: String!, $perPage: Int) {\n Studio(search: $search, sort: SEARCH_MATCH) {\n id\n name\n isAnimationStudio\n siteUrl\n media(sort: POPULARITY_DESC, perPage: $perPage) {\n edges {\n isMainStudio\n node {\n id\n title { romaji english }\n format\n type\n status\n meanScore\n siteUrl\n }\n }\n }\n }\n }\n";
@@ -364,6 +364,40 @@ export const CHARACTER_SEARCH_QUERY = `
364
364
  }
365
365
  }
366
366
  `;
367
+ /** Create or update a list entry */
368
+ export const SAVE_MEDIA_LIST_ENTRY_MUTATION = `
369
+ mutation SaveMediaListEntry(
370
+ $mediaId: Int
371
+ $status: MediaListStatus
372
+ $score: Float
373
+ $progress: Int
374
+ $notes: String
375
+ $private: Boolean
376
+ ) {
377
+ SaveMediaListEntry(
378
+ mediaId: $mediaId
379
+ status: $status
380
+ score: $score
381
+ progress: $progress
382
+ notes: $notes
383
+ private: $private
384
+ ) {
385
+ id
386
+ mediaId
387
+ status
388
+ score(format: POINT_10)
389
+ progress
390
+ }
391
+ }
392
+ `;
393
+ /** Remove a list entry */
394
+ export const DELETE_MEDIA_LIST_ENTRY_MUTATION = `
395
+ mutation DeleteMediaListEntry($id: Int!) {
396
+ DeleteMediaListEntry(id: $id) {
397
+ deleted
398
+ }
399
+ }
400
+ `;
367
401
  /** User's anime/manga list, grouped by status. Omit $status to get all lists. */
368
402
  export const USER_LIST_QUERY = `
369
403
  query UserMediaList(
@@ -399,3 +433,55 @@ export const USER_LIST_QUERY = `
399
433
  }
400
434
  ${MEDIA_FRAGMENT}
401
435
  `;
436
+ /** Search for staff by name with their top works */
437
+ export const STAFF_SEARCH_QUERY = `
438
+ query StaffSearch($search: String!, $page: Int, $perPage: Int, $mediaPerPage: Int) {
439
+ Page(page: $page, perPage: $perPage) {
440
+ pageInfo { total hasNextPage }
441
+ staff(search: $search, sort: SEARCH_MATCH) {
442
+ id
443
+ name { full native }
444
+ primaryOccupations
445
+ siteUrl
446
+ staffMedia(sort: POPULARITY_DESC, perPage: $mediaPerPage) {
447
+ edges {
448
+ staffRole
449
+ node {
450
+ id
451
+ title { romaji english }
452
+ format
453
+ type
454
+ meanScore
455
+ siteUrl
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ `;
463
+ /** Search for a studio by name with their productions */
464
+ export const STUDIO_SEARCH_QUERY = `
465
+ query StudioSearch($search: String!, $perPage: Int) {
466
+ Studio(search: $search, sort: SEARCH_MATCH) {
467
+ id
468
+ name
469
+ isAnimationStudio
470
+ siteUrl
471
+ media(sort: POPULARITY_DESC, perPage: $perPage) {
472
+ edges {
473
+ isMainStudio
474
+ node {
475
+ id
476
+ title { romaji english }
477
+ format
478
+ type
479
+ status
480
+ meanScore
481
+ siteUrl
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ `;
@@ -8,5 +8,25 @@ export interface MatchResult {
8
8
  reasons: string[];
9
9
  moodFit: string | null;
10
10
  }
11
+ export interface ScoreBreakdown {
12
+ genreScore: number;
13
+ tagScore: number;
14
+ communityScore: number;
15
+ popularityFactor: number;
16
+ moodMultiplier: number;
17
+ finalScore: number;
18
+ }
19
+ export interface ExplainResult {
20
+ media: AniListMedia;
21
+ breakdown: ScoreBreakdown;
22
+ matchedGenres: string[];
23
+ unmatchedGenres: string[];
24
+ matchedTags: string[];
25
+ unmatchedTags: string[];
26
+ reasons: string[];
27
+ moodFit: string | null;
28
+ }
11
29
  /** Score and rank candidates against a user's taste profile */
12
30
  export declare function matchCandidates(candidates: AniListMedia[], profile: TasteProfile, mood?: MoodModifiers): MatchResult[];
31
+ /** Score a single title against a taste profile with a detailed breakdown */
32
+ export declare function explainMatch(media: AniListMedia, profile: TasteProfile, mood?: MoodModifiers): ExplainResult;
@@ -139,6 +139,67 @@ function applyMood(media, mood, reasons) {
139
139
  }
140
140
  return null;
141
141
  }
142
+ // === Single-Title Explain ===
143
+ /** Score a single title against a taste profile with a detailed breakdown */
144
+ export function explainMatch(media, profile, mood) {
145
+ const genreWeights = toWeightMap(profile.genres);
146
+ const tagWeights = toWeightMap(profile.tags);
147
+ const maxGenreWeight = profile.genres[0]?.weight ?? 1;
148
+ const maxTagWeight = profile.tags[0]?.weight ?? 1;
149
+ const reasons = [];
150
+ // Genre affinity with matched/unmatched tracking
151
+ const genreScore = computeGenreAffinity(media, genreWeights, maxGenreWeight, reasons);
152
+ const matchedGenres = media.genres.filter((g) => genreWeights.has(g));
153
+ const unmatchedGenres = media.genres.filter((g) => !genreWeights.has(g));
154
+ // Tag affinity with matched/unmatched tracking
155
+ const tagScore = computeTagAffinity(media, tagWeights, maxTagWeight, reasons);
156
+ const nonSpoilerTags = media.tags.filter((t) => !t.isMediaSpoiler);
157
+ const matchedTags = nonSpoilerTags
158
+ .filter((t) => tagWeights.has(t.name))
159
+ .map((t) => t.name);
160
+ const unmatchedTags = nonSpoilerTags
161
+ .filter((t) => !tagWeights.has(t.name))
162
+ .map((t) => t.name);
163
+ const communityScore = (media.meanScore ?? 70) / 100;
164
+ const popFactor = popularityDiversityFactor(media.popularity);
165
+ let finalScore = genreScore * GENRE_WEIGHT +
166
+ tagScore * TAG_WEIGHT +
167
+ communityScore * COMMUNITY_WEIGHT;
168
+ finalScore *= popFactor;
169
+ // Mood modifier
170
+ let moodMultiplier = 1;
171
+ const moodFit = mood ? applyMood(media, mood, reasons) : null;
172
+ if (moodFit === "boost") {
173
+ moodMultiplier = MOOD_BOOST;
174
+ finalScore *= MOOD_BOOST;
175
+ }
176
+ if (moodFit === "penalty") {
177
+ moodMultiplier = MOOD_PENALTY;
178
+ finalScore *= MOOD_PENALTY;
179
+ }
180
+ return {
181
+ media,
182
+ breakdown: {
183
+ genreScore,
184
+ tagScore,
185
+ communityScore,
186
+ popularityFactor: popFactor,
187
+ moodMultiplier,
188
+ // Scale 0-1 to 0-100
189
+ finalScore: Math.round(Math.min(1, finalScore) * 100),
190
+ },
191
+ matchedGenres,
192
+ unmatchedGenres,
193
+ matchedTags,
194
+ unmatchedTags,
195
+ reasons,
196
+ moodFit: moodFit
197
+ ? moodFit === "boost"
198
+ ? "Strong mood match"
199
+ : "Weak mood fit"
200
+ : null,
201
+ };
202
+ }
142
203
  // === Helpers ===
143
204
  /** Convert WeightedItem[] to a Map for fast lookup */
144
205
  function toWeightMap(items) {
@@ -15,3 +15,8 @@ export declare function parseMood(mood: string): MoodModifiers;
15
15
  export declare function hasMoodMatch(mood: string): boolean;
16
16
  /** List all recognized mood keywords */
17
17
  export declare function getMoodKeywords(): string[];
18
+ /** Suggest mood keywords that fit the current anime season */
19
+ export declare function seasonalMoodSuggestions(): {
20
+ season: string;
21
+ moods: string[];
22
+ };
@@ -66,6 +66,24 @@ const BASE_MOOD_RULES = {
66
66
  boost: ["Psychological", "Avant Garde", "Surreal", "Experimental"],
67
67
  penalize: ["Shounen", "Sports"],
68
68
  },
69
+ nostalgic: {
70
+ boost: [
71
+ "Coming of Age",
72
+ "Drama",
73
+ "Slice of Life",
74
+ "School",
75
+ "Ensemble Cast",
76
+ ],
77
+ penalize: ["Isekai", "Mecha"],
78
+ },
79
+ artistic: {
80
+ boost: ["Avant Garde", "Drama", "Music", "Surreal", "Visual Arts"],
81
+ penalize: ["Shounen", "Ecchi"],
82
+ },
83
+ competitive: {
84
+ boost: ["Sports", "Strategy Game", "Shounen", "Tournament", "Martial Arts"],
85
+ penalize: ["Slice of Life", "Iyashikei"],
86
+ },
69
87
  };
70
88
  // Synonyms that resolve to a base keyword's rules
71
89
  const MOOD_SYNONYMS = {
@@ -117,6 +135,17 @@ const MOOD_SYNONYMS = {
117
135
  // trippy
118
136
  surreal: "trippy",
119
137
  experimental: "trippy",
138
+ // nostalgic
139
+ retro: "nostalgic",
140
+ throwback: "nostalgic",
141
+ classic: "nostalgic",
142
+ // artistic
143
+ artsy: "artistic",
144
+ beautiful: "artistic",
145
+ aesthetic: "artistic",
146
+ // competitive
147
+ rivalry: "competitive",
148
+ tournament: "competitive",
120
149
  };
121
150
  // Merge base rules and synonyms into a single lookup
122
151
  const MOOD_RULES = { ...BASE_MOOD_RULES };
@@ -163,3 +192,22 @@ export function hasMoodMatch(mood) {
163
192
  export function getMoodKeywords() {
164
193
  return Object.keys(MOOD_RULES);
165
194
  }
195
+ // === Seasonal Suggestions ===
196
+ const SEASONAL_MOODS = {
197
+ WINTER: ["cozy", "dark", "nostalgic", "brainy"],
198
+ SPRING: ["romantic", "wholesome", "chill", "artistic"],
199
+ SUMMER: ["hype", "action", "epic", "competitive"],
200
+ FALL: ["mystery", "scary", "dark", "intense"],
201
+ };
202
+ /** Suggest mood keywords that fit the current anime season */
203
+ export function seasonalMoodSuggestions() {
204
+ const month = new Date().getMonth() + 1;
205
+ const season = month <= 3
206
+ ? "WINTER"
207
+ : month <= 6
208
+ ? "SPRING"
209
+ : month <= 9
210
+ ? "SUMMER"
211
+ : "FALL";
212
+ return { season, moods: SEASONAL_MOODS[season] };
213
+ }
@@ -0,0 +1,9 @@
1
+ /** Ranks candidates by content similarity to a source title */
2
+ import type { AniListMedia } from "../types.js";
3
+ export interface SimilarResult {
4
+ media: AniListMedia;
5
+ similarityScore: number;
6
+ reasons: string[];
7
+ }
8
+ /** Rank candidates by genre/tag overlap and community recommendation strength */
9
+ export declare function rankSimilar(source: AniListMedia, candidates: AniListMedia[], recRatings: Map<number, number>): SimilarResult[];
@@ -0,0 +1,50 @@
1
+ /** Ranks candidates by content similarity to a source title */
2
+ // === Constants ===
3
+ const GENRE_WEIGHT = 0.4;
4
+ const TAG_WEIGHT = 0.3;
5
+ const REC_WEIGHT = 0.3;
6
+ // === Similarity Engine ===
7
+ /** Rank candidates by genre/tag overlap and community recommendation strength */
8
+ export function rankSimilar(source, candidates, recRatings) {
9
+ if (candidates.length === 0)
10
+ return [];
11
+ // Normalize rec ratings to 0-1
12
+ const maxRating = Math.max(1, ...recRatings.values());
13
+ const sourceGenres = new Set(source.genres);
14
+ const sourceTags = new Set(source.tags.filter((t) => !t.isMediaSpoiler).map((t) => t.name));
15
+ const results = [];
16
+ for (const candidate of candidates) {
17
+ const reasons = [];
18
+ // Genre overlap: Jaccard coefficient
19
+ const candidateGenres = new Set(candidate.genres);
20
+ const genreIntersection = [...sourceGenres].filter((g) => candidateGenres.has(g));
21
+ const genreUnion = new Set([...sourceGenres, ...candidateGenres]);
22
+ const genreOverlap = genreUnion.size > 0 ? genreIntersection.length / genreUnion.size : 0;
23
+ if (genreIntersection.length > 0) {
24
+ reasons.push(`Shares genres: ${genreIntersection.join(", ")}`);
25
+ }
26
+ // Tag overlap: Jaccard on non-spoiler tags
27
+ const candidateTags = new Set(candidate.tags.filter((t) => !t.isMediaSpoiler).map((t) => t.name));
28
+ const tagIntersection = [...sourceTags].filter((t) => candidateTags.has(t));
29
+ const tagUnion = new Set([...sourceTags, ...candidateTags]);
30
+ const tagOverlap = tagUnion.size > 0 ? tagIntersection.length / tagUnion.size : 0;
31
+ if (tagIntersection.length > 0) {
32
+ reasons.push(`Similar themes: ${tagIntersection.slice(0, 3).join(", ")}`);
33
+ }
34
+ // Community recommendation rating
35
+ const rating = recRatings.get(candidate.id) ?? 0;
36
+ const recBoost = rating > 0 ? rating / maxRating : 0;
37
+ if (rating > 0) {
38
+ reasons.push(`Recommended by community (${rating > 0 ? "+" : ""}${rating})`);
39
+ }
40
+ const score = genreOverlap * GENRE_WEIGHT +
41
+ tagOverlap * TAG_WEIGHT +
42
+ recBoost * REC_WEIGHT;
43
+ results.push({
44
+ media: candidate,
45
+ similarityScore: Math.round(Math.min(1, score) * 100),
46
+ reasons,
47
+ });
48
+ }
49
+ return results.sort((a, b) => b.similarityScore - a.similarityScore);
50
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { registerListTools } from "./tools/lists.js";
7
7
  import { registerRecommendTools } from "./tools/recommend.js";
8
8
  import { registerDiscoverTools } from "./tools/discover.js";
9
9
  import { registerInfoTools } from "./tools/info.js";
10
+ import { registerWriteTools } from "./tools/write.js";
10
11
  // Both vars are optional - warn on missing so operators know what's available
11
12
  if (!process.env.ANILIST_USERNAME) {
12
13
  console.warn("ANILIST_USERNAME not set - tools will require a username argument.");
@@ -23,4 +24,18 @@ registerListTools(server);
23
24
  registerRecommendTools(server);
24
25
  registerDiscoverTools(server);
25
26
  registerInfoTools(server);
26
- server.start({ transportType: "stdio" });
27
+ registerWriteTools(server);
28
+ // === Transport ===
29
+ const transport = process.env.MCP_TRANSPORT === "http" ? "httpStream" : "stdio";
30
+ if (transport === "httpStream") {
31
+ const port = Number(process.env.MCP_PORT) || 3000;
32
+ const host = process.env.MCP_HOST || "localhost";
33
+ console.error(`Listening on http://${host}:${port}/mcp`);
34
+ server.start({
35
+ transportType: "httpStream",
36
+ httpStream: { port, host },
37
+ });
38
+ }
39
+ else {
40
+ server.start({ transportType: "stdio" });
41
+ }
package/dist/schemas.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare const SearchInputSchema: z.ZodObject<{
21
21
  }>>;
22
22
  isAdult: z.ZodDefault<z.ZodBoolean>;
23
23
  limit: z.ZodDefault<z.ZodNumber>;
24
+ page: z.ZodDefault<z.ZodNumber>;
24
25
  }, z.core.$strip>;
25
26
  export type SearchInput = z.infer<typeof SearchInputSchema>;
26
27
  /** Input for looking up a single anime or manga by ID or title */
@@ -51,6 +52,7 @@ export declare const ListInputSchema: z.ZodObject<{
51
52
  PROGRESS: "PROGRESS";
52
53
  }>>;
53
54
  limit: z.ZodDefault<z.ZodNumber>;
55
+ page: z.ZodDefault<z.ZodNumber>;
54
56
  }, z.core.$strip>;
55
57
  export type ListInput = z.infer<typeof ListInputSchema>;
56
58
  /** Input for generating a taste profile summary */
@@ -101,6 +103,7 @@ export declare const SeasonalInputSchema: z.ZodObject<{
101
103
  }>>;
102
104
  isAdult: z.ZodDefault<z.ZodBoolean>;
103
105
  limit: z.ZodDefault<z.ZodNumber>;
106
+ page: z.ZodDefault<z.ZodNumber>;
104
107
  }, z.core.$strip>;
105
108
  export type SeasonalInput = z.infer<typeof SeasonalInputSchema>;
106
109
  /** Input for fetching user statistics */
@@ -127,6 +130,7 @@ export declare const TrendingInputSchema: z.ZodObject<{
127
130
  }>>;
128
131
  isAdult: z.ZodDefault<z.ZodBoolean>;
129
132
  limit: z.ZodDefault<z.ZodNumber>;
133
+ page: z.ZodDefault<z.ZodNumber>;
130
134
  }, z.core.$strip>;
131
135
  export type TrendingInput = z.infer<typeof TrendingInputSchema>;
132
136
  /** Input for browsing by genre */
@@ -161,6 +165,7 @@ export declare const GenreBrowseInputSchema: z.ZodObject<{
161
165
  }>>;
162
166
  isAdult: z.ZodDefault<z.ZodBoolean>;
163
167
  limit: z.ZodDefault<z.ZodNumber>;
168
+ page: z.ZodDefault<z.ZodNumber>;
164
169
  }, z.core.$strip>;
165
170
  export type GenreBrowseInput = z.infer<typeof GenreBrowseInputSchema>;
166
171
  /** Input for staff/VA credits lookup */
@@ -179,6 +184,7 @@ export type ScheduleInput = z.infer<typeof ScheduleInputSchema>;
179
184
  export declare const CharacterSearchInputSchema: z.ZodObject<{
180
185
  query: z.ZodString;
181
186
  limit: z.ZodDefault<z.ZodNumber>;
187
+ page: z.ZodDefault<z.ZodNumber>;
182
188
  }, z.core.$strip>;
183
189
  export type CharacterSearchInput = z.infer<typeof CharacterSearchInputSchema>;
184
190
  /** Input for community recommendations for a specific title */
@@ -188,3 +194,72 @@ export declare const RecommendationsInputSchema: z.ZodObject<{
188
194
  limit: z.ZodDefault<z.ZodNumber>;
189
195
  }, z.core.$strip>;
190
196
  export type RecommendationsInput = z.infer<typeof RecommendationsInputSchema>;
197
+ /** Input for updating episode or chapter progress */
198
+ export declare const UpdateProgressInputSchema: z.ZodObject<{
199
+ mediaId: z.ZodNumber;
200
+ progress: z.ZodNumber;
201
+ status: z.ZodOptional<z.ZodEnum<{
202
+ CURRENT: "CURRENT";
203
+ COMPLETED: "COMPLETED";
204
+ DROPPED: "DROPPED";
205
+ PAUSED: "PAUSED";
206
+ REPEATING: "REPEATING";
207
+ }>>;
208
+ }, z.core.$strip>;
209
+ export type UpdateProgressInput = z.infer<typeof UpdateProgressInputSchema>;
210
+ /** Input for adding a title to the user's list */
211
+ export declare const AddToListInputSchema: z.ZodObject<{
212
+ mediaId: z.ZodNumber;
213
+ status: z.ZodEnum<{
214
+ CURRENT: "CURRENT";
215
+ COMPLETED: "COMPLETED";
216
+ PLANNING: "PLANNING";
217
+ DROPPED: "DROPPED";
218
+ PAUSED: "PAUSED";
219
+ REPEATING: "REPEATING";
220
+ }>;
221
+ score: z.ZodOptional<z.ZodNumber>;
222
+ }, z.core.$strip>;
223
+ export type AddToListInput = z.infer<typeof AddToListInputSchema>;
224
+ /** Input for rating a title */
225
+ export declare const RateInputSchema: z.ZodObject<{
226
+ mediaId: z.ZodNumber;
227
+ score: z.ZodNumber;
228
+ }, z.core.$strip>;
229
+ export type RateInput = z.infer<typeof RateInputSchema>;
230
+ /** Input for removing a title from the list */
231
+ export declare const DeleteFromListInputSchema: z.ZodObject<{
232
+ entryId: z.ZodNumber;
233
+ }, z.core.$strip>;
234
+ /** Input for scoring a title against a user's taste profile */
235
+ export declare const ExplainInputSchema: z.ZodObject<{
236
+ mediaId: z.ZodNumber;
237
+ username: z.ZodOptional<z.ZodString>;
238
+ type: z.ZodDefault<z.ZodEnum<{
239
+ ANIME: "ANIME";
240
+ MANGA: "MANGA";
241
+ BOTH: "BOTH";
242
+ }>>;
243
+ mood: z.ZodOptional<z.ZodString>;
244
+ }, z.core.$strip>;
245
+ export type ExplainInput = z.infer<typeof ExplainInputSchema>;
246
+ /** Input for finding titles similar to a specific anime or manga */
247
+ export declare const SimilarInputSchema: z.ZodObject<{
248
+ mediaId: z.ZodNumber;
249
+ limit: z.ZodDefault<z.ZodNumber>;
250
+ }, z.core.$strip>;
251
+ export type SimilarInput = z.infer<typeof SimilarInputSchema>;
252
+ /** Input for searching staff/people by name */
253
+ export declare const StaffSearchInputSchema: z.ZodObject<{
254
+ query: z.ZodString;
255
+ limit: z.ZodDefault<z.ZodNumber>;
256
+ mediaLimit: z.ZodDefault<z.ZodNumber>;
257
+ page: z.ZodDefault<z.ZodNumber>;
258
+ }, z.core.$strip>;
259
+ export type StaffSearchInput = z.infer<typeof StaffSearchInputSchema>;
260
+ /** Input for searching studios by name */
261
+ export declare const StudioSearchInputSchema: z.ZodObject<{
262
+ query: z.ZodString;
263
+ limit: z.ZodDefault<z.ZodNumber>;
264
+ }, z.core.$strip>;
265
+ export type StudioSearchInput = z.infer<typeof StudioSearchInputSchema>;