UNPKG

@ankimcp/anki-mcp-server

Version:

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

155 lines • 7.7 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 GetCardsTool_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.GetCardsTool = 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"); const CardStateEnum = zod_1.z.enum(["due", "new", "learning", "suspended", "buried"]); const CARD_STATE_QUERY_MAP = { due: "is:due", new: "is:new", learning: "is:learn", suspended: "is:suspended", buried: "is:buried", }; let GetCardsTool = GetCardsTool_1 = class GetCardsTool { ankiClient; logger = new common_1.Logger(GetCardsTool_1.name); constructor(ankiClient) { this.ankiClient = ankiClient; } async getCards({ deck_name, card_state = "due", limit, include_answer = false, }) { try { const cardLimit = Math.min(limit || 10, 50); this.logger.log(`Getting ${card_state} cards from deck: ${deck_name || "all"}, limit: ${cardLimit}`); const stateQuery = CARD_STATE_QUERY_MAP[card_state]; const excludeSuspended = card_state !== "suspended" ? "-is:suspended " : ""; let query = `${excludeSuspended}${stateQuery}`; 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 ${card_state} cards found`); return { success: true, message: `No ${card_state} cards found`, cards: [], total: 0, }; } const selectedCardIds = cardIds.slice(0, cardLimit); const cardsInfo = await this.ankiClient.invoke("cardsInfo", { cards: selectedCardIds, }); const existingCards = cardsInfo.filter(card_validation_utils_1.isExistingCardEntry); const total = cardIds.length - (cardsInfo.length - existingCards.length); const cards = 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 ${cards.length} ${card_state} cards out of ${total} total`); return { success: true, cards, total, returned: cards.length, message: `Found ${total} ${card_state} cards, returning ${cards.length}`, }; } catch (error) { this.logger.error(`Failed to get ${card_state || "due"} cards`, error); return (0, anki_utils_1.createErrorResponse)(error); } } }; exports.GetCardsTool = GetCardsTool; __decorate([ (0, mcp_nest_1.Tool)({ name: "get_cards", description: "Retrieve cards from Anki with flexible filtering by deck and card state. 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"), card_state: CardStateEnum.default("due").describe("Filter by card state: 'due' (cards due for review), 'new' (never seen), 'learning' (in learning queue), 'suspended' (manually suspended), 'buried' (temporarily hidden)"), limit: zod_1.z .number() .min(1) .max(50) .default(10) .describe("Maximum number of cards to return"), 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 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 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) ], GetCardsTool.prototype, "getCards", null); exports.GetCardsTool = GetCardsTool = GetCardsTool_1 = __decorate([ (0, mcp_nest_1.McpController)(), __metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient]) ], GetCardsTool); //# sourceMappingURL=get-cards.tool.js.map