@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
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 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