ani-mcp 0.2.3 → 0.2.4

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
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="assets/icon.svg" width="128" height="128" alt="ani-mcp">
3
+ </p>
4
+
1
5
  # ani-mcp
2
6
 
3
7
  A smart [MCP](https://modelcontextprotocol.io) server for [AniList](https://anilist.co) that understands your anime and manga taste - not just raw API calls.
@@ -94,6 +98,29 @@ Works with any MCP-compatible client.
94
98
  | `anilist_rate` | Score a title (0-10) |
95
99
  | `anilist_delete_from_list` | Remove an entry from your list |
96
100
 
101
+ ## Examples
102
+
103
+ Here are some things you can ask your AI assistant once ani-mcp is connected:
104
+
105
+ **"What should I watch next?"**
106
+ Uses `anilist_pick` to analyze your completed list, build a taste profile, and recommend titles from your Planning list (or discover new ones) ranked by how well they match your preferences.
107
+
108
+ **"Compare my taste with username123"**
109
+ Uses `anilist_compare` to find shared titles, compute a compatibility score, highlight biggest disagreements, and suggest cross-recommendations between the two profiles.
110
+
111
+ **"What's airing this season?"**
112
+ Uses `anilist_seasonal` to show the current season's anime lineup sorted by popularity, with scores, genres, and episode counts.
113
+
114
+ **"Why would I like Vinland Saga?"**
115
+ Uses `anilist_explain` to score a specific title against your taste profile, breaking down genre affinity, theme alignment, and community reception.
116
+
117
+ **"Show me my anime year in review"**
118
+ Uses `anilist_wrapped` to summarize everything you watched in a given year - titles completed, average score, top genres, most controversial pick, and total episodes.
119
+
120
+ ## Privacy
121
+
122
+ See [PRIVACY.md](PRIVACY.md) for details. In short: ani-mcp runs locally, sends requests only to the AniList API, stores nothing, and collects no analytics.
123
+
97
124
  ## Docker
98
125
 
99
126
  ```sh
@@ -113,6 +140,10 @@ npm run build
113
140
  npm test
114
141
  ```
115
142
 
143
+ ## Support
144
+
145
+ Bug reports and feature requests: [GitHub Issues](https://github.com/gavxm/ani-mcp/issues)
146
+
116
147
  ## License
117
148
 
118
149
  MIT
@@ -12,6 +12,12 @@ export function registerDiscoverTools(server) {
12
12
  "Use when the user asks what's hot, trending, or generating buzz. " +
13
13
  "No search term needed - returns titles ranked by current trending score.",
14
14
  parameters: TrendingInputSchema,
15
+ annotations: {
16
+ title: "Trending Now",
17
+ readOnlyHint: true,
18
+ destructiveHint: false,
19
+ openWorldHint: true,
20
+ },
15
21
  execute: async (args) => {
16
22
  try {
17
23
  const data = await anilistClient.query(TRENDING_MEDIA_QUERY, {
@@ -47,6 +53,12 @@ export function registerDiscoverTools(server) {
47
53
  'e.g. "best romance anime" or "top thriller manga from 2023". ' +
48
54
  "No search term needed - discovers by genre with optional year/status/format filters.",
49
55
  parameters: GenreBrowseInputSchema,
56
+ annotations: {
57
+ title: "Browse by Genre",
58
+ readOnlyHint: true,
59
+ destructiveHint: false,
60
+ openWorldHint: true,
61
+ },
50
62
  execute: async (args) => {
51
63
  try {
52
64
  const sortMap = {
@@ -25,6 +25,12 @@ export function registerInfoTools(server) {
25
25
  "Use when the user asks who directed, wrote, or voiced characters in a title. " +
26
26
  "Shows directors, writers, character designers, and Japanese voice actors.",
27
27
  parameters: StaffInputSchema,
28
+ annotations: {
29
+ title: "Get Staff Credits",
30
+ readOnlyHint: true,
31
+ destructiveHint: false,
32
+ openWorldHint: true,
33
+ },
28
34
  execute: async (args) => {
29
35
  try {
30
36
  const variables = {};
@@ -78,6 +84,12 @@ export function registerInfoTools(server) {
78
84
  "Use when the user asks when the next episode airs, " +
79
85
  "or wants to see upcoming episode dates for a currently airing show.",
80
86
  parameters: ScheduleInputSchema,
87
+ annotations: {
88
+ title: "Airing Schedule",
89
+ readOnlyHint: true,
90
+ destructiveHint: false,
91
+ openWorldHint: true,
92
+ },
81
93
  execute: async (args) => {
82
94
  try {
83
95
  const variables = { notYetAired: true };
@@ -134,6 +146,12 @@ export function registerInfoTools(server) {
134
146
  "Use when the user asks about a specific character, wants to know " +
135
147
  "which series a character appears in, or who voices them.",
136
148
  parameters: CharacterSearchInputSchema,
149
+ annotations: {
150
+ title: "Search Characters",
151
+ readOnlyHint: true,
152
+ destructiveHint: false,
153
+ openWorldHint: true,
154
+ },
137
155
  execute: async (args) => {
138
156
  try {
139
157
  const data = await anilistClient.query(CHARACTER_SEARCH_QUERY, { search: args.query, page: args.page, perPage: args.limit }, { cache: "search" });
@@ -179,6 +197,12 @@ export function registerInfoTools(server) {
179
197
  "Use when the user asks about a director, voice actor, animator, or writer " +
180
198
  "and wants to see everything they have worked on.",
181
199
  parameters: StaffSearchInputSchema,
200
+ annotations: {
201
+ title: "Search Staff",
202
+ readOnlyHint: true,
203
+ destructiveHint: false,
204
+ openWorldHint: true,
205
+ },
182
206
  execute: async (args) => {
183
207
  try {
184
208
  const data = await anilistClient.query(STAFF_SEARCH_QUERY, { search: args.query, page: args.page, perPage: args.limit, mediaPerPage: args.mediaLimit }, { cache: "search" });
@@ -243,6 +267,12 @@ export function registerInfoTools(server) {
243
267
  "Use when the user asks about a studio like MAPPA, Kyoto Animation, or Bones " +
244
268
  "and wants to see what they have produced.",
245
269
  parameters: StudioSearchInputSchema,
270
+ annotations: {
271
+ title: "Search Studios",
272
+ readOnlyHint: true,
273
+ destructiveHint: false,
274
+ openWorldHint: true,
275
+ },
246
276
  execute: async (args) => {
247
277
  try {
248
278
  const data = await anilistClient.query(STUDIO_SEARCH_QUERY, { search: args.query, perPage: args.limit }, { cache: "search" });
@@ -19,6 +19,12 @@ export function registerListTools(server) {
19
19
  "what they've completed, or what's on their plan-to-watch. " +
20
20
  "Defaults to the configured username if not provided.",
21
21
  parameters: ListInputSchema,
22
+ annotations: {
23
+ title: "Get User List",
24
+ readOnlyHint: true,
25
+ destructiveHint: false,
26
+ openWorldHint: true,
27
+ },
22
28
  execute: async (args) => {
23
29
  try {
24
30
  const username = getDefaultUsername(args.username);
@@ -61,6 +67,12 @@ export function registerListTools(server) {
61
67
  "their average score, top genres, or score distribution. " +
62
68
  "Shows anime and manga stats side by side.",
63
69
  parameters: StatsInputSchema,
70
+ annotations: {
71
+ title: "Get User Stats",
72
+ readOnlyHint: true,
73
+ destructiveHint: false,
74
+ openWorldHint: true,
75
+ },
64
76
  execute: async (args) => {
65
77
  try {
66
78
  const username = getDefaultUsername(args.username);
@@ -50,6 +50,12 @@ export function registerRecommendTools(server) {
50
50
  "what genres they like, or how they tend to score. " +
51
51
  "Analyzes genre preferences, theme weights, scoring patterns, and format split.",
52
52
  parameters: TasteInputSchema,
53
+ annotations: {
54
+ title: "Taste Profile",
55
+ readOnlyHint: true,
56
+ destructiveHint: false,
57
+ openWorldHint: true,
58
+ },
53
59
  execute: async (args) => {
54
60
  try {
55
61
  const username = getDefaultUsername(args.username);
@@ -99,6 +105,12 @@ export function registerRecommendTools(server) {
99
105
  "based on your taste profile. Falls back to top-rated AniList titles " +
100
106
  "if the Planning list is empty. Optionally filter by mood or max episodes.",
101
107
  parameters: PickInputSchema,
108
+ annotations: {
109
+ title: "Pick Next Watch",
110
+ readOnlyHint: true,
111
+ destructiveHint: false,
112
+ openWorldHint: true,
113
+ },
102
114
  execute: async (args) => {
103
115
  try {
104
116
  const username = getDefaultUsername(args.username);
@@ -200,6 +212,12 @@ export function registerRecommendTools(server) {
200
212
  "and genre divergences. Use when someone asks to compare their taste " +
201
213
  "with another user.",
202
214
  parameters: CompareInputSchema,
215
+ annotations: {
216
+ title: "Compare Users",
217
+ readOnlyHint: true,
218
+ destructiveHint: false,
219
+ openWorldHint: true,
220
+ },
203
221
  execute: async (args) => {
204
222
  try {
205
223
  // Fetch both users' completed lists in parallel
@@ -311,6 +329,12 @@ export function registerRecommendTools(server) {
311
329
  "Use when the user asks about their anime/manga year, what they watched/read " +
312
330
  "in a given year, or wants a recap. Defaults to the current year.",
313
331
  parameters: WrappedInputSchema,
332
+ annotations: {
333
+ title: "Year in Review",
334
+ readOnlyHint: true,
335
+ destructiveHint: false,
336
+ openWorldHint: true,
337
+ },
314
338
  execute: async (args) => {
315
339
  try {
316
340
  const username = getDefaultUsername(args.username);
@@ -415,6 +439,12 @@ export function registerRecommendTools(server) {
415
439
  'Use when the user asks "why would I like this?", "is this for me?", or ' +
416
440
  "wants to know how well a specific anime/manga matches their preferences.",
417
441
  parameters: ExplainInputSchema,
442
+ annotations: {
443
+ title: "Explain Match",
444
+ readOnlyHint: true,
445
+ destructiveHint: false,
446
+ openWorldHint: true,
447
+ },
418
448
  execute: async (args) => {
419
449
  try {
420
450
  const username = getDefaultUsername(args.username);
@@ -500,6 +530,12 @@ export function registerRecommendTools(server) {
500
530
  "Use when the user asks for shows like a specific title, " +
501
531
  "or wants content-based recommendations without needing a user profile.",
502
532
  parameters: SimilarInputSchema,
533
+ annotations: {
534
+ title: "Find Similar",
535
+ readOnlyHint: true,
536
+ destructiveHint: false,
537
+ openWorldHint: true,
538
+ },
503
539
  execute: async (args) => {
504
540
  try {
505
541
  // Fetch source details and recommendations in parallel
@@ -15,6 +15,12 @@ export function registerSearchTools(server) {
15
15
  "in a genre, or find what aired in a specific year. " +
16
16
  "Returns a ranked list with title, format, score, genres, and AniList URL.",
17
17
  parameters: SearchInputSchema,
18
+ annotations: {
19
+ title: "Search Anime/Manga",
20
+ readOnlyHint: true,
21
+ destructiveHint: false,
22
+ openWorldHint: true,
23
+ },
18
24
  execute: async (args) => {
19
25
  try {
20
26
  const variables = {
@@ -61,6 +67,12 @@ export function registerSearchTools(server) {
61
67
  "episodes, studios, related works, and recommendations. " +
62
68
  "Provide either an AniList ID (faster, exact) or a title (fuzzy match).",
63
69
  parameters: DetailsInputSchema,
70
+ annotations: {
71
+ title: "Get Title Details",
72
+ readOnlyHint: true,
73
+ destructiveHint: false,
74
+ openWorldHint: true,
75
+ },
64
76
  execute: async (args) => {
65
77
  try {
66
78
  // AniList uses "search" as the GraphQL variable name for title lookups
@@ -147,6 +159,12 @@ export function registerSearchTools(server) {
147
159
  "Use when the user asks what's airing this season, what aired in a past season, " +
148
160
  "or wants to discover seasonal anime. Defaults to the current season/year.",
149
161
  parameters: SeasonalInputSchema,
162
+ annotations: {
163
+ title: "Browse Seasonal Anime",
164
+ readOnlyHint: true,
165
+ destructiveHint: false,
166
+ openWorldHint: true,
167
+ },
150
168
  execute: async (args) => {
151
169
  try {
152
170
  const { season, year } = resolveSeasonYear(args.season, args.year);
@@ -192,6 +210,12 @@ export function registerSearchTools(server) {
192
210
  'or says "I liked X, what else should I watch?" ' +
193
211
  "Returns titles recommended by AniList users, sorted by recommendation count.",
194
212
  parameters: RecommendationsInputSchema,
213
+ annotations: {
214
+ title: "Get Recommendations",
215
+ readOnlyHint: true,
216
+ destructiveHint: false,
217
+ openWorldHint: true,
218
+ },
195
219
  execute: async (args) => {
196
220
  try {
197
221
  const variables = {
@@ -20,6 +20,13 @@ export function registerWriteTools(server) {
20
20
  "Use when the user says they watched an episode, finished a chapter, " +
21
21
  "or wants to record how far they are. Requires ANILIST_TOKEN.",
22
22
  parameters: UpdateProgressInputSchema,
23
+ annotations: {
24
+ title: "Update Progress",
25
+ readOnlyHint: false,
26
+ destructiveHint: false,
27
+ idempotentHint: true,
28
+ openWorldHint: true,
29
+ },
23
30
  execute: async (args) => {
24
31
  try {
25
32
  requireAuth();
@@ -50,6 +57,13 @@ export function registerWriteTools(server) {
50
57
  "Use when the user wants to start watching, plan to watch, " +
51
58
  "or mark a title as completed. Requires ANILIST_TOKEN.",
52
59
  parameters: AddToListInputSchema,
60
+ annotations: {
61
+ title: "Add to List",
62
+ readOnlyHint: false,
63
+ destructiveHint: false,
64
+ idempotentHint: true,
65
+ openWorldHint: true,
66
+ },
53
67
  execute: async (args) => {
54
68
  try {
55
69
  requireAuth();
@@ -81,6 +95,13 @@ export function registerWriteTools(server) {
81
95
  "Use when the user wants to give a rating (0-10). " +
82
96
  "Use 0 to remove an existing score. Requires ANILIST_TOKEN.",
83
97
  parameters: RateInputSchema,
98
+ annotations: {
99
+ title: "Rate Title",
100
+ readOnlyHint: false,
101
+ destructiveHint: false,
102
+ idempotentHint: true,
103
+ openWorldHint: true,
104
+ },
84
105
  execute: async (args) => {
85
106
  try {
86
107
  requireAuth();
@@ -104,6 +125,13 @@ export function registerWriteTools(server) {
104
125
  "Requires the list entry ID (not the media ID) - use anilist_list to find it. " +
105
126
  "Requires ANILIST_TOKEN.",
106
127
  parameters: DeleteFromListInputSchema,
128
+ annotations: {
129
+ title: "Delete from List",
130
+ readOnlyHint: false,
131
+ destructiveHint: true,
132
+ idempotentHint: true,
133
+ openWorldHint: true,
134
+ },
107
135
  execute: async (args) => {
108
136
  try {
109
137
  requireAuth();
package/manifest.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "manifest_version": "0.3",
3
+ "name": "ani-mcp",
4
+ "version": "0.2.4",
5
+ "display_name": "AniList MCP",
6
+ "description": "A smart MCP server for AniList that gets your anime/manga taste - not just API calls.",
7
+ "author": {
8
+ "name": "gavxm"
9
+ },
10
+ "license": "MIT",
11
+ "icon": "assets/icon.png",
12
+ "keywords": [
13
+ "anime",
14
+ "manga",
15
+ "anilist",
16
+ "recommendations"
17
+ ],
18
+ "privacy_policies": [
19
+ "https://github.com/gavxm/ani-mcp/blob/main/PRIVACY.md"
20
+ ],
21
+ "server": {
22
+ "type": "node",
23
+ "entry_point": "dist/index.js",
24
+ "mcp_config": {
25
+ "command": "node",
26
+ "args": [
27
+ "${__dirname}/dist/index.js"
28
+ ],
29
+ "env": {
30
+ "ANILIST_USERNAME": "${user_config.anilist_username}",
31
+ "ANILIST_TOKEN": "${user_config.anilist_token}"
32
+ }
33
+ }
34
+ },
35
+ "user_config": {
36
+ "anilist_username": {
37
+ "type": "string",
38
+ "title": "AniList Username",
39
+ "description": "Default AniList username for list and stats tools",
40
+ "required": false
41
+ },
42
+ "anilist_token": {
43
+ "type": "string",
44
+ "title": "AniList Token",
45
+ "description": "AniList OAuth token for write operations (update progress, rate, etc.)",
46
+ "required": false,
47
+ "sensitive": true
48
+ }
49
+ },
50
+ "tools": [
51
+ {
52
+ "name": "anilist_search"
53
+ },
54
+ {
55
+ "name": "anilist_details"
56
+ },
57
+ {
58
+ "name": "anilist_seasonal"
59
+ },
60
+ {
61
+ "name": "anilist_recommendations"
62
+ },
63
+ {
64
+ "name": "anilist_list"
65
+ },
66
+ {
67
+ "name": "anilist_stats"
68
+ },
69
+ {
70
+ "name": "anilist_trending"
71
+ },
72
+ {
73
+ "name": "anilist_genres"
74
+ },
75
+ {
76
+ "name": "anilist_taste"
77
+ },
78
+ {
79
+ "name": "anilist_pick"
80
+ },
81
+ {
82
+ "name": "anilist_compare"
83
+ },
84
+ {
85
+ "name": "anilist_wrapped"
86
+ },
87
+ {
88
+ "name": "anilist_explain"
89
+ },
90
+ {
91
+ "name": "anilist_similar"
92
+ },
93
+ {
94
+ "name": "anilist_staff"
95
+ },
96
+ {
97
+ "name": "anilist_staff_search"
98
+ },
99
+ {
100
+ "name": "anilist_studio_search"
101
+ },
102
+ {
103
+ "name": "anilist_schedule"
104
+ },
105
+ {
106
+ "name": "anilist_characters"
107
+ },
108
+ {
109
+ "name": "anilist_update_progress"
110
+ },
111
+ {
112
+ "name": "anilist_add_to_list"
113
+ },
114
+ {
115
+ "name": "anilist_rate"
116
+ },
117
+ {
118
+ "name": "anilist_delete_from_list"
119
+ }
120
+ ]
121
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ani-mcp",
3
3
  "mcpName": "io.github.gavxm/ani-mcp",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "A smart [MCP](https://modelcontextprotocol.io) server for [AniList](https://anilist.co) that gets your anime/manga taste - not just API calls.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -19,6 +19,7 @@
19
19
  "test": "vitest run",
20
20
  "test:coverage": "vitest run --coverage",
21
21
  "test:watch": "vitest",
22
+ "pack": "npx @anthropic-ai/mcpb pack",
22
23
  "release": "git tag v$(node -p \"require('./package.json').version\") && git push origin v$(node -p \"require('./package.json').version\")"
23
24
  },
24
25
  "keywords": [
@@ -36,7 +37,8 @@
36
37
  "homepage": "https://github.com/gavxm/ani-mcp",
37
38
  "files": [
38
39
  "dist",
39
- "server.json"
40
+ "server.json",
41
+ "manifest.json"
40
42
  ],
41
43
  "engines": {
42
44
  "node": ">=18"
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/gavxm/ani-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.3",
9
+ "version": "0.2.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ani-mcp",
14
- "version": "0.2.3",
14
+ "version": "0.2.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },