@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
143 lines • 6.8 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var RateCardTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateCardTool = void 0;
const common_1 = require("@nestjs/common");
const microservices_1 = require("@nestjs/microservices");
const mcp_nest_1 = require("@rekog/mcp-nest");
const zod_1 = require("zod");
const anki_connect_client_1 = require("../../../clients/anki-connect.client.js");
const anki_utils_1 = require("../../../utils/anki.utils.js");
const card_validation_utils_1 = require("../../../utils/card-validation.utils.js");
let RateCardTool = RateCardTool_1 = class RateCardTool {
ankiClient;
logger = new common_1.Logger(RateCardTool_1.name);
constructor(ankiClient) {
this.ankiClient = ankiClient;
}
async rateCard({ card_id, rating }) {
try {
if (!Number.isInteger(rating) || rating < 1 || rating > 4) {
return (0, anki_utils_1.createErrorResponse)(new Error("Invalid rating. Must be 1 (Again), 2 (Hard), 3 (Good), or 4 (Easy)"), { cardId: card_id, attemptedRating: rating });
}
this.logger.log(`Rating card ${card_id} with rating ${rating}`);
const existingInfo = await this.ankiClient.invoke("cardsInfo", { cards: [card_id] });
const found = existingInfo?.[0] && typeof existingInfo[0].cardId === "number";
if (!found) {
return (0, anki_utils_1.createErrorResponse)(new Error(`Card ID ${card_id} does not exist in the Anki collection. Cannot rate.`), {
cardId: card_id,
attemptedRating: rating,
hint: "The card ID may be invalid; get_due_cards returns valid card IDs",
});
}
const answers = [
{
cardId: card_id,
ease: rating,
},
];
const result = await this.ankiClient.invoke("answerCards", {
answers,
});
if (!result) {
throw new Error(`Failed to rate card ${card_id}`);
}
const ratingDesc = (0, anki_utils_1.getRatingDescription)(rating);
this.logger.log(`Card ${card_id} rated as ${ratingDesc}`);
let cardsInfo;
try {
cardsInfo = await this.ankiClient.invoke("cardsInfo", {
cards: [card_id],
});
}
catch (readBackError) {
this.logger.warn(`Card ${card_id} was rated, but reading back its scheduling failed`, readBackError);
}
const card = cardsInfo?.[0];
const nextReview = (0, card_validation_utils_1.isExistingCardEntry)(card)
? {
interval: card.interval || 0,
due: card.due || 0,
factor: card.factor || 2500,
}
: null;
return {
success: true,
cardId: card_id,
rating: rating,
ratingDescription: ratingDesc,
message: nextReview
? `Card successfully rated as ${ratingDesc}`
: `Card successfully rated as ${ratingDesc}; its next review could not be read back from Anki`,
nextReview,
};
}
catch (error) {
this.logger.error(`Failed to rate card ${card_id}`, error);
return (0, anki_utils_1.createErrorResponse)(error, {
cardId: card_id,
attemptedRating: rating,
hint: "This can happen when Anki is not running or the card does not exist",
});
}
}
};
exports.RateCardTool = RateCardTool;
__decorate([
(0, mcp_nest_1.Tool)({
name: "rate_card",
description: "Records a real review of a card in Anki's scheduler: it is added to the card's review history and statistics, and the rating sets the card's next due date. The rating is meant to be the user's own assessment of their recall, given after they have seen the answer (present_card with show_answer=true).",
parameters: zod_1.z.object({
card_id: zod_1.z.number().describe("The ID of the card to rate"),
rating: zod_1.z
.number()
.min(1)
.max(4)
.describe("The user's rating for the card: 1=Again (failed), 2=Hard, 3=Good, 4=Easy"),
}),
outputSchema: zod_1.z.object({
success: zod_1.z.boolean(),
cardId: zod_1.z.number(),
rating: zod_1.z.number(),
ratingDescription: zod_1.z.string(),
message: zod_1.z.string(),
nextReview: zod_1.z
.object({
interval: zod_1.z.number(),
due: zod_1.z.number(),
factor: zod_1.z.number(),
})
.nullable()
.describe("The card's scheduling after the rating, read back from Anki. null when the read-back " +
"failed or found no card, for example because it was deleted after the rating; the rating itself was still recorded."),
}),
annotations: {
title: "Rate Card",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
}),
__param(0, (0, microservices_1.Payload)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], RateCardTool.prototype, "rateCard", null);
exports.RateCardTool = RateCardTool = RateCardTool_1 = __decorate([
(0, mcp_nest_1.McpController)(),
__metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient])
], RateCardTool);
//# sourceMappingURL=rate-card.tool.js.map