ani-mcp 0.15.0 → 0.15.2

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.
@@ -249,7 +249,7 @@ export function warmCache() {
249
249
  const username = process.env.ANILIST_USERNAME;
250
250
  if (!username)
251
251
  return;
252
- log("cache-warm", username);
252
+ log("cache-warm", "starting cache warm for default user");
253
253
  // Fire and forget - don't block startup
254
254
  Promise.all([
255
255
  anilistClient.fetchList(username, "ANIME"),
@@ -0,0 +1,21 @@
1
+ /** Tuning constants used across multiple engine modules */
2
+ export declare const MAX_UNDO = 20;
3
+ export declare const MAX_TAGS = 20;
4
+ export declare const MIN_TAG_COUNT = 3;
5
+ export declare const BAYESIAN_PRIOR_WEIGHT = 0.5;
6
+ export declare const BAYESIAN_PRIOR_COUNT = 3;
7
+ export declare const MAX_DEPTH = 30;
8
+ export declare const CARD_WIDTH = 800;
9
+ export declare const CARD_HEIGHT = 560;
10
+ export declare const COMPAT_CARD_HEIGHT = 640;
11
+ export declare const MATCHER_GENRE_WEIGHT = 0.5;
12
+ export declare const MATCHER_TAG_WEIGHT = 0.3;
13
+ export declare const MATCHER_COMMUNITY_WEIGHT = 0.2;
14
+ export declare const MOOD_BOOST = 1.3;
15
+ export declare const MOOD_PENALTY = 0.6;
16
+ export declare const MIN_COMMUNITY_SCORE = 50;
17
+ export declare const POPULARITY_PENALTY_MAX = 0.15;
18
+ export declare const POPULARITY_CEILING = 100000;
19
+ export declare const SIMILAR_GENRE_WEIGHT = 0.4;
20
+ export declare const SIMILAR_TAG_WEIGHT = 0.3;
21
+ export declare const SIMILAR_REC_WEIGHT = 0.3;
@@ -0,0 +1,43 @@
1
+ /** Tuning constants used across multiple engine modules */
2
+ // === Undo ===
3
+ // Max undo records kept in the session stack
4
+ export const MAX_UNDO = 20;
5
+ // === Taste Profile ===
6
+ // Max tags returned in a taste profile to keep output focused
7
+ export const MAX_TAGS = 20;
8
+ // Tags must appear in at least this many entries to rank
9
+ export const MIN_TAG_COUNT = 3;
10
+ // Bayesian smoothing: pull sparse genre/tag observations toward neutral
11
+ // prior_weight blends prior with observed; prior_count is the pseudocount threshold
12
+ export const BAYESIAN_PRIOR_WEIGHT = 0.5;
13
+ export const BAYESIAN_PRIOR_COUNT = 3;
14
+ // === Franchise ===
15
+ // Max BFS depth to prevent runaway traversal on deeply linked franchises
16
+ export const MAX_DEPTH = 30;
17
+ // === Card Dimensions ===
18
+ // 800x560 fits social media previews (roughly 10:7 aspect ratio)
19
+ export const CARD_WIDTH = 800;
20
+ export const CARD_HEIGHT = 560;
21
+ export const COMPAT_CARD_HEIGHT = 640;
22
+ // === Matcher Weights ===
23
+ // Personal taste matcher (anilist_pick, anilist_explain):
24
+ // genre dominates because users identify most strongly with genre preferences,
25
+ // tags refine within genre, community score guards against niche low-quality titles
26
+ export const MATCHER_GENRE_WEIGHT = 0.5;
27
+ export const MATCHER_TAG_WEIGHT = 0.3;
28
+ export const MATCHER_COMMUNITY_WEIGHT = 0.2;
29
+ // Mood boost/penalty as a multiplier on the final score
30
+ export const MOOD_BOOST = 1.3;
31
+ export const MOOD_PENALTY = 0.6;
32
+ // Minimum community score (out of 100) to avoid poorly-rated titles
33
+ export const MIN_COMMUNITY_SCORE = 50;
34
+ // Diversity nudge: log-scale penalty for very popular titles (max 15%)
35
+ export const POPULARITY_PENALTY_MAX = 0.15;
36
+ export const POPULARITY_CEILING = 100_000;
37
+ // === Similar Weights ===
38
+ // Content similarity matcher (anilist_similar):
39
+ // genre and tag overlap are equally important for content matching,
40
+ // community recs provide a collaborative-filtering signal as a tiebreaker
41
+ export const SIMILAR_GENRE_WEIGHT = 0.4;
42
+ export const SIMILAR_TAG_WEIGHT = 0.3;
43
+ export const SIMILAR_REC_WEIGHT = 0.3;
@@ -1,9 +1,15 @@
1
1
  /** Generates shareable SVG cards for taste profiles, compatibility, and year-in-review */
2
2
  import sharp from "sharp";
3
+ import { CARD_WIDTH, CARD_HEIGHT, COMPAT_CARD_HEIGHT } from "../constants.js";
4
+ // === Logging ===
5
+ const DEBUG = process.env.DEBUG === "true" || process.env.DEBUG === "1";
6
+ function log(event, detail) {
7
+ if (!DEBUG)
8
+ return;
9
+ const msg = detail ? `[ani-mcp] ${event}: ${detail}` : `[ani-mcp] ${event}`;
10
+ console.error(msg);
11
+ }
3
12
  // === Constants ===
4
- const CARD_WIDTH = 800;
5
- const CARD_HEIGHT = 560;
6
- const COMPAT_CARD_HEIGHT = 640;
7
13
  // Brand palette (from assets/icon.svg)
8
14
  const BRAND_BLUE = "#02A9FF";
9
15
  const BRAND_BLUE_DARK = "#0284C7";
@@ -34,7 +40,8 @@ export async function fetchAvatarB64(url) {
34
40
  const contentType = res.headers.get("content-type") ?? "image/jpeg";
35
41
  return `data:${contentType};base64,${buf.toString("base64")}`;
36
42
  }
37
- catch {
43
+ catch (err) {
44
+ log("avatar fetch failed", err instanceof Error ? err.message : String(err));
38
45
  return null;
39
46
  }
40
47
  }
@@ -1,8 +1,7 @@
1
1
  /** Franchise graph traversal for watch order guidance. */
2
+ import { MAX_DEPTH } from "../constants.js";
2
3
  // Main formats in a franchise timeline
3
4
  const MAIN_FORMATS = new Set(["TV", "MOVIE", "ONA", "TV_SHORT"]);
4
- // Max BFS depth to prevent runaway traversal
5
- const MAX_DEPTH = 30;
6
5
  /** Find the earliest entry by following PREQUEL edges backward */
7
6
  function findRoot(startId, relationsMap) {
8
7
  let current = startId;
@@ -1,17 +1,5 @@
1
1
  /** Scores candidate media against a taste profile with natural-language explanations */
2
- // === Constants ===
3
- // How much each signal contributes to the final score
4
- const GENRE_WEIGHT = 0.5;
5
- const TAG_WEIGHT = 0.3;
6
- const COMMUNITY_WEIGHT = 0.2;
7
- // Mood boost/penalty as a multiplier on the final score
8
- const MOOD_BOOST = 1.3;
9
- const MOOD_PENALTY = 0.6;
10
- // Minimum community score (out of 100) to avoid recommending poorly-rated titles
11
- const MIN_COMMUNITY_SCORE = 50;
12
- // Diversity nudge: log-scale penalty for very popular titles (max 15%)
13
- const POPULARITY_PENALTY_MAX = 0.15;
14
- const POPULARITY_CEILING = 100_000;
2
+ import { MATCHER_GENRE_WEIGHT as GENRE_WEIGHT, MATCHER_TAG_WEIGHT as TAG_WEIGHT, MATCHER_COMMUNITY_WEIGHT as COMMUNITY_WEIGHT, MOOD_BOOST, MOOD_PENALTY, MIN_COMMUNITY_SCORE, POPULARITY_PENALTY_MAX, POPULARITY_CEILING, } from "../constants.js";
15
3
  // === Matcher ===
16
4
  /** Score and rank candidates against a user's taste profile */
17
5
  export function matchCandidates(candidates, profile, mood) {
@@ -1,8 +1,9 @@
1
1
  /** Ranks candidates by content similarity to a source title */
2
- // === Constants ===
3
- const GENRE_WEIGHT = 0.4;
4
- const TAG_WEIGHT = 0.3;
5
- const REC_WEIGHT = 0.3;
2
+ import { SIMILAR_GENRE_WEIGHT as GENRE_WEIGHT, SIMILAR_TAG_WEIGHT as TAG_WEIGHT, SIMILAR_REC_WEIGHT as REC_WEIGHT, } from "../constants.js";
3
+ // Non-spoiler tag names from a media entry
4
+ function tagNames(media) {
5
+ return new Set(media.tags.filter((t) => !t.isMediaSpoiler).map((t) => t.name));
6
+ }
6
7
  // === Similarity Engine ===
7
8
  /** Rank candidates by genre/tag overlap and community recommendation strength */
8
9
  export function rankSimilar(source, candidates, recRatings) {
@@ -11,7 +12,7 @@ export function rankSimilar(source, candidates, recRatings) {
11
12
  // Normalize rec ratings to 0-1
12
13
  const maxRating = Math.max(1, ...recRatings.values());
13
14
  const sourceGenres = new Set(source.genres);
14
- const sourceTags = new Set(source.tags.filter((t) => !t.isMediaSpoiler).map((t) => t.name));
15
+ const sourceTags = tagNames(source);
15
16
  const results = [];
16
17
  for (const candidate of candidates) {
17
18
  const reasons = [];
@@ -24,7 +25,7 @@ export function rankSimilar(source, candidates, recRatings) {
24
25
  reasons.push(`Shares genres: ${genreIntersection.join(", ")}`);
25
26
  }
26
27
  // Tag overlap: Jaccard on non-spoiler tags
27
- const candidateTags = new Set(candidate.tags.filter((t) => !t.isMediaSpoiler).map((t) => t.name));
28
+ const candidateTags = tagNames(candidate);
28
29
  const tagIntersection = [...sourceTags].filter((t) => candidateTags.has(t));
29
30
  const tagUnion = new Set([...sourceTags, ...candidateTags]);
30
31
  const tagOverlap = tagUnion.size > 0 ? tagIntersection.length / tagUnion.size : 0;
@@ -1,4 +1,5 @@
1
1
  /** Builds a weighted taste profile from a user's scored anime/manga list */
2
+ import { MAX_TAGS, MIN_TAG_COUNT, BAYESIAN_PRIOR_WEIGHT, BAYESIAN_PRIOR_COUNT, } from "../constants.js";
2
3
  import { dateToEpoch } from "../utils.js";
3
4
  // === Constants ===
4
5
  // AniList community mean hovers around 7.0-7.2
@@ -7,16 +8,9 @@ const SITE_MEAN = 7.0;
7
8
  const MIN_ENTRIES = 5;
8
9
  // Entries scored 0 are unscored on AniList (not a real 0/10)
9
10
  const UNSCORED = 0;
10
- // Cap the number of tags returned to keep output focused
11
- const MAX_TAGS = 20;
12
- // Tags must appear in at least this many entries to rank
13
- const MIN_TAG_COUNT = 3;
14
11
  // Recency decay: entries from HALF_LIFE years ago get ~50% weight
15
12
  const DECAY_HALF_LIFE_YEARS = 3;
16
13
  const DECAY_LAMBDA = Math.LN2 / DECAY_HALF_LIFE_YEARS;
17
- // Bayesian smoothing: pull sparse observations toward a neutral prior
18
- const BAYESIAN_PRIOR_WEIGHT = 0.5;
19
- const BAYESIAN_PRIOR_COUNT = 3;
20
14
  // === Profile Builder ===
21
15
  /** Build a taste profile from scored list entries */
22
16
  export function buildTasteProfile(entries) {
@@ -1,6 +1,6 @@
1
1
  /** Session-scoped undo stack for write operations. */
2
+ import { MAX_UNDO } from "../constants.js";
2
3
  // === Stack ===
3
- const MAX_UNDO = 20;
4
4
  const stack = [];
5
5
  /** Push a record onto the undo stack, trimming oldest if full */
6
6
  export function pushUndo(record) {
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ if (!process.env.ANILIST_TOKEN) {
36
36
  }
37
37
  const server = new FastMCP({
38
38
  name: "ani-mcp",
39
- version: "0.15.0",
39
+ version: "0.15.2",
40
40
  instructions: "ani-mcp is a local MCP server for AniList. " +
41
41
  "Read-only tools work without authentication. " +
42
42
  "Write tools require ANILIST_TOKEN set in the server's environment config. " +
package/dist/resources.js CHANGED
@@ -1,10 +1,16 @@
1
1
  /** MCP Resources: expose user context without tool calls */
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { anilistClient } from "./api/client.js";
3
6
  import { USER_PROFILE_QUERY } from "./api/queries.js";
4
7
  import { buildTasteProfile, describeTasteProfile, formatTasteProfileText, } from "./engine/taste.js";
5
8
  import { formatProfile } from "./tools/social.js";
6
9
  import { formatListEntry } from "./tools/lists.js";
7
10
  import { getDefaultUsername, getScoreFormat } from "./utils.js";
11
+ // Read version from package.json at startup
12
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
13
+ const PKG_VERSION = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
8
14
  /** Register MCP resources on the server */
9
15
  export function registerResources(server) {
10
16
  // === User Profile ===
@@ -100,7 +106,7 @@ export function registerResources(server) {
100
106
  async load() {
101
107
  const lines = ["# ani-mcp Status", ""];
102
108
  // Server version
103
- lines.push(`Version: 0.14.0`);
109
+ lines.push(`Version: ${PKG_VERSION}`);
104
110
  // Auth status
105
111
  const hasToken = Boolean(process.env.ANILIST_TOKEN);
106
112
  const hasUsername = Boolean(process.env.ANILIST_USERNAME);
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "ani-mcp",
4
- "version": "0.15.0",
4
+ "version": "0.15.2",
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": {
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.15.0",
4
+ "version": "0.15.2",
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.15.0",
9
+ "version": "0.15.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ani-mcp",
14
- "version": "0.15.0",
14
+ "version": "0.15.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },