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