@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
173 lines • 9.08 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 SetDueDateTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SetDueDateTool = exports.setDueDateInputSchema = 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 DAYS_SPEC = /^(\d+)(?:-(\d+))?(!?)$/;
exports.setDueDateInputSchema = zod_1.z.object({
cards: zod_1.z
.array(zod_1.z.number().int().positive())
.min(1)
.max(100)
.describe("Array of card IDs to reschedule (max 100). Card IDs (not note IDs) — use get_cards, " +
"get_due_cards, or notesInfo to obtain them."),
days: zod_1.z
.string()
.describe("How many days from now the cards become due. " +
'"0" = today, "1" = tomorrow, "3-7" = a random day in that inclusive range ' +
"(spreads a batch out instead of stacking it on one day). " +
'Append "!" — e.g. "1!" — to also overwrite the card\'s interval with that value, ' +
"rather than leaving the existing interval in place."),
});
let SetDueDateTool = SetDueDateTool_1 = class SetDueDateTool {
ankiClient;
logger = new common_1.Logger(SetDueDateTool_1.name);
constructor(ankiClient) {
this.ankiClient = ankiClient;
}
async execute(params) {
const cards = Array.isArray(params?.cards)
? [...new Set(params.cards)]
: [];
const rawDays = params?.days;
try {
this.logger.log(`Executing setDueDate: ${cards.length} card(s) -> "${rawDays}"`);
if (cards.length === 0) {
throw new Error("cards array is required for setDueDate action");
}
if (!rawDays || rawDays.trim() === "") {
throw new Error("days is required for setDueDate action");
}
const days = rawDays.trim();
const match = DAYS_SPEC.exec(days);
if (!match) {
throw new Error(`Invalid days spec "${days}". Expected a day count ("0", "5"), an ` +
'inclusive range ("3-7"), optionally suffixed with "!" to also ' +
'overwrite the interval ("1!").');
}
const [, startStr, endStr] = match;
if (endStr !== undefined && Number(endStr) < Number(startStr)) {
throw new Error(`Invalid days range "${days}": the end of the range must not be ` +
`earlier than the start.`);
}
await (0, card_validation_utils_1.assertCardIdsExist)(cards, this.ankiClient, "No cards were rescheduled.");
await this.ankiClient.invoke("setDueDate", { cards, days });
let scheduled = [];
let readBackFailed = false;
try {
const updated = await this.ankiClient.invoke("cardsInfo", { cards });
if (!Array.isArray(updated) || updated.length !== cards.length) {
throw new Error(`cardsInfo read-back returned ${Array.isArray(updated) ? updated.length : typeof updated} entries for ${cards.length} card(s)`);
}
const missingIds = (0, card_validation_utils_1.findMissingIds)(cards, updated);
if (missingIds.length > 0) {
throw new Error(`cardsInfo read-back is missing ${missingIds.length} of ` +
`${cards.length} card(s): [${missingIds.join(", ")}]`);
}
scheduled = cards.map((cardId, index) => ({
cardId,
intervalDays: updated[index]?.interval ?? 0,
due: updated[index]?.due ?? 0,
}));
}
catch (readBackError) {
readBackFailed = true;
this.logger.warn(`Rescheduled ${cards.length} card(s) to "${days}" but could not read ` +
`the new scheduling back`, readBackError);
}
if (!readBackFailed) {
this.logger.log(`Rescheduled ${cards.length} card(s) to "${days}"`);
}
return {
success: true,
message: readBackFailed
? `Successfully rescheduled ${cards.length} card(s) to "${days}", but reading ` +
`the new scheduling back failed. The reschedule was applied, and a retry would reschedule the cards again.`
: `Successfully rescheduled ${cards.length} card(s) to "${days}"`,
cardsAffected: cards.length,
days,
intervalOverwritten: days.endsWith("!"),
scheduled,
};
}
catch (error) {
this.logger.error("Failed to execute setDueDate", error);
return (0, anki_utils_1.createErrorResponse)(error, {
action: "setDueDate",
cardIds: cards,
days: rawDays,
hint: 'This can happen when Anki is not running, the card IDs are invalid (note IDs are not card IDs), or days is not in a form like "0", "5", "3-7", or "1!"',
});
}
}
};
exports.SetDueDateTool = SetDueDateTool;
__decorate([
(0, mcp_nest_1.Tool)({
name: "setDueDate",
description: "Reschedule cards to become due in a given number of days, without recording a review. " +
"Use this to bring a card forward (or push it back) when its current interval doesn't match how well " +
"the user actually knows it — rating the card instead would log a fake review and skew its ease factor. " +
"Unlike forgetCards this keeps the card's existing interval and ease unless you ask otherwise, so it is " +
"the gentler option when the card's history is still worth keeping. " +
"Note that applying this to a new card turns it into a review card.",
parameters: exports.setDueDateInputSchema,
outputSchema: zod_1.z.object({
success: zod_1.z.boolean(),
message: zod_1.z.string(),
cardsAffected: zod_1.z.number(),
days: zod_1.z.string().describe("The due-date spec that was applied"),
intervalOverwritten: zod_1.z
.boolean()
.describe('True when the spec ended in "!", meaning the interval was reset to the day count'),
scheduled: zod_1.z
.array(zod_1.z.object({
cardId: zod_1.z.number(),
intervalDays: zod_1.z
.number()
.describe("The card's interval after rescheduling"),
due: zod_1.z
.number()
.describe("Anki's raw due value: days since collection creation for review cards, " +
"a queue position for new cards, a unix timestamp for learning cards"),
}))
.describe("Per-card scheduling state read back after the change. Empty when the " +
"read-back failed — the reschedule itself still succeeded, so check " +
"`message` rather than treating an empty array as a no-op."),
}),
annotations: {
title: "Set Card Due Date",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
},
}),
__param(0, (0, microservices_1.Payload)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], SetDueDateTool.prototype, "execute", null);
exports.SetDueDateTool = SetDueDateTool = SetDueDateTool_1 = __decorate([
(0, mcp_nest_1.McpController)(),
__metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient])
], SetDueDateTool);
//# sourceMappingURL=set-due-date.tool.js.map