UNPKG

@ankimcp/anki-mcp-server

Version:

Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards

183 lines • 9.01 kB
"use strict"; 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 GetDueCardsTool_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.GetDueCardsTool = 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 card_states_utils_1 = require("../../../utils/card-states.utils.js"); const card_validation_utils_1 = require("../../../utils/card-validation.utils.js"); const anki_utils_1 = require("../../../utils/anki.utils.js"); let GetDueCardsTool = GetDueCardsTool_1 = class GetDueCardsTool { ankiClient; logger = new common_1.Logger(GetDueCardsTool_1.name); constructor(ankiClient) { this.ankiClient = ankiClient; } async getDueCards({ deck_name, limit, include_learning = true, include_new = false, include_answer = false, }) { try { const cardLimit = Math.min(limit || 10, 50); this.logger.log(`Getting due cards from deck: ${deck_name || "all"}, limit: ${cardLimit}`); const states = ["is:due"]; if (include_learning) { states.push("is:learn"); } if (include_new) { states.push("is:new"); } let query = `-is:suspended (${states.join(" OR ")})`; if (deck_name) { query = `${(0, card_states_utils_1.deckScopeQuery)(deck_name)} ${query}`; } const cardIds = await this.ankiClient.invoke("findCards", { query, }); if (cardIds.length === 0) { this.logger.log("No due cards found"); return { success: true, message: "No cards are due for review", cards: [], total: 0, }; } let newIdSet = new Set(); if (include_new) { const newOnlyStates = ["is:new"]; let newQuery = `-is:suspended (${newOnlyStates.join(" OR ")})`; if (deck_name) { newQuery = `${(0, card_states_utils_1.deckScopeQuery)(deck_name)} ${newQuery}`; } try { const newIds = await this.ankiClient.invoke("findCards", { query: newQuery, }); const resultSet = new Set(cardIds); newIdSet = new Set(newIds.filter((id) => resultSet.has(id))); } catch (err) { this.logger.warn(`Could not enumerate new cards for count: ${err instanceof Error ? err.message : String(err)}`); } } const selectedCardIds = cardIds.slice(0, cardLimit); const cardsInfo = await this.ankiClient.invoke("cardsInfo", { cards: selectedCardIds, }); const deletedIds = cardsInfo.flatMap((card, index) => (0, card_validation_utils_1.isExistingCardEntry)(card) ? [] : [selectedCardIds[index]]); const existingCards = cardsInfo.filter(card_validation_utils_1.isExistingCardEntry); const total = cardIds.length - deletedIds.length; const newCount = newIdSet.size - deletedIds.filter((id) => newIdSet.has(id)).length; const dueOnlyCount = total - newCount; const dueCards = existingCards.map((card) => { const { front, back } = (0, anki_utils_1.extractRenderedCardContent)(card); return { cardId: card.cardId, front, ...(include_answer ? { back } : {}), deckName: card.deckName, modelName: card.modelName, due: card.due || 0, interval: card.interval || 0, factor: card.factor || 2500, }; }); this.logger.log(`Retrieved ${dueCards.length} cards out of ${total} total`); const message = include_new ? `Found ${total} cards (${newCount} new, ${dueOnlyCount} due), returning ${dueCards.length}` : `Found ${total} due cards, returning ${dueCards.length}`; return { success: true, cards: dueCards, total, returned: dueCards.length, message, }; } catch (error) { this.logger.error("Failed to get due cards", error); return (0, anki_utils_1.createErrorResponse)(error); } } }; exports.GetDueCardsTool = GetDueCardsTool; __decorate([ (0, mcp_nest_1.Tool)({ name: "get_due_cards", description: "Retrieve cards that are due for review from Anki. Reads the local collection as-is and does not sync with AnkiWeb, so reviews made on other devices since the last sync are not reflected. Answers are not included by default (include_answer=false), so a card's back stays out of the conversation until present_card reveals it; include_answer=true returns the backs too, which suits content analysis or editing rather than a live review.", parameters: zod_1.z.object({ deck_name: zod_1.z .string() .optional() .describe("Specific deck name to get cards from. If not specified, gets cards from all decks"), limit: zod_1.z .number() .min(1) .max(50) .default(10) .describe("Maximum number of cards to return"), include_learning: zod_1.z .boolean() .default(true) .describe("Include cards in learning phase (seen but not yet graduated). Default: true"), include_new: zod_1.z .boolean() .default(false) .describe("Include new cards (never seen before). Default: false"), include_answer: zod_1.z .boolean() .default(false) .describe("Whether to include each card's answer (back). Defaults to false so answers are not seen before the user reveals them via present_card; true suits content analysis or editing rather than live review."), }), outputSchema: zod_1.z.object({ success: zod_1.z.boolean(), cards: zod_1.z.array(zod_1.z.object({ cardId: zod_1.z.number(), front: zod_1.z.string(), back: zod_1.z .string() .optional() .describe("Present only when include_answer=true"), deckName: zod_1.z.string(), modelName: zod_1.z.string(), due: zod_1.z.number(), interval: zod_1.z.number(), factor: zod_1.z.number(), })), total: zod_1.z .number() .describe("Number of cards the due-card search matched, including matches beyond limit, minus cards found deleted among the ones looked up (the first limit matches)"), returned: zod_1.z.number().optional(), message: zod_1.z.string(), }), annotations: { title: "Get Due Cards", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }), __param(0, (0, microservices_1.Payload)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], GetDueCardsTool.prototype, "getDueCards", null); exports.GetDueCardsTool = GetDueCardsTool = GetDueCardsTool_1 = __decorate([ (0, mcp_nest_1.McpController)(), __metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient]) ], GetDueCardsTool); //# sourceMappingURL=get-due-cards.tool.js.map