ani-mcp 0.12.0 → 0.13.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.d.ts +5 -0
- package/dist/api/client.js +4 -0
- package/dist/api/kitsu-client.d.ts +39 -0
- package/dist/api/kitsu-client.js +87 -0
- package/dist/index.js +1 -1
- package/dist/resources.js +31 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.js +15 -0
- package/dist/tools/import.js +114 -1
- package/manifest.json +25 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ Works with any MCP-compatible client.
|
|
|
114
114
|
| `anilist_watch_order` | Viewing order for a franchise |
|
|
115
115
|
| `anilist_session` | Plan a viewing session within a time budget |
|
|
116
116
|
| `anilist_mal_import` | Import a MyAnimeList user's list and generate recommendations |
|
|
117
|
+
| `anilist_kitsu_import` | Import a Kitsu user's list and generate recommendations |
|
|
117
118
|
|
|
118
119
|
### Cards
|
|
119
120
|
|
package/dist/api/client.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ declare class AniListClient {
|
|
|
39
39
|
fetchList(username: string, type: string, status?: string, sort?: string[]): Promise<AniListMediaListEntry[]>;
|
|
40
40
|
/** Invalidate the entire query cache */
|
|
41
41
|
clearCache(): void;
|
|
42
|
+
/** Cache size and capacity for health checks */
|
|
43
|
+
cacheStats(): {
|
|
44
|
+
size: number;
|
|
45
|
+
maxSize: number;
|
|
46
|
+
};
|
|
42
47
|
/** Evict cache entries related to a specific user (lists and stats) */
|
|
43
48
|
invalidateUser(username: string): void;
|
|
44
49
|
/** Retries with exponential backoff via p-retry */
|
package/dist/api/client.js
CHANGED
|
@@ -121,6 +121,10 @@ class AniListClient {
|
|
|
121
121
|
clearCache() {
|
|
122
122
|
queryCache.clear();
|
|
123
123
|
}
|
|
124
|
+
/** Cache size and capacity for health checks */
|
|
125
|
+
cacheStats() {
|
|
126
|
+
return { size: queryCache.size, maxSize: 500 };
|
|
127
|
+
}
|
|
124
128
|
/** Evict cache entries related to a specific user (lists and stats) */
|
|
125
129
|
invalidateUser(username) {
|
|
126
130
|
const needle = `"${username}"`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Kitsu API client for read-only list import */
|
|
2
|
+
export interface KitsuLibraryEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
attributes: {
|
|
5
|
+
status: string;
|
|
6
|
+
ratingTwenty: number | null;
|
|
7
|
+
progress: number;
|
|
8
|
+
};
|
|
9
|
+
relationships: {
|
|
10
|
+
anime: {
|
|
11
|
+
data: {
|
|
12
|
+
type: string;
|
|
13
|
+
id: string;
|
|
14
|
+
} | null;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface KitsuAnime {
|
|
19
|
+
id: string;
|
|
20
|
+
attributes: {
|
|
21
|
+
canonicalTitle: string;
|
|
22
|
+
episodeCount: number | null;
|
|
23
|
+
averageRating: string | null;
|
|
24
|
+
subtype: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface KitsuCategory {
|
|
28
|
+
id: string;
|
|
29
|
+
attributes: {
|
|
30
|
+
title: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Map Kitsu subtype to AniList format */
|
|
34
|
+
export declare function mapKitsuFormat(subtype: string): string;
|
|
35
|
+
/** Fetch a Kitsu user's completed anime library */
|
|
36
|
+
export declare function fetchKitsuList(username: string, maxPages?: number): Promise<{
|
|
37
|
+
entries: KitsuLibraryEntry[];
|
|
38
|
+
anime: Map<string, KitsuAnime>;
|
|
39
|
+
}>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Kitsu API client for read-only list import */
|
|
2
|
+
import pThrottle from "p-throttle";
|
|
3
|
+
import pRetry, { AbortError } from "p-retry";
|
|
4
|
+
const KITSU_BASE = process.env.KITSU_API_URL || "https://kitsu.io/api/edge";
|
|
5
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
6
|
+
const PAGE_LIMIT = 20;
|
|
7
|
+
// Kitsu has no documented rate limit; be conservative
|
|
8
|
+
const throttle = pThrottle({
|
|
9
|
+
limit: process.env.VITEST ? 10_000 : 5,
|
|
10
|
+
interval: 1_000,
|
|
11
|
+
});
|
|
12
|
+
const throttled = throttle(() => { });
|
|
13
|
+
// === Format Mapping ===
|
|
14
|
+
const KITSU_TO_ANILIST_FORMAT = {
|
|
15
|
+
TV: "TV",
|
|
16
|
+
movie: "MOVIE",
|
|
17
|
+
OVA: "OVA",
|
|
18
|
+
ONA: "ONA",
|
|
19
|
+
special: "SPECIAL",
|
|
20
|
+
music: "MUSIC",
|
|
21
|
+
};
|
|
22
|
+
/** Map Kitsu subtype to AniList format */
|
|
23
|
+
export function mapKitsuFormat(subtype) {
|
|
24
|
+
return KITSU_TO_ANILIST_FORMAT[subtype] ?? "TV";
|
|
25
|
+
}
|
|
26
|
+
// === Client ===
|
|
27
|
+
/** Resolve a Kitsu username to a user ID */
|
|
28
|
+
async function resolveUserId(username) {
|
|
29
|
+
return pRetry(async () => {
|
|
30
|
+
await throttled();
|
|
31
|
+
const url = `${KITSU_BASE}/users?filter[name]=${encodeURIComponent(username)}&fields[users]=id,name&page[limit]=1`;
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
headers: { Accept: "application/vnd.api+json" },
|
|
34
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Kitsu API error (HTTP ${response.status})`);
|
|
38
|
+
}
|
|
39
|
+
const json = (await response.json());
|
|
40
|
+
if (!json.data.length) {
|
|
41
|
+
throw new AbortError(`Kitsu user "${username}" not found.`);
|
|
42
|
+
}
|
|
43
|
+
return json.data[0].id;
|
|
44
|
+
}, { retries: 3 });
|
|
45
|
+
}
|
|
46
|
+
/** Fetch a Kitsu user's completed anime library */
|
|
47
|
+
export async function fetchKitsuList(username, maxPages = 10) {
|
|
48
|
+
const userId = await resolveUserId(username);
|
|
49
|
+
const entries = [];
|
|
50
|
+
const animeMap = new Map();
|
|
51
|
+
let url = `${KITSU_BASE}/library-entries?filter[userId]=${userId}` +
|
|
52
|
+
`&filter[status]=completed&filter[kind]=anime` +
|
|
53
|
+
`&page[limit]=${PAGE_LIMIT}` +
|
|
54
|
+
`&include=anime` +
|
|
55
|
+
`&fields[libraryEntries]=status,ratingTwenty,progress,anime` +
|
|
56
|
+
`&fields[anime]=canonicalTitle,episodeCount,averageRating,subtype`;
|
|
57
|
+
for (let page = 0; page < maxPages && url; page++) {
|
|
58
|
+
const data = await fetchPage(url);
|
|
59
|
+
entries.push(...data.data);
|
|
60
|
+
// Index included anime
|
|
61
|
+
if (data.included) {
|
|
62
|
+
for (const inc of data.included) {
|
|
63
|
+
if (inc.id && "canonicalTitle" in (inc.attributes ?? {})) {
|
|
64
|
+
animeMap.set(inc.id, inc);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
url = data.links?.next ?? null;
|
|
69
|
+
}
|
|
70
|
+
return { entries, anime: animeMap };
|
|
71
|
+
}
|
|
72
|
+
async function fetchPage(url) {
|
|
73
|
+
return pRetry(async () => {
|
|
74
|
+
await throttled();
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
headers: { Accept: "application/vnd.api+json" },
|
|
77
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
if (response.status === 404) {
|
|
81
|
+
throw new AbortError("Kitsu user not found.");
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Kitsu API error (HTTP ${response.status})`);
|
|
84
|
+
}
|
|
85
|
+
return (await response.json());
|
|
86
|
+
}, { retries: 3 });
|
|
87
|
+
}
|
package/dist/index.js
CHANGED
package/dist/resources.js
CHANGED
|
@@ -91,6 +91,37 @@ export function registerResources(server) {
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
});
|
|
94
|
+
// === Health Check ===
|
|
95
|
+
server.addResource({
|
|
96
|
+
uri: "anilist://status",
|
|
97
|
+
name: "Server Status",
|
|
98
|
+
description: "Health check showing API connectivity, auth status, cache state, and server version.",
|
|
99
|
+
mimeType: "text/plain",
|
|
100
|
+
async load() {
|
|
101
|
+
const lines = ["# ani-mcp Status", ""];
|
|
102
|
+
// Server version
|
|
103
|
+
lines.push(`Version: 0.13.0`);
|
|
104
|
+
// Auth status
|
|
105
|
+
const hasToken = Boolean(process.env.ANILIST_TOKEN);
|
|
106
|
+
const hasUsername = Boolean(process.env.ANILIST_USERNAME);
|
|
107
|
+
lines.push(`Auth: ${hasToken ? "token configured" : "no token (read-only mode)"}`);
|
|
108
|
+
lines.push(`Username: ${hasUsername ? process.env.ANILIST_USERNAME : "not configured"}`);
|
|
109
|
+
// API connectivity
|
|
110
|
+
try {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
await anilistClient.query("query Ping { Viewer { id } }", {}, { cache: null });
|
|
113
|
+
const latency = Date.now() - start;
|
|
114
|
+
lines.push(`API: connected (${latency}ms)`);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
lines.push("API: unreachable");
|
|
118
|
+
}
|
|
119
|
+
// Cache stats
|
|
120
|
+
const cacheStats = anilistClient.cacheStats();
|
|
121
|
+
lines.push(`Cache: ${cacheStats.size}/${cacheStats.maxSize} entries`);
|
|
122
|
+
return { text: lines.join("\n") };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
94
125
|
}
|
|
95
126
|
// === Formatting Helpers ===
|
|
96
127
|
/** Format a taste profile with detailed breakdowns */
|
package/dist/schemas.d.ts
CHANGED
|
@@ -260,6 +260,12 @@ export declare const MalImportInputSchema: z.ZodObject<{
|
|
|
260
260
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
261
261
|
}, z.core.$strip>;
|
|
262
262
|
export type MalImportInput = z.infer<typeof MalImportInputSchema>;
|
|
263
|
+
/** Input for importing a Kitsu user's completed list */
|
|
264
|
+
export declare const KitsuImportInputSchema: z.ZodObject<{
|
|
265
|
+
kitsuUsername: z.ZodString;
|
|
266
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
267
|
+
}, z.core.$strip>;
|
|
268
|
+
export type KitsuImportInput = z.infer<typeof KitsuImportInputSchema>;
|
|
263
269
|
/** Input for character search */
|
|
264
270
|
export declare const CharacterSearchInputSchema: z.ZodObject<{
|
|
265
271
|
query: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -449,6 +449,21 @@ export const MalImportInputSchema = z.object({
|
|
|
449
449
|
.default(5)
|
|
450
450
|
.describe("Number of recommendations to return (default 5, max 15)"),
|
|
451
451
|
});
|
|
452
|
+
/** Input for importing a Kitsu user's completed list */
|
|
453
|
+
export const KitsuImportInputSchema = z.object({
|
|
454
|
+
kitsuUsername: z
|
|
455
|
+
.string()
|
|
456
|
+
.min(2)
|
|
457
|
+
.max(30)
|
|
458
|
+
.describe("Kitsu username to import"),
|
|
459
|
+
limit: z
|
|
460
|
+
.number()
|
|
461
|
+
.int()
|
|
462
|
+
.min(1)
|
|
463
|
+
.max(15)
|
|
464
|
+
.default(5)
|
|
465
|
+
.describe("Number of recommendations to return (default 5, max 15)"),
|
|
466
|
+
});
|
|
452
467
|
/** Input for character search */
|
|
453
468
|
export const CharacterSearchInputSchema = z.object({
|
|
454
469
|
query: z
|
package/dist/tools/import.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Import tools: cross-platform list import for recommendations */
|
|
2
2
|
import { fetchMalList, mapMalGenre, mapMalFormat, } from "../api/mal-client.js";
|
|
3
|
+
import { fetchKitsuList, mapKitsuFormat, } from "../api/kitsu-client.js";
|
|
3
4
|
import { anilistClient } from "../api/client.js";
|
|
4
5
|
import { DISCOVER_MEDIA_QUERY } from "../api/queries.js";
|
|
5
|
-
import { MalImportInputSchema } from "../schemas.js";
|
|
6
|
+
import { MalImportInputSchema, KitsuImportInputSchema } from "../schemas.js";
|
|
6
7
|
import { throwToolError, formatMediaSummary } from "../utils.js";
|
|
7
8
|
import { buildTasteProfile, describeTasteProfile, formatTasteProfileText, } from "../engine/taste.js";
|
|
8
9
|
import { matchCandidates } from "../engine/matcher.js";
|
|
@@ -72,8 +73,120 @@ export function registerImportTools(server) {
|
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
75
|
});
|
|
76
|
+
// === Kitsu Import ===
|
|
77
|
+
server.addTool({
|
|
78
|
+
name: "anilist_kitsu_import",
|
|
79
|
+
description: "Import a Kitsu user's completed anime list and generate " +
|
|
80
|
+
"personalized recommendations based on their taste. No auth needed. " +
|
|
81
|
+
"Use when the user mentions their Kitsu account or wants recs from Kitsu history. " +
|
|
82
|
+
"Returns a taste profile summary and recommended titles from AniList.",
|
|
83
|
+
parameters: KitsuImportInputSchema,
|
|
84
|
+
annotations: {
|
|
85
|
+
title: "Kitsu Import",
|
|
86
|
+
readOnlyHint: true,
|
|
87
|
+
destructiveHint: false,
|
|
88
|
+
openWorldHint: true,
|
|
89
|
+
},
|
|
90
|
+
execute: async (args) => {
|
|
91
|
+
try {
|
|
92
|
+
const { entries, anime } = await fetchKitsuList(args.kitsuUsername);
|
|
93
|
+
if (entries.length === 0) {
|
|
94
|
+
return `No completed anime found for Kitsu user "${args.kitsuUsername}".`;
|
|
95
|
+
}
|
|
96
|
+
// Convert to AniList format for taste engine
|
|
97
|
+
const converted = kitsuEntriesToAniList(entries, anime);
|
|
98
|
+
const profile = buildTasteProfile(converted);
|
|
99
|
+
const lines = [
|
|
100
|
+
`# Kitsu Import: ${args.kitsuUsername}`,
|
|
101
|
+
`Imported ${entries.length} completed anime from Kitsu.`,
|
|
102
|
+
"",
|
|
103
|
+
"## Taste Profile",
|
|
104
|
+
describeTasteProfile(profile, args.kitsuUsername),
|
|
105
|
+
...formatTasteProfileText(profile),
|
|
106
|
+
];
|
|
107
|
+
// Fetch AniList candidates using top genres
|
|
108
|
+
const topGenres = profile.genres.slice(0, 3).map((g) => g.name);
|
|
109
|
+
if (topGenres.length > 0) {
|
|
110
|
+
const candidates = await fetchDiscoverCandidates(topGenres);
|
|
111
|
+
// Filter out titles already on Kitsu list
|
|
112
|
+
const kitsuTitles = new Set(Array.from(anime.values()).map((a) => a.attributes.canonicalTitle.toLowerCase()));
|
|
113
|
+
const filtered = candidates.filter((m) => !kitsuTitles.has((m.title.english ?? m.title.romaji ?? "").toLowerCase()));
|
|
114
|
+
const ranked = matchCandidates(filtered, profile).slice(0, args.limit);
|
|
115
|
+
lines.push("", "## Recommendations", "");
|
|
116
|
+
if (ranked.length === 0) {
|
|
117
|
+
lines.push("No new recommendations found.");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
121
|
+
const r = ranked[i];
|
|
122
|
+
lines.push(`${i + 1}. ${formatMediaSummary(r.media)}`);
|
|
123
|
+
if (r.reasons.length > 0) {
|
|
124
|
+
lines.push(` Why: ${r.reasons.slice(0, 3).join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return throwToolError(error, "importing Kitsu list");
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
75
137
|
}
|
|
76
138
|
// === Helpers ===
|
|
139
|
+
// Convert Kitsu entries to AniList format for the taste engine
|
|
140
|
+
function kitsuEntriesToAniList(entries, animeMap) {
|
|
141
|
+
return entries
|
|
142
|
+
.filter((e) => e.attributes.ratingTwenty !== null && e.attributes.ratingTwenty > 0)
|
|
143
|
+
.map((e) => {
|
|
144
|
+
const animeId = e.relationships.anime.data?.id ?? "0";
|
|
145
|
+
const anime = animeMap.get(animeId);
|
|
146
|
+
const title = anime?.attributes.canonicalTitle ?? "Unknown";
|
|
147
|
+
// ratingTwenty is 2-20 scale; convert to 1-10
|
|
148
|
+
const score = Math.round((e.attributes.ratingTwenty ?? 0) / 2);
|
|
149
|
+
const avgRating = anime?.attributes.averageRating;
|
|
150
|
+
return {
|
|
151
|
+
id: parseInt(animeId, 10),
|
|
152
|
+
score,
|
|
153
|
+
progress: e.attributes.progress,
|
|
154
|
+
progressVolumes: 0,
|
|
155
|
+
status: "COMPLETED",
|
|
156
|
+
updatedAt: 0,
|
|
157
|
+
startedAt: { year: null, month: null, day: null },
|
|
158
|
+
completedAt: { year: null, month: null, day: null },
|
|
159
|
+
notes: null,
|
|
160
|
+
media: {
|
|
161
|
+
id: parseInt(animeId, 10),
|
|
162
|
+
type: "ANIME",
|
|
163
|
+
title: { romaji: title, english: title, native: null },
|
|
164
|
+
format: mapKitsuFormat(anime?.attributes.subtype ?? "TV"),
|
|
165
|
+
status: "FINISHED",
|
|
166
|
+
episodes: anime?.attributes.episodeCount ?? null,
|
|
167
|
+
duration: null,
|
|
168
|
+
chapters: null,
|
|
169
|
+
volumes: null,
|
|
170
|
+
meanScore: avgRating ? Math.round(parseFloat(avgRating)) : null,
|
|
171
|
+
averageScore: null,
|
|
172
|
+
popularity: null,
|
|
173
|
+
genres: [],
|
|
174
|
+
tags: [],
|
|
175
|
+
season: null,
|
|
176
|
+
seasonYear: null,
|
|
177
|
+
startDate: { year: null, month: null, day: null },
|
|
178
|
+
endDate: { year: null, month: null, day: null },
|
|
179
|
+
studios: { nodes: [] },
|
|
180
|
+
source: null,
|
|
181
|
+
isAdult: false,
|
|
182
|
+
coverImage: { large: null, extraLarge: null },
|
|
183
|
+
trailer: null,
|
|
184
|
+
siteUrl: `https://kitsu.io/anime/${animeId}`,
|
|
185
|
+
description: null,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
77
190
|
// Convert MAL entries to AniList format for the taste engine
|
|
78
191
|
function malEntriesToAniList(entries) {
|
|
79
192
|
return entries
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "ani-mcp",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"display_name": "AniList MCP",
|
|
6
6
|
"description": "A smart MCP server for AniList that gets your anime/manga taste - not just API calls.",
|
|
7
7
|
"author": {
|
|
@@ -179,6 +179,30 @@
|
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"name": "anilist_compat_card"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "anilist_mal_import"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "anilist_kitsu_import"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "anilist_lookup"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "anilist_airing"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "anilist_group_pick"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "anilist_shared_planning"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "anilist_follow_suggestions"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "anilist_react"
|
|
182
206
|
}
|
|
183
207
|
]
|
|
184
208
|
}
|
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.
|
|
4
|
+
"version": "0.13.0",
|
|
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.
|
|
9
|
+
"version": "0.13.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ani-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.13.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|