ani-mcp 0.8.2 → 0.8.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/dist/engine/taste.d.ts +2 -0
- package/dist/engine/taste.js +31 -0
- package/dist/index.js +1 -1
- package/dist/resources.js +2 -26
- package/dist/schemas.js +3 -9
- package/dist/tools/recommend.js +2 -27
- package/dist/tools/write.js +6 -2
- package/dist/utils.js +7 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/engine/taste.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export interface TasteProfile {
|
|
|
28
28
|
export declare function buildTasteProfile(entries: AniListMediaListEntry[]): TasteProfile;
|
|
29
29
|
/** Summarize a taste profile as natural language */
|
|
30
30
|
export declare function describeTasteProfile(profile: TasteProfile, username: string): string;
|
|
31
|
+
/** Detailed profile breakdown: genre weights, top themes, score distribution */
|
|
32
|
+
export declare function formatTasteProfileText(profile: TasteProfile): string[];
|
package/dist/engine/taste.js
CHANGED
|
@@ -177,6 +177,37 @@ function computeDecay(entry) {
|
|
|
177
177
|
const yearsSince = (now - epoch) / (365.25 * 24 * 3600);
|
|
178
178
|
return Math.exp(-DECAY_LAMBDA * Math.max(0, yearsSince));
|
|
179
179
|
}
|
|
180
|
+
/** Detailed profile breakdown: genre weights, top themes, score distribution */
|
|
181
|
+
export function formatTasteProfileText(profile) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
// Detailed genre breakdown
|
|
184
|
+
if (profile.genres.length > 0) {
|
|
185
|
+
lines.push("", "Genre Weights (higher = stronger preference):");
|
|
186
|
+
for (const g of profile.genres.slice(0, 10)) {
|
|
187
|
+
lines.push(` ${g.name}: ${g.weight.toFixed(2)} (${g.count} titles)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Detailed tag breakdown
|
|
191
|
+
if (profile.tags.length > 0) {
|
|
192
|
+
lines.push("", "Top Themes:");
|
|
193
|
+
for (const t of profile.tags.slice(0, 10)) {
|
|
194
|
+
lines.push(` ${t.name}: ${t.weight.toFixed(2)} (${t.count} titles)`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Score distribution bar chart
|
|
198
|
+
if (profile.scoring.totalScored > 0) {
|
|
199
|
+
lines.push("", "Score Distribution:");
|
|
200
|
+
for (let s = 10; s >= 1; s--) {
|
|
201
|
+
const count = profile.scoring.distribution[s] ?? 0;
|
|
202
|
+
if (count > 0) {
|
|
203
|
+
// Cap at 30 chars
|
|
204
|
+
const bar = "#".repeat(Math.min(count, 30));
|
|
205
|
+
lines.push(` ${s}/10: ${bar} (${count})`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
180
211
|
/** Empty profile for users with too few scored entries */
|
|
181
212
|
function emptyProfile(totalCompleted) {
|
|
182
213
|
return {
|
package/dist/index.js
CHANGED
package/dist/resources.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** MCP Resources: expose user context without tool calls */
|
|
2
2
|
import { anilistClient } from "./api/client.js";
|
|
3
3
|
import { USER_PROFILE_QUERY } from "./api/queries.js";
|
|
4
|
-
import { buildTasteProfile, describeTasteProfile, } from "./engine/taste.js";
|
|
4
|
+
import { buildTasteProfile, describeTasteProfile, formatTasteProfileText, } from "./engine/taste.js";
|
|
5
5
|
import { formatProfile } from "./tools/social.js";
|
|
6
6
|
import { formatListEntry } from "./tools/lists.js";
|
|
7
7
|
import { getDefaultUsername, getScoreFormat } from "./utils.js";
|
|
@@ -99,31 +99,7 @@ function formatTasteProfile(profile, username) {
|
|
|
99
99
|
`# Taste Profile: ${username}`,
|
|
100
100
|
"",
|
|
101
101
|
describeTasteProfile(profile, username),
|
|
102
|
+
...formatTasteProfileText(profile),
|
|
102
103
|
];
|
|
103
|
-
// Detailed genre breakdown
|
|
104
|
-
if (profile.genres.length > 0) {
|
|
105
|
-
lines.push("", "Genre Weights (higher = stronger preference):");
|
|
106
|
-
for (const g of profile.genres.slice(0, 10)) {
|
|
107
|
-
lines.push(` ${g.name}: ${g.weight.toFixed(2)} (${g.count} titles)`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Detailed tag breakdown
|
|
111
|
-
if (profile.tags.length > 0) {
|
|
112
|
-
lines.push("", "Top Themes:");
|
|
113
|
-
for (const t of profile.tags.slice(0, 10)) {
|
|
114
|
-
lines.push(` ${t.name}: ${t.weight.toFixed(2)} (${t.count} titles)`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Score distribution
|
|
118
|
-
if (profile.scoring.totalScored > 0) {
|
|
119
|
-
lines.push("", "Score Distribution:");
|
|
120
|
-
for (let s = 10; s >= 1; s--) {
|
|
121
|
-
const count = profile.scoring.distribution[s] ?? 0;
|
|
122
|
-
if (count > 0) {
|
|
123
|
-
const bar = "#".repeat(Math.min(count, 30));
|
|
124
|
-
lines.push(` ${s}/10: ${bar} (${count})`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
104
|
return lines.join("\n");
|
|
129
105
|
}
|
package/dist/schemas.js
CHANGED
|
@@ -468,7 +468,7 @@ export const RateInputSchema = z.object({
|
|
|
468
468
|
.number()
|
|
469
469
|
.min(0)
|
|
470
470
|
.max(10)
|
|
471
|
-
.describe("Score on a 0-10 scale. Use 0 to remove a score."),
|
|
471
|
+
.describe("Score on a 0-10 scale (decimals like 7.5 are supported). Use 0 to remove a score."),
|
|
472
472
|
});
|
|
473
473
|
/** Input for removing a title from the list */
|
|
474
474
|
export const DeleteFromListInputSchema = z
|
|
@@ -498,10 +498,7 @@ export const ExplainInputSchema = z
|
|
|
498
498
|
.positive()
|
|
499
499
|
.optional()
|
|
500
500
|
.describe("AniList media ID to evaluate against your taste profile"),
|
|
501
|
-
title: z
|
|
502
|
-
.string()
|
|
503
|
-
.optional()
|
|
504
|
-
.describe("Search by title if no ID is known"),
|
|
501
|
+
title: z.string().optional().describe("Search by title if no ID is known"),
|
|
505
502
|
username: usernameSchema
|
|
506
503
|
.optional()
|
|
507
504
|
.describe("AniList username. Falls back to configured default if not provided."),
|
|
@@ -526,10 +523,7 @@ export const SimilarInputSchema = z
|
|
|
526
523
|
.positive()
|
|
527
524
|
.optional()
|
|
528
525
|
.describe("AniList media ID to find similar titles for"),
|
|
529
|
-
title: z
|
|
530
|
-
.string()
|
|
531
|
-
.optional()
|
|
532
|
-
.describe("Search by title if no ID is known"),
|
|
526
|
+
title: z.string().optional().describe("Search by title if no ID is known"),
|
|
533
527
|
limit: z
|
|
534
528
|
.number()
|
|
535
529
|
.int()
|
package/dist/tools/recommend.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BATCH_RELATIONS_QUERY, DISCOVER_MEDIA_QUERY, MEDIA_DETAILS_QUERY, RECOM
|
|
|
4
4
|
import { TasteInputSchema, PickInputSchema, SessionInputSchema, SequelAlertInputSchema, WatchOrderInputSchema, CompareInputSchema, WrappedInputSchema, ExplainInputSchema, SimilarInputSchema, } from "../schemas.js";
|
|
5
5
|
import { getTitle, getDefaultUsername, throwToolError, isNsfwEnabled, resolveSeasonYear, resolveAlias, } from "../utils.js";
|
|
6
6
|
import { SEARCH_MEDIA_QUERY } from "../api/queries.js";
|
|
7
|
-
import { buildTasteProfile, describeTasteProfile, } from "../engine/taste.js";
|
|
7
|
+
import { buildTasteProfile, describeTasteProfile, formatTasteProfileText, } from "../engine/taste.js";
|
|
8
8
|
import { matchCandidates, explainMatch } from "../engine/matcher.js";
|
|
9
9
|
import { parseMood, hasMoodMatch, seasonalMoodSuggestions, } from "../engine/mood.js";
|
|
10
10
|
import { computeCompatibility, computeGenreDivergences, findCrossRecs, } from "../engine/compare.js";
|
|
@@ -80,33 +80,8 @@ export function registerRecommendTools(server) {
|
|
|
80
80
|
`# Taste Profile: ${username}`,
|
|
81
81
|
"",
|
|
82
82
|
describeTasteProfile(profile, username),
|
|
83
|
+
...formatTasteProfileText(profile),
|
|
83
84
|
];
|
|
84
|
-
// Detailed genre breakdown
|
|
85
|
-
if (profile.genres.length > 0) {
|
|
86
|
-
lines.push("", "Genre Weights (higher = stronger preference):");
|
|
87
|
-
for (const g of profile.genres.slice(0, 10)) {
|
|
88
|
-
lines.push(` ${g.name}: ${g.weight.toFixed(2)} (${g.count} titles)`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Detailed tag breakdown
|
|
92
|
-
if (profile.tags.length > 0) {
|
|
93
|
-
lines.push("", "Top Themes:");
|
|
94
|
-
for (const t of profile.tags.slice(0, 10)) {
|
|
95
|
-
lines.push(` ${t.name}: ${t.weight.toFixed(2)} (${t.count} titles)`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Score distribution bar chart
|
|
99
|
-
if (profile.scoring.totalScored > 0) {
|
|
100
|
-
lines.push("", "Score Distribution:");
|
|
101
|
-
for (let s = 10; s >= 1; s--) {
|
|
102
|
-
const count = profile.scoring.distribution[s] ?? 0;
|
|
103
|
-
if (count > 0) {
|
|
104
|
-
// Cap at 30 chars
|
|
105
|
-
const bar = "#".repeat(Math.min(count, 30));
|
|
106
|
-
lines.push(` ${s}/10: ${bar} (${count})`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
85
|
return lines.join("\n");
|
|
111
86
|
}
|
|
112
87
|
catch (error) {
|
package/dist/tools/write.js
CHANGED
|
@@ -378,7 +378,9 @@ export function registerWriteTools(server) {
|
|
|
378
378
|
requireAuth();
|
|
379
379
|
const variables = { [FAVOURITE_VAR_MAP[args.type]]: args.id };
|
|
380
380
|
const data = await anilistClient.query(TOGGLE_FAVOURITE_MUTATION, variables, { cache: null });
|
|
381
|
-
|
|
381
|
+
const viewerName = await getViewerName();
|
|
382
|
+
anilistClient.invalidateUser(viewerName);
|
|
383
|
+
invalidateUserProfiles(viewerName);
|
|
382
384
|
// Check if entity is now in favourites (added) or absent (removed)
|
|
383
385
|
const field = FAVOURITE_FIELD_MAP[args.type];
|
|
384
386
|
const isFavourited = data.ToggleFavourite[field].nodes.some((n) => n.id === args.id);
|
|
@@ -410,7 +412,9 @@ export function registerWriteTools(server) {
|
|
|
410
412
|
try {
|
|
411
413
|
requireAuth();
|
|
412
414
|
const data = await anilistClient.query(SAVE_TEXT_ACTIVITY_MUTATION, { text: args.text }, { cache: null });
|
|
413
|
-
|
|
415
|
+
const viewerName = await getViewerName();
|
|
416
|
+
anilistClient.invalidateUser(viewerName);
|
|
417
|
+
invalidateUserProfiles(viewerName);
|
|
414
418
|
const activity = data.SaveTextActivity;
|
|
415
419
|
const dateStr = new Date(activity.createdAt * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
416
420
|
return [
|
package/dist/utils.js
CHANGED
|
@@ -60,7 +60,13 @@ export function truncateDescription(text, maxLength = 500) {
|
|
|
60
60
|
if (!text)
|
|
61
61
|
return "No description available.";
|
|
62
62
|
// AniList descriptions can contain HTML even with asHtml: false
|
|
63
|
-
|
|
63
|
+
let clean = text.replace(/<br\s*\/?>/gi, "\n");
|
|
64
|
+
// Loop to handle nested fragments like <scr<script>ipt>
|
|
65
|
+
let prev = "";
|
|
66
|
+
while (prev !== clean) {
|
|
67
|
+
prev = clean;
|
|
68
|
+
clean = clean.replace(/<[^>]+>/g, "");
|
|
69
|
+
}
|
|
64
70
|
if (clean.length <= maxLength)
|
|
65
71
|
return clean;
|
|
66
72
|
const truncated = clean.slice(0, maxLength);
|
package/manifest.json
CHANGED
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.8.
|
|
4
|
+
"version": "0.8.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",
|
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.8.
|
|
9
|
+
"version": "0.8.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ani-mcp",
|
|
14
|
-
"version": "0.8.
|
|
14
|
+
"version": "0.8.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|