@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
146 lines • 7.89 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 SuspendCardsTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SuspendCardsTool = exports.suspendCardsInputSchema = 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");
const card_suspension_utils_1 = require("../../../utils/card-suspension.utils.js");
exports.suspendCardsInputSchema = zod_1.z.object({
cards: card_suspension_utils_1.cardIdsSchema,
});
let SuspendCardsTool = SuspendCardsTool_1 = class SuspendCardsTool {
ankiClient;
logger = new common_1.Logger(SuspendCardsTool_1.name);
constructor(ankiClient) {
this.ankiClient = ankiClient;
}
async execute(params) {
const cards = Array.isArray(params?.cards)
? [...new Set(params.cards)]
: [];
try {
this.logger.log(`Executing suspend: ${cards.length} card(s)`);
if (cards.length === 0) {
throw new Error("cards array is required for suspend action");
}
const before = await (0, card_suspension_utils_1.fetchSuspensionStatuses)(cards, this.ankiClient);
const missingIds = (0, card_suspension_utils_1.findMissingCardIds)(before);
if (missingIds.length > 0) {
throw new card_validation_utils_1.MissingCardIdsError(missingIds, cards.length, "No cards were suspended.");
}
const alreadySuspended = before
.filter((status) => status.suspended === true)
.map((status) => status.cardId);
await this.ankiClient.invoke("suspend", { cards });
let after = [];
let cardsChanged;
let stillNotSuspended = 0;
let readBackFailed = false;
try {
after = await (0, card_suspension_utils_1.fetchSuspensionStatuses)(cards, this.ankiClient);
cardsChanged = after.filter((status, index) => status.suspended === true && before[index].suspended !== true).length;
stillNotSuspended = after.filter((status) => status.suspended !== true).length;
}
catch (readBackError) {
readBackFailed = true;
this.logger.warn(`Suspended ${cards.length} card(s) but could not read the new ` +
`suspension state back`, readBackError);
}
const success = readBackFailed || stillNotSuspended === 0;
if (!readBackFailed) {
this.logger.log(`suspend: ${cardsChanged} card(s) newly suspended, ${alreadySuspended.length} already suspended`);
}
return {
success,
message: readBackFailed
? `Suspended ${cards.length} card(s), but reading the new suspension ` +
`state back failed. The suspend was applied, so a retry is not needed.`
: success
? `Suspended ${cards.length} card(s): ${cardsChanged} newly suspended, ` +
`${alreadySuspended.length} already suspended`
: `Requested suspend of ${cards.length} card(s), but ${stillNotSuspended} did not end up suspended`,
cardsRequested: cards.length,
cardsChanged,
alreadySuspended,
cards: after,
};
}
catch (error) {
this.logger.error("Failed to execute suspend", error);
return (0, anki_utils_1.createErrorResponse)(error, {
action: "suspend",
cardIds: cards,
hint: "This can happen when Anki is not running or the card IDs are invalid (note IDs are not card IDs)",
});
}
}
};
exports.SuspendCardsTool = SuspendCardsTool;
__decorate([
(0, mcp_nest_1.Tool)({
name: "suspend",
description: "Suspend cards so they're skipped during review until unsuspended. Card IDs (not note IDs) — " +
"use get_cards, get_due_cards, or notesInfo to obtain them. Card IDs are checked for existence " +
"first, and nothing is changed if any is missing — AnkiConnect's own suspend action handles " +
"nonexistent IDs inconsistently (usually an error, sometimes a silent skip depending on input " +
"order). Suspending a card that's " +
"already suspended is a safe no-op, so re-running with the same IDs is safe. A suspended card " +
"silently drops out of the user's reviews (in Anki and on synced devices) until it is unsuspended.",
parameters: exports.suspendCardsInputSchema,
outputSchema: zod_1.z.object({
success: zod_1.z
.boolean()
.describe("False means the call ran but the read-back shows some cards are not " +
"suspended; inspect `cards` to see which. Retrying with the same IDs " +
"is safe. A call that failed outright returns an error instead."),
message: zod_1.z.string(),
cardsRequested: zod_1.z.number(),
cardsChanged: zod_1.z
.number()
.optional()
.describe("Cards that were NOT suspended before this call and are suspended now. " +
"Omitted when the read-back failed — the suspend was still applied."),
alreadySuspended: zod_1.z
.array(zod_1.z.number())
.describe("Card IDs that were already suspended before this call"),
cards: zod_1.z
.array(card_suspension_utils_1.cardSuspensionStatusSchema)
.describe("Final suspension status of each requested card, read back after the " +
"mutation. Empty when the read-back failed — the suspend itself still " +
"succeeded, so check `message` rather than treating an empty array as a no-op."),
}),
annotations: {
title: "Suspend Cards",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
}),
__param(0, (0, microservices_1.Payload)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], SuspendCardsTool.prototype, "execute", null);
exports.SuspendCardsTool = SuspendCardsTool = SuspendCardsTool_1 = __decorate([
(0, mcp_nest_1.McpController)(),
__metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient])
], SuspendCardsTool);
//# sourceMappingURL=suspend-cards.tool.js.map