ani-mcp 0.4.0 → 0.5.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 +1 -0
- package/dist/api/client.js +2 -2
- package/dist/api/queries.d.ts +10 -8
- package/dist/api/queries.js +28 -0
- package/dist/engine/franchise.d.ts +36 -0
- package/dist/engine/franchise.js +87 -0
- package/dist/engine/mood.d.ts +6 -0
- package/dist/engine/mood.js +64 -2
- package/dist/index.js +1 -1
- package/dist/schemas.d.ts +46 -0
- package/dist/schemas.js +87 -11
- package/dist/tools/discover.js +4 -4
- package/dist/tools/info.js +9 -7
- package/dist/tools/lists.js +4 -3
- package/dist/tools/recommend.js +330 -20
- package/dist/tools/search.js +9 -25
- package/dist/tools/social.js +11 -9
- package/dist/tools/write.js +6 -4
- package/dist/types.d.ts +32 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +17 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# ani-mcp
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/ani-mcp)
|
|
8
|
+
[](https://www.npmjs.com/package/ani-mcp)
|
|
8
9
|
[](https://github.com/gavxm/ani-mcp/actions/workflows/ci.yml)
|
|
9
10
|
[](https://opensource.org/licenses/MIT)
|
|
10
11
|
[](https://nodejs.org)
|
package/dist/api/client.js
CHANGED
|
@@ -9,8 +9,8 @@ import pRetry, { AbortError } from "p-retry";
|
|
|
9
9
|
import pThrottle from "p-throttle";
|
|
10
10
|
import { USER_LIST_QUERY } from "./queries.js";
|
|
11
11
|
const ANILIST_API_URL = process.env.ANILIST_API_URL || "https://graphql.anilist.co";
|
|
12
|
-
//
|
|
13
|
-
const RATE_LIMIT_PER_MINUTE = 85;
|
|
12
|
+
// No rate limit needed when API is mocked in tests
|
|
13
|
+
const RATE_LIMIT_PER_MINUTE = process.env.VITEST ? 10_000 : 85;
|
|
14
14
|
const MAX_RETRIES = 3;
|
|
15
15
|
// Hard timeout per fetch attempt (retries get their own timeout)
|
|
16
16
|
const FETCH_TIMEOUT_MS = 15_000;
|
package/dist/api/queries.d.ts
CHANGED
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
* if the AniList schema changes.
|
|
6
6
|
*/
|
|
7
7
|
/** Paginated search with optional genre, year, and format filters */
|
|
8
|
-
export declare const SEARCH_MEDIA_QUERY = "\n query SearchMedia(\n $search: String!\n $type: MediaType\n $genre: [String]\n $year: Int\n $format: MediaFormat\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n total\n currentPage\n lastPage\n hasNextPage\n }\n media(\n search: $search\n type: $type\n genre_in: $genre\n startDate_year: $year\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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";
|
|
8
|
+
export declare const SEARCH_MEDIA_QUERY = "\n query SearchMedia(\n $search: String!\n $type: MediaType\n $genre: [String]\n $year: Int\n $format: MediaFormat\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n total\n currentPage\n lastPage\n hasNextPage\n }\n media(\n search: $search\n type: $type\n genre_in: $genre\n startDate_year: $year\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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 duration\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";
|
|
9
9
|
/** Full media lookup with relations and recommendations */
|
|
10
|
-
export declare const MEDIA_DETAILS_QUERY = "\n query MediaDetails($id: Int, $search: String) {\n Media(id: $id, search: $search) {\n ...MediaFields\n relations {\n edges {\n relationType\n node {\n id\n title { romaji english }\n format\n status\n type\n }\n }\n }\n recommendations(sort: RATING_DESC, perPage: 5) {\n nodes {\n rating\n mediaRecommendation {\n id\n title { romaji english }\n format\n meanScore\n genres\n siteUrl\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";
|
|
10
|
+
export declare const MEDIA_DETAILS_QUERY = "\n query MediaDetails($id: Int, $search: String) {\n Media(id: $id, search: $search) {\n ...MediaFields\n relations {\n edges {\n relationType\n node {\n id\n title { romaji english }\n format\n status\n type\n }\n }\n }\n recommendations(sort: RATING_DESC, perPage: 5) {\n nodes {\n rating\n mediaRecommendation {\n id\n title { romaji english }\n format\n meanScore\n genres\n siteUrl\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 duration\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";
|
|
11
11
|
/** Discover top-rated titles by genre without a search term */
|
|
12
|
-
export declare const DISCOVER_MEDIA_QUERY = "\n query DiscoverMedia(\n $type: MediaType\n $genre_in: [String]\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, genre_in: $genre_in, sort: $sort) {\n ...MediaFields\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";
|
|
12
|
+
export declare const DISCOVER_MEDIA_QUERY = "\n query DiscoverMedia(\n $type: MediaType\n $genre_in: [String]\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, genre_in: $genre_in, sort: $sort) {\n ...MediaFields\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 duration\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";
|
|
13
13
|
/** Browse anime by season and year */
|
|
14
|
-
export declare const SEASONAL_MEDIA_QUERY = "\n query SeasonalMedia(\n $season: MediaSeason\n $seasonYear: Int\n $type: MediaType\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total currentPage lastPage hasNextPage }\n media(\n season: $season\n seasonYear: $seasonYear\n type: $type\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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";
|
|
14
|
+
export declare const SEASONAL_MEDIA_QUERY = "\n query SeasonalMedia(\n $season: MediaSeason\n $seasonYear: Int\n $type: MediaType\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total currentPage lastPage hasNextPage }\n media(\n season: $season\n seasonYear: $seasonYear\n type: $type\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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 duration\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";
|
|
15
15
|
/** User profile statistics - watching/reading stats, genre/tag/score breakdowns */
|
|
16
16
|
export declare const USER_STATS_QUERY = "\n query UserStats($name: String!) {\n User(name: $name) {\n id\n name\n mediaListOptions {\n scoreFormat\n }\n statistics {\n anime {\n count\n meanScore\n minutesWatched\n episodesWatched\n genres(sort: COUNT_DESC, limit: 10) {\n genre\n count\n meanScore\n minutesWatched\n }\n scores(sort: MEAN_SCORE_DESC) {\n score\n count\n }\n formats(sort: COUNT_DESC) {\n format\n count\n }\n }\n manga {\n count\n meanScore\n chaptersRead\n volumesRead\n genres(sort: COUNT_DESC, limit: 10) {\n genre\n count\n meanScore\n chaptersRead\n }\n scores(sort: MEAN_SCORE_DESC) {\n score\n count\n }\n formats(sort: COUNT_DESC) {\n format\n count\n }\n }\n }\n }\n }\n";
|
|
17
17
|
/** Media recommendations for a given title */
|
|
18
|
-
export declare const RECOMMENDATIONS_QUERY = "\n query MediaRecommendations($id: Int, $search: String, $perPage: Int) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n recommendations(sort: RATING_DESC, perPage: $perPage) {\n nodes {\n rating\n mediaRecommendation {\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";
|
|
18
|
+
export declare const RECOMMENDATIONS_QUERY = "\n query MediaRecommendations($id: Int, $search: String, $perPage: Int) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n recommendations(sort: RATING_DESC, perPage: $perPage) {\n nodes {\n rating\n mediaRecommendation {\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 duration\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";
|
|
19
19
|
/** Trending anime or manga right now */
|
|
20
|
-
export declare const TRENDING_MEDIA_QUERY = "\n query TrendingMedia(\n $type: MediaType\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, isAdult: $isAdult, sort: TRENDING_DESC) {\n ...MediaFields\n trending\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";
|
|
20
|
+
export declare const TRENDING_MEDIA_QUERY = "\n query TrendingMedia(\n $type: MediaType\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, isAdult: $isAdult, sort: TRENDING_DESC) {\n ...MediaFields\n trending\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 duration\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";
|
|
21
21
|
/** Browse by genre without a search term, with optional filters */
|
|
22
|
-
export declare const GENRE_BROWSE_QUERY = "\n query GenreBrowse(\n $type: MediaType\n $genre_in: [String]\n $year: Int\n $status: MediaStatus\n $format: MediaFormat\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(\n type: $type\n genre_in: $genre_in\n startDate_year: $year\n status: $status\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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";
|
|
22
|
+
export declare const GENRE_BROWSE_QUERY = "\n query GenreBrowse(\n $type: MediaType\n $genre_in: [String]\n $year: Int\n $status: MediaStatus\n $format: MediaFormat\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(\n type: $type\n genre_in: $genre_in\n startDate_year: $year\n status: $status\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\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 duration\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";
|
|
23
23
|
/** Staff and voice actors for a media title */
|
|
24
24
|
export declare const STAFF_QUERY = "\n query MediaStaff($id: Int, $search: String) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n format\n siteUrl\n staff(sort: RELEVANCE, perPage: 15) {\n edges {\n role\n node {\n id\n name { full native }\n siteUrl\n }\n }\n }\n characters(sort: ROLE, perPage: 10) {\n edges {\n role\n node {\n id\n name { full native }\n siteUrl\n }\n voiceActors(language: JAPANESE) {\n id\n name { full native }\n siteUrl\n }\n }\n }\n }\n }\n";
|
|
25
25
|
/** Airing schedule for currently airing anime */
|
|
@@ -31,7 +31,7 @@ export declare const SAVE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation SaveMediaLis
|
|
|
31
31
|
/** Remove a list entry */
|
|
32
32
|
export declare const DELETE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation DeleteMediaListEntry($id: Int!) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n";
|
|
33
33
|
/** User's anime/manga list, grouped by status. Omit $status to get all lists. */
|
|
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 isCustomList\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";
|
|
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 isCustomList\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 duration\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
35
|
/** Search for staff by name with their top works */
|
|
36
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
37
|
/** Authenticated user info */
|
|
@@ -50,3 +50,5 @@ export declare const USER_PROFILE_QUERY = "\n query UserProfile($name: String)
|
|
|
50
50
|
export declare const MEDIA_REVIEWS_QUERY = "\n query MediaReviews($id: Int, $search: String, $page: Int, $perPage: Int, $sort: [ReviewSort]) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n reviews(page: $page, perPage: $perPage, sort: $sort) {\n pageInfo { total hasNextPage }\n nodes {\n id\n score\n summary\n body\n rating\n ratingAmount\n createdAt\n user { name siteUrl }\n }\n }\n }\n }\n";
|
|
51
51
|
/** Search for a studio by name with their productions */
|
|
52
52
|
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";
|
|
53
|
+
/** Batch-fetch relations for a list of media IDs */
|
|
54
|
+
export declare const BATCH_RELATIONS_QUERY = "\n query BatchRelations($ids: [Int]) {\n Page(perPage: 50) {\n media(id_in: $ids) {\n id\n title { romaji english }\n format\n status\n relations {\n edges {\n relationType\n node {\n id\n title { romaji english }\n format\n status\n type\n season\n seasonYear\n }\n }\n }\n }\n }\n }\n";
|
package/dist/api/queries.js
CHANGED
|
@@ -17,6 +17,7 @@ const MEDIA_FRAGMENT = `
|
|
|
17
17
|
format
|
|
18
18
|
status
|
|
19
19
|
episodes
|
|
20
|
+
duration
|
|
20
21
|
chapters
|
|
21
22
|
volumes
|
|
22
23
|
meanScore
|
|
@@ -650,3 +651,30 @@ export const STUDIO_SEARCH_QUERY = `
|
|
|
650
651
|
}
|
|
651
652
|
}
|
|
652
653
|
`;
|
|
654
|
+
/** Batch-fetch relations for a list of media IDs */
|
|
655
|
+
export const BATCH_RELATIONS_QUERY = `
|
|
656
|
+
query BatchRelations($ids: [Int]) {
|
|
657
|
+
Page(perPage: 50) {
|
|
658
|
+
media(id_in: $ids) {
|
|
659
|
+
id
|
|
660
|
+
title { romaji english }
|
|
661
|
+
format
|
|
662
|
+
status
|
|
663
|
+
relations {
|
|
664
|
+
edges {
|
|
665
|
+
relationType
|
|
666
|
+
node {
|
|
667
|
+
id
|
|
668
|
+
title { romaji english }
|
|
669
|
+
format
|
|
670
|
+
status
|
|
671
|
+
type
|
|
672
|
+
season
|
|
673
|
+
seasonYear
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Franchise graph traversal for watch order guidance. */
|
|
2
|
+
/** Single entry in a franchise watch order */
|
|
3
|
+
export interface FranchiseEntry {
|
|
4
|
+
id: number;
|
|
5
|
+
title: string;
|
|
6
|
+
format: string | null;
|
|
7
|
+
status: string | null;
|
|
8
|
+
type: "main" | "special";
|
|
9
|
+
}
|
|
10
|
+
/** Media entry with its relations */
|
|
11
|
+
export interface RelationNode {
|
|
12
|
+
id: number;
|
|
13
|
+
title: {
|
|
14
|
+
romaji: string | null;
|
|
15
|
+
english: string | null;
|
|
16
|
+
};
|
|
17
|
+
format: string | null;
|
|
18
|
+
status: string | null;
|
|
19
|
+
relations: {
|
|
20
|
+
edges: Array<{
|
|
21
|
+
relationType: string;
|
|
22
|
+
node: {
|
|
23
|
+
id: number;
|
|
24
|
+
title: {
|
|
25
|
+
romaji: string | null;
|
|
26
|
+
english: string | null;
|
|
27
|
+
};
|
|
28
|
+
format: string | null;
|
|
29
|
+
status: string | null;
|
|
30
|
+
type: string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Build a watch order by following SEQUEL edges from the franchise root */
|
|
36
|
+
export declare function buildWatchOrder(startId: number, relationsMap: Map<number, RelationNode>, includeSpecials: boolean): FranchiseEntry[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Franchise graph traversal for watch order guidance. */
|
|
2
|
+
// Main formats in a franchise timeline
|
|
3
|
+
const MAIN_FORMATS = new Set(["TV", "MOVIE", "ONA", "TV_SHORT"]);
|
|
4
|
+
// Max BFS depth to prevent runaway traversal
|
|
5
|
+
const MAX_DEPTH = 30;
|
|
6
|
+
/** Find the earliest entry by following PREQUEL edges backward */
|
|
7
|
+
function findRoot(startId, relationsMap) {
|
|
8
|
+
let current = startId;
|
|
9
|
+
const visited = new Set();
|
|
10
|
+
while (true) {
|
|
11
|
+
visited.add(current);
|
|
12
|
+
const node = relationsMap.get(current);
|
|
13
|
+
if (!node)
|
|
14
|
+
break;
|
|
15
|
+
const prequel = node.relations.edges.find((e) => e.relationType === "PREQUEL" && !visited.has(e.node.id));
|
|
16
|
+
if (!prequel)
|
|
17
|
+
break;
|
|
18
|
+
current = prequel.node.id;
|
|
19
|
+
}
|
|
20
|
+
return current;
|
|
21
|
+
}
|
|
22
|
+
/** Resolve format/status for a node from the map or from relation edges */
|
|
23
|
+
function resolveNodeInfo(id, relationsMap) {
|
|
24
|
+
// Direct lookup
|
|
25
|
+
const node = relationsMap.get(id);
|
|
26
|
+
if (node)
|
|
27
|
+
return { format: node.format, status: node.status };
|
|
28
|
+
// Fallback: scan relation edges from other nodes
|
|
29
|
+
for (const n of relationsMap.values()) {
|
|
30
|
+
for (const edge of n.relations.edges) {
|
|
31
|
+
if (edge.node.id === id) {
|
|
32
|
+
return { format: edge.node.format, status: edge.node.status };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { format: null, status: null };
|
|
37
|
+
}
|
|
38
|
+
/** Build a watch order by following SEQUEL edges from the franchise root */
|
|
39
|
+
export function buildWatchOrder(startId, relationsMap, includeSpecials) {
|
|
40
|
+
const rootId = findRoot(startId, relationsMap);
|
|
41
|
+
const entries = [];
|
|
42
|
+
const visited = new Set();
|
|
43
|
+
// BFS through sequel and side-story edges
|
|
44
|
+
const queue = [rootId];
|
|
45
|
+
let depth = 0;
|
|
46
|
+
while (queue.length > 0 && depth < MAX_DEPTH) {
|
|
47
|
+
const id = queue.shift();
|
|
48
|
+
if (id === undefined || visited.has(id))
|
|
49
|
+
continue;
|
|
50
|
+
visited.add(id);
|
|
51
|
+
depth++;
|
|
52
|
+
const node = relationsMap.get(id);
|
|
53
|
+
const title = node
|
|
54
|
+
? (node.title.english ?? node.title.romaji ?? "Unknown")
|
|
55
|
+
: "Unknown";
|
|
56
|
+
const { format, status } = resolveNodeInfo(id, relationsMap);
|
|
57
|
+
const isMain = MAIN_FORMATS.has(format ?? "");
|
|
58
|
+
if (isMain || includeSpecials) {
|
|
59
|
+
entries.push({
|
|
60
|
+
id,
|
|
61
|
+
title,
|
|
62
|
+
format,
|
|
63
|
+
status,
|
|
64
|
+
type: isMain ? "main" : "special",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!node)
|
|
68
|
+
continue;
|
|
69
|
+
// Collect sequels and side stories
|
|
70
|
+
const sequels = [];
|
|
71
|
+
const sides = [];
|
|
72
|
+
for (const edge of node.relations.edges) {
|
|
73
|
+
if (visited.has(edge.node.id))
|
|
74
|
+
continue;
|
|
75
|
+
if (edge.relationType === "SEQUEL") {
|
|
76
|
+
sequels.push(edge.node.id);
|
|
77
|
+
}
|
|
78
|
+
else if (includeSpecials &&
|
|
79
|
+
(edge.relationType === "SIDE_STORY" || edge.relationType === "SPIN_OFF")) {
|
|
80
|
+
sides.push(edge.node.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Side stories appear after their parent, before the next sequel
|
|
84
|
+
queue.push(...sides, ...sequels);
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
package/dist/engine/mood.d.ts
CHANGED
|
@@ -9,12 +9,18 @@ export interface MoodModifiers {
|
|
|
9
9
|
penalizeGenres: Set<string>;
|
|
10
10
|
penalizeTags: Set<string>;
|
|
11
11
|
}
|
|
12
|
+
export declare function loadCustomMoods(): void;
|
|
12
13
|
/** Parse a freeform mood string into genre/tag boost and penalize sets */
|
|
13
14
|
export declare function parseMood(mood: string): MoodModifiers;
|
|
14
15
|
/** Check whether a mood string matches any known keywords */
|
|
15
16
|
export declare function hasMoodMatch(mood: string): boolean;
|
|
16
17
|
/** List all recognized mood keywords */
|
|
17
18
|
export declare function getMoodKeywords(): string[];
|
|
19
|
+
/** Extract mood keywords as separate genre and tag arrays */
|
|
20
|
+
export declare function parseMoodFilters(mood: string): {
|
|
21
|
+
genres: string[];
|
|
22
|
+
tags: string[];
|
|
23
|
+
};
|
|
18
24
|
/** Suggest mood keywords that fit the current anime season */
|
|
19
25
|
export declare function seasonalMoodSuggestions(): {
|
|
20
26
|
season: string;
|
package/dist/engine/mood.js
CHANGED
|
@@ -146,16 +146,44 @@ const MOOD_SYNONYMS = {
|
|
|
146
146
|
// competitive
|
|
147
147
|
rivalry: "competitive",
|
|
148
148
|
tournament: "competitive",
|
|
149
|
+
// additional natural language terms
|
|
150
|
+
psychological: "brainy",
|
|
151
|
+
thoughtful: "brainy",
|
|
152
|
+
battle: "action",
|
|
153
|
+
fighting: "action",
|
|
154
|
+
heartfelt: "sad",
|
|
155
|
+
touching: "sad",
|
|
156
|
+
lighthearted: "wholesome",
|
|
157
|
+
feel: "wholesome",
|
|
158
|
+
feels: "wholesome",
|
|
159
|
+
suspense: "intense",
|
|
160
|
+
suspenseful: "intense",
|
|
149
161
|
};
|
|
150
162
|
// Merge base rules and synonyms into a single lookup
|
|
151
163
|
const MOOD_RULES = { ...BASE_MOOD_RULES };
|
|
152
164
|
for (const [synonym, base] of Object.entries(MOOD_SYNONYMS)) {
|
|
153
165
|
MOOD_RULES[synonym] = BASE_MOOD_RULES[base];
|
|
154
166
|
}
|
|
167
|
+
// Load user-defined mood overrides from env
|
|
168
|
+
export function loadCustomMoods() {
|
|
169
|
+
const raw = process.env.ANILIST_MOOD_CONFIG;
|
|
170
|
+
if (!raw)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
const custom = JSON.parse(raw);
|
|
174
|
+
for (const [key, rule] of Object.entries(custom)) {
|
|
175
|
+
MOOD_RULES[key.toLowerCase()] = rule;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
console.warn("[ani-mcp] Invalid ANILIST_MOOD_CONFIG JSON, using defaults.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
loadCustomMoods();
|
|
155
183
|
// === Mood Parser ===
|
|
156
184
|
/** Parse a freeform mood string into genre/tag boost and penalize sets */
|
|
157
185
|
export function parseMood(mood) {
|
|
158
|
-
//
|
|
186
|
+
// Lowercase tokens, stripped of punctuation
|
|
159
187
|
const words = mood
|
|
160
188
|
.toLowerCase()
|
|
161
189
|
.replace(/[^a-z\s]/g, "")
|
|
@@ -168,7 +196,7 @@ export function parseMood(mood) {
|
|
|
168
196
|
const rule = MOOD_RULES[word];
|
|
169
197
|
if (!rule)
|
|
170
198
|
continue;
|
|
171
|
-
// Add to both
|
|
199
|
+
// Add to both sets (matcher checks genres and tags separately)
|
|
172
200
|
for (const name of rule.boost) {
|
|
173
201
|
boostGenres.add(name);
|
|
174
202
|
boostTags.add(name);
|
|
@@ -192,6 +220,40 @@ export function hasMoodMatch(mood) {
|
|
|
192
220
|
export function getMoodKeywords() {
|
|
193
221
|
return Object.keys(MOOD_RULES);
|
|
194
222
|
}
|
|
223
|
+
// AniList's fixed genre set (stable, rarely changes)
|
|
224
|
+
const ANILIST_GENRES = new Set([
|
|
225
|
+
"Action",
|
|
226
|
+
"Adventure",
|
|
227
|
+
"Comedy",
|
|
228
|
+
"Drama",
|
|
229
|
+
"Ecchi",
|
|
230
|
+
"Fantasy",
|
|
231
|
+
"Horror",
|
|
232
|
+
"Mahou Shoujo",
|
|
233
|
+
"Mecha",
|
|
234
|
+
"Music",
|
|
235
|
+
"Mystery",
|
|
236
|
+
"Psychological",
|
|
237
|
+
"Romance",
|
|
238
|
+
"Sci-Fi",
|
|
239
|
+
"Slice of Life",
|
|
240
|
+
"Sports",
|
|
241
|
+
"Supernatural",
|
|
242
|
+
"Thriller",
|
|
243
|
+
]);
|
|
244
|
+
/** Extract mood keywords as separate genre and tag arrays */
|
|
245
|
+
export function parseMoodFilters(mood) {
|
|
246
|
+
const mods = parseMood(mood);
|
|
247
|
+
const genres = [];
|
|
248
|
+
const tags = [];
|
|
249
|
+
for (const name of mods.boostGenres) {
|
|
250
|
+
if (ANILIST_GENRES.has(name))
|
|
251
|
+
genres.push(name);
|
|
252
|
+
else
|
|
253
|
+
tags.push(name);
|
|
254
|
+
}
|
|
255
|
+
return { genres, tags };
|
|
256
|
+
}
|
|
195
257
|
// === Seasonal Suggestions ===
|
|
196
258
|
const SEASONAL_MOODS = {
|
|
197
259
|
WINTER: ["cozy", "dark", "nostalgic", "brainy"],
|
package/dist/index.js
CHANGED
package/dist/schemas.d.ts
CHANGED
|
@@ -74,11 +74,57 @@ export declare const PickInputSchema: z.ZodObject<{
|
|
|
74
74
|
ANIME: "ANIME";
|
|
75
75
|
MANGA: "MANGA";
|
|
76
76
|
}>>;
|
|
77
|
+
profileType: z.ZodOptional<z.ZodEnum<{
|
|
78
|
+
ANIME: "ANIME";
|
|
79
|
+
MANGA: "MANGA";
|
|
80
|
+
}>>;
|
|
81
|
+
source: z.ZodDefault<z.ZodEnum<{
|
|
82
|
+
PLANNING: "PLANNING";
|
|
83
|
+
SEASONAL: "SEASONAL";
|
|
84
|
+
DISCOVER: "DISCOVER";
|
|
85
|
+
}>>;
|
|
86
|
+
season: z.ZodOptional<z.ZodEnum<{
|
|
87
|
+
WINTER: "WINTER";
|
|
88
|
+
SPRING: "SPRING";
|
|
89
|
+
SUMMER: "SUMMER";
|
|
90
|
+
FALL: "FALL";
|
|
91
|
+
}>>;
|
|
92
|
+
year: z.ZodOptional<z.ZodNumber>;
|
|
77
93
|
mood: z.ZodOptional<z.ZodString>;
|
|
78
94
|
maxEpisodes: z.ZodOptional<z.ZodNumber>;
|
|
79
95
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
80
96
|
}, z.core.$strip>;
|
|
81
97
|
export type PickInput = z.infer<typeof PickInputSchema>;
|
|
98
|
+
/** Input for planning a watch/read session within a time budget */
|
|
99
|
+
export declare const SessionInputSchema: z.ZodObject<{
|
|
100
|
+
username: z.ZodOptional<z.ZodString>;
|
|
101
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
102
|
+
ANIME: "ANIME";
|
|
103
|
+
MANGA: "MANGA";
|
|
104
|
+
}>>;
|
|
105
|
+
minutes: z.ZodNumber;
|
|
106
|
+
mood: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strip>;
|
|
108
|
+
export type SessionInput = z.infer<typeof SessionInputSchema>;
|
|
109
|
+
/** Input for finding sequels to completed titles airing this season */
|
|
110
|
+
export declare const SequelAlertInputSchema: z.ZodObject<{
|
|
111
|
+
username: z.ZodOptional<z.ZodString>;
|
|
112
|
+
season: z.ZodOptional<z.ZodEnum<{
|
|
113
|
+
WINTER: "WINTER";
|
|
114
|
+
SPRING: "SPRING";
|
|
115
|
+
SUMMER: "SUMMER";
|
|
116
|
+
FALL: "FALL";
|
|
117
|
+
}>>;
|
|
118
|
+
year: z.ZodOptional<z.ZodNumber>;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
export type SequelAlertInput = z.infer<typeof SequelAlertInputSchema>;
|
|
121
|
+
/** Input for franchise watch order guidance */
|
|
122
|
+
export declare const WatchOrderInputSchema: z.ZodObject<{
|
|
123
|
+
id: z.ZodOptional<z.ZodNumber>;
|
|
124
|
+
title: z.ZodOptional<z.ZodString>;
|
|
125
|
+
includeSpecials: z.ZodDefault<z.ZodBoolean>;
|
|
126
|
+
}, z.core.$strip>;
|
|
127
|
+
export type WatchOrderInput = z.infer<typeof WatchOrderInputSchema>;
|
|
82
128
|
/** Input for comparing taste profiles between two users */
|
|
83
129
|
export declare const CompareInputSchema: z.ZodObject<{
|
|
84
130
|
user1: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -88,7 +88,15 @@ export const ListInputSchema = z.object({
|
|
|
88
88
|
.default("ANIME")
|
|
89
89
|
.describe("Get anime or manga list"),
|
|
90
90
|
status: z
|
|
91
|
-
.enum([
|
|
91
|
+
.enum([
|
|
92
|
+
"CURRENT",
|
|
93
|
+
"COMPLETED",
|
|
94
|
+
"PLANNING",
|
|
95
|
+
"DROPPED",
|
|
96
|
+
"PAUSED",
|
|
97
|
+
"ALL",
|
|
98
|
+
"CUSTOM",
|
|
99
|
+
])
|
|
92
100
|
.default("ALL")
|
|
93
101
|
.describe("Filter by list status. CURRENT = watching/reading now. CUSTOM = user-created lists."),
|
|
94
102
|
customListName: z
|
|
@@ -127,6 +135,27 @@ export const PickInputSchema = z.object({
|
|
|
127
135
|
.enum(["ANIME", "MANGA"])
|
|
128
136
|
.default("ANIME")
|
|
129
137
|
.describe("Recommend from anime or manga planning list"),
|
|
138
|
+
profileType: z
|
|
139
|
+
.enum(["ANIME", "MANGA"])
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Build taste profile from this media type. Defaults to same as type. " +
|
|
142
|
+
"Set to get cross-media recs, e.g. anime picks based on manga taste."),
|
|
143
|
+
source: z
|
|
144
|
+
.enum(["PLANNING", "SEASONAL", "DISCOVER"])
|
|
145
|
+
.default("PLANNING")
|
|
146
|
+
.describe("Where to find candidates. PLANNING = user's plan-to-watch list (default). " +
|
|
147
|
+
"SEASONAL = currently airing anime. DISCOVER = top-rated titles matching taste."),
|
|
148
|
+
season: z
|
|
149
|
+
.enum(["WINTER", "SPRING", "SUMMER", "FALL"])
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Season for SEASONAL source. Defaults to the current season."),
|
|
152
|
+
year: z
|
|
153
|
+
.number()
|
|
154
|
+
.int()
|
|
155
|
+
.min(1940)
|
|
156
|
+
.max(new Date().getFullYear() + 2)
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Year for SEASONAL source. Defaults to the current year."),
|
|
130
159
|
mood: z
|
|
131
160
|
.string()
|
|
132
161
|
.optional()
|
|
@@ -145,6 +174,61 @@ export const PickInputSchema = z.object({
|
|
|
145
174
|
.default(5)
|
|
146
175
|
.describe("Number of recommendations to return (default 5, max 15)"),
|
|
147
176
|
});
|
|
177
|
+
/** Input for planning a watch/read session within a time budget */
|
|
178
|
+
export const SessionInputSchema = z.object({
|
|
179
|
+
username: usernameSchema
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("AniList username. Falls back to configured default if not provided."),
|
|
182
|
+
type: z
|
|
183
|
+
.enum(["ANIME", "MANGA"])
|
|
184
|
+
.default("ANIME")
|
|
185
|
+
.describe("Plan session from anime or manga currently-watching list"),
|
|
186
|
+
minutes: z
|
|
187
|
+
.number()
|
|
188
|
+
.int()
|
|
189
|
+
.min(10)
|
|
190
|
+
.max(720)
|
|
191
|
+
.describe("Time budget in minutes (10-720)"),
|
|
192
|
+
mood: z
|
|
193
|
+
.string()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('Optional mood to prioritize titles, e.g. "dark", "chill"'),
|
|
196
|
+
});
|
|
197
|
+
/** Input for finding sequels to completed titles airing this season */
|
|
198
|
+
export const SequelAlertInputSchema = z.object({
|
|
199
|
+
username: usernameSchema
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("AniList username. Falls back to configured default if not provided."),
|
|
202
|
+
season: z
|
|
203
|
+
.enum(["WINTER", "SPRING", "SUMMER", "FALL"])
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Season to check for sequels. Defaults to the current season."),
|
|
206
|
+
year: z
|
|
207
|
+
.number()
|
|
208
|
+
.int()
|
|
209
|
+
.min(1940)
|
|
210
|
+
.max(new Date().getFullYear() + 2)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("Year to check. Defaults to the current year."),
|
|
213
|
+
});
|
|
214
|
+
/** Input for franchise watch order guidance */
|
|
215
|
+
export const WatchOrderInputSchema = z
|
|
216
|
+
.object({
|
|
217
|
+
id: z
|
|
218
|
+
.number()
|
|
219
|
+
.int()
|
|
220
|
+
.positive()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("AniList media ID of any title in the franchise"),
|
|
223
|
+
title: z.string().optional().describe("Search by title if no ID is known"),
|
|
224
|
+
includeSpecials: z
|
|
225
|
+
.boolean()
|
|
226
|
+
.default(false)
|
|
227
|
+
.describe("Include OVAs, specials, and spin-offs in the watch order"),
|
|
228
|
+
})
|
|
229
|
+
.refine((data) => data.id !== undefined || data.title !== undefined, {
|
|
230
|
+
message: "Provide either an id or a title.",
|
|
231
|
+
});
|
|
148
232
|
/** Input for comparing taste profiles between two users */
|
|
149
233
|
export const CompareInputSchema = z.object({
|
|
150
234
|
user1: usernameSchema.describe("First AniList username"),
|
|
@@ -514,16 +598,8 @@ export const ProfileInputSchema = z.object({
|
|
|
514
598
|
/** Input for fetching community reviews for a title */
|
|
515
599
|
export const ReviewsInputSchema = z
|
|
516
600
|
.object({
|
|
517
|
-
id: z
|
|
518
|
-
|
|
519
|
-
.int()
|
|
520
|
-
.positive()
|
|
521
|
-
.optional()
|
|
522
|
-
.describe("AniList media ID"),
|
|
523
|
-
title: z
|
|
524
|
-
.string()
|
|
525
|
-
.optional()
|
|
526
|
-
.describe("Search by title if no ID is known"),
|
|
601
|
+
id: z.number().int().positive().optional().describe("AniList media ID"),
|
|
602
|
+
title: z.string().optional().describe("Search by title if no ID is known"),
|
|
527
603
|
sort: z
|
|
528
604
|
.enum(["HELPFUL", "NEWEST"])
|
|
529
605
|
.default("HELPFUL")
|
package/dist/tools/discover.js
CHANGED
|
@@ -10,7 +10,7 @@ export function registerDiscoverTools(server) {
|
|
|
10
10
|
name: "anilist_trending",
|
|
11
11
|
description: "Show what's trending on AniList right now. " +
|
|
12
12
|
"Use when the user asks what's hot, trending, or generating buzz. " +
|
|
13
|
-
"No search term needed
|
|
13
|
+
"No search term needed. Returns ranked list with title, format, score, genres, and episode count.",
|
|
14
14
|
parameters: TrendingInputSchema,
|
|
15
15
|
annotations: {
|
|
16
16
|
title: "Trending Now",
|
|
@@ -51,7 +51,7 @@ export function registerDiscoverTools(server) {
|
|
|
51
51
|
description: "Browse top anime or manga in a specific genre. " +
|
|
52
52
|
"Use when the user asks for the best titles in a genre, " +
|
|
53
53
|
'e.g. "best romance anime" or "top thriller manga from 2023". ' +
|
|
54
|
-
"
|
|
54
|
+
"Supports year, status, and format filters. Returns ranked list with title, score, and genres.",
|
|
55
55
|
parameters: GenreBrowseInputSchema,
|
|
56
56
|
annotations: {
|
|
57
57
|
title: "Browse by Genre",
|
|
@@ -113,8 +113,8 @@ export function registerDiscoverTools(server) {
|
|
|
113
113
|
server.addTool({
|
|
114
114
|
name: "anilist_genre_list",
|
|
115
115
|
description: "List all valid AniList genres and content tags. " +
|
|
116
|
-
"Use
|
|
117
|
-
"
|
|
116
|
+
"Use before genre-filtering tools to ensure valid genre names. " +
|
|
117
|
+
"Returns genres and content tags grouped by category with descriptions.",
|
|
118
118
|
parameters: GenreListInputSchema,
|
|
119
119
|
annotations: {
|
|
120
120
|
title: "List Genres & Tags",
|
package/dist/tools/info.js
CHANGED
|
@@ -79,7 +79,7 @@ export function registerInfoTools(server) {
|
|
|
79
79
|
name: "anilist_staff",
|
|
80
80
|
description: "Get staff and voice actor credits for an anime or manga. " +
|
|
81
81
|
"Use when the user asks who directed, wrote, or voiced characters in a title. " +
|
|
82
|
-
"
|
|
82
|
+
"Returns production staff with roles and characters with Japanese voice actors.",
|
|
83
83
|
parameters: StaffInputSchema,
|
|
84
84
|
annotations: {
|
|
85
85
|
title: "Get Staff Credits",
|
|
@@ -138,7 +138,8 @@ export function registerInfoTools(server) {
|
|
|
138
138
|
name: "anilist_schedule",
|
|
139
139
|
description: "Get the airing schedule for an anime. " +
|
|
140
140
|
"Use when the user asks when the next episode airs, " +
|
|
141
|
-
"or wants to see upcoming episode dates for a currently airing show."
|
|
141
|
+
"or wants to see upcoming episode dates for a currently airing show. " +
|
|
142
|
+
"Returns next episode date/countdown and upcoming episode schedule.",
|
|
142
143
|
parameters: ScheduleInputSchema,
|
|
143
144
|
annotations: {
|
|
144
145
|
title: "Airing Schedule",
|
|
@@ -200,7 +201,8 @@ export function registerInfoTools(server) {
|
|
|
200
201
|
name: "anilist_characters",
|
|
201
202
|
description: "Search for anime/manga characters by name. " +
|
|
202
203
|
"Use when the user asks about a specific character, wants to know " +
|
|
203
|
-
"which series a character appears in, or who voices them."
|
|
204
|
+
"which series a character appears in, or who voices them. " +
|
|
205
|
+
"Returns character appearances with roles and voice actors.",
|
|
204
206
|
parameters: CharacterSearchInputSchema,
|
|
205
207
|
annotations: {
|
|
206
208
|
title: "Search Characters",
|
|
@@ -250,8 +252,8 @@ export function registerInfoTools(server) {
|
|
|
250
252
|
server.addTool({
|
|
251
253
|
name: "anilist_staff_search",
|
|
252
254
|
description: "Search for anime/manga staff by name and see their works. " +
|
|
253
|
-
"Use when the user asks about a director, voice actor, animator, or writer " +
|
|
254
|
-
"
|
|
255
|
+
"Use when the user asks about a director, voice actor, animator, or writer. " +
|
|
256
|
+
"Returns staff occupations, works with roles, and scores.",
|
|
255
257
|
parameters: StaffSearchInputSchema,
|
|
256
258
|
annotations: {
|
|
257
259
|
title: "Search Staff",
|
|
@@ -325,8 +327,8 @@ export function registerInfoTools(server) {
|
|
|
325
327
|
server.addTool({
|
|
326
328
|
name: "anilist_studio_search",
|
|
327
329
|
description: "Search for an animation studio by name and see their productions. " +
|
|
328
|
-
"Use when the user asks about a studio like MAPPA, Kyoto Animation, or Bones " +
|
|
329
|
-
"and
|
|
330
|
+
"Use when the user asks about a studio like MAPPA, Kyoto Animation, or Bones. " +
|
|
331
|
+
"Returns main and supporting productions with format, score, and status.",
|
|
330
332
|
parameters: StudioSearchInputSchema,
|
|
331
333
|
annotations: {
|
|
332
334
|
title: "Search Studios",
|