@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
145 lines • 7.82 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 UnsuspendCardsTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnsuspendCardsTool = exports.unsuspendCardsInputSchema = 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.unsuspendCardsInputSchema = zod_1.z.object({
cards: card_suspension_utils_1.cardIdsSchema,
});
let UnsuspendCardsTool = UnsuspendCardsTool_1 = class UnsuspendCardsTool {
ankiClient;
logger = new common_1.Logger(UnsuspendCardsTool_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 unsuspend: ${cards.length} card(s)`);
if (cards.length === 0) {
throw new Error("cards array is required for unsuspend 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 unsuspended.");
}
const alreadyUnsuspended = before
.filter((status) => status.suspended === false)
.map((status) => status.cardId);
await this.ankiClient.invoke("unsuspend", { cards });
let after = [];
let cardsChanged;
let stillSuspended = 0;
let readBackFailed = false;
try {
after = await (0, card_suspension_utils_1.fetchSuspensionStatuses)(cards, this.ankiClient);
cardsChanged = after.filter((status, index) => status.suspended === false && before[index].suspended !== false).length;
stillSuspended = after.filter((status) => status.suspended !== false).length;
}
catch (readBackError) {
readBackFailed = true;
this.logger.warn(`Unsuspended ${cards.length} card(s) but could not read the new ` +
`suspension state back`, readBackError);
}
const success = readBackFailed || stillSuspended === 0;
if (!readBackFailed) {
this.logger.log(`unsuspend: ${cardsChanged} card(s) newly unsuspended, ${alreadyUnsuspended.length} already unsuspended`);
}
return {
success,
message: readBackFailed
? `Unsuspended ${cards.length} card(s), but reading the new suspension ` +
`state back failed. The unsuspend was applied, so a retry is not needed.`
: success
? `Unsuspended ${cards.length} card(s): ${cardsChanged} newly unsuspended, ` +
`${alreadyUnsuspended.length} already unsuspended`
: `Requested unsuspend of ${cards.length} card(s), but ${stillSuspended} are still suspended`,
cardsRequested: cards.length,
cardsChanged,
alreadyUnsuspended,
cards: after,
};
}
catch (error) {
this.logger.error("Failed to execute unsuspend", error);
return (0, anki_utils_1.createErrorResponse)(error, {
action: "unsuspend",
cardIds: cards,
hint: "This can happen when Anki is not running or the card IDs are invalid (note IDs are not card IDs)",
});
}
}
};
exports.UnsuspendCardsTool = UnsuspendCardsTool;
__decorate([
(0, mcp_nest_1.Tool)({
name: "unsuspend",
description: "Unsuspend cards so they return to normal review. 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 unsuspend action handles " +
"nonexistent IDs inconsistently (usually an error, sometimes a silent skip depending on input " +
"order). Unsuspending a card that's " +
"already unsuspended is a safe no-op, so re-running with the same IDs is safe.",
parameters: exports.unsuspendCardsInputSchema,
outputSchema: zod_1.z.object({
success: zod_1.z
.boolean()
.describe("False means the call ran but the read-back shows some cards are " +
"still 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 suspended before this call and are not suspended now. " +
"Omitted when the read-back failed — the unsuspend was still applied."),
alreadyUnsuspended: zod_1.z
.array(zod_1.z.number())
.describe("Card IDs that were already not 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 unsuspend itself still " +
"succeeded, so check `message` rather than treating an empty array as a no-op."),
}),
annotations: {
title: "Unsuspend 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)
], UnsuspendCardsTool.prototype, "execute", null);
exports.UnsuspendCardsTool = UnsuspendCardsTool = UnsuspendCardsTool_1 = __decorate([
(0, mcp_nest_1.McpController)(),
__metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient])
], UnsuspendCardsTool);
//# sourceMappingURL=unsuspend-cards.tool.js.map