@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
212 lines • 9.96 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 AddNotesTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AddNotesTool = 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");
let AddNotesTool = AddNotesTool_1 = class AddNotesTool {
ankiClient;
logger = new common_1.Logger(AddNotesTool_1.name);
constructor(ankiClient) {
this.ankiClient = ankiClient;
}
async addNotes({ deckName, modelName, tags: sharedTags, allowDuplicate, duplicateScope, notes, }) {
try {
this.logger.log(`Adding ${notes.length} notes to deck "${deckName}" with model "${modelName}"`);
const fieldNames = await this.ankiClient.invoke("modelFieldNames", { modelName });
if (!fieldNames || fieldNames.length === 0) {
return (0, anki_utils_1.createErrorResponse)(new Error(`Model "${modelName}" not found or has no fields`), {
deckName,
modelName,
totalRequested: notes.length,
hint: "modelNames lists the available models",
});
}
const sortField = fieldNames[0];
const sortFieldErrors = [];
for (let i = 0; i < notes.length; i++) {
const sortFieldValue = notes[i].fields[sortField];
if (!sortFieldValue || sortFieldValue.trim() === "") {
sortFieldErrors.push({
index: i,
error: `The first field "${sortField}" cannot be empty. Anki requires the sort field to have content.`,
});
}
}
if (sortFieldErrors.length > 0) {
return (0, anki_utils_1.createErrorResponse)(new Error(`${sortFieldErrors.length} note(s) have empty sort field "${sortField}"`), {
deckName,
modelName,
totalRequested: notes.length,
invalidNotes: sortFieldErrors,
hint: `The first field "${sortField}" is the sort field and must contain non-empty content for every note.`,
});
}
const results = [];
let createdCount = 0;
let skippedCount = 0;
let failedCount = 0;
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
const mergedTags = [
...new Set([...(sharedTags ?? []), ...(note.tags ?? [])]),
];
const noteParams = {
deckName,
modelName,
fields: note.fields,
};
if (mergedTags.length > 0) {
noteParams.tags = mergedTags;
}
const options = {};
let hasOptions = false;
if (allowDuplicate !== undefined) {
options.allowDuplicate = allowDuplicate;
hasOptions = true;
}
if (duplicateScope !== undefined) {
options.duplicateScope = duplicateScope;
hasOptions = true;
}
if (hasOptions) {
noteParams.options = options;
}
try {
const noteId = await this.ankiClient.invoke("addNote", { note: noteParams });
if (noteId != null) {
results.push({ index: i, status: "created", noteId });
createdCount++;
}
else {
results.push({ index: i, status: "skipped", reason: "duplicate" });
skippedCount++;
}
}
catch (error) {
if (error instanceof anki_connect_client_1.ReadOnlyModeError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("duplicate") ||
errorMessage.includes("cannot create note because it is a duplicate")) {
results.push({ index: i, status: "skipped", reason: "duplicate" });
skippedCount++;
}
else {
results.push({ index: i, status: "failed", error: errorMessage });
failedCount++;
}
}
}
this.logger.log(`Batch complete: ${createdCount} created, ${skippedCount} skipped, ${failedCount} failed`);
return {
success: createdCount > 0 || (failedCount === 0 && skippedCount > 0),
deckName,
modelName,
totalRequested: notes.length,
created: createdCount,
skipped: skippedCount,
failed: failedCount,
results,
};
}
catch (error) {
this.logger.error("Failed to add notes batch", error);
return (0, anki_utils_1.createErrorResponse)(error, {
deckName,
modelName,
totalRequested: notes.length,
hint: "This can happen when Anki is not running or the deck/model names are wrong",
});
}
}
};
exports.AddNotesTool = AddNotesTool;
__decorate([
(0, mcp_nest_1.Tool)({
name: "addNotes",
description: "Add multiple notes to Anki in a single batch — faster than repeated addNote calls for more than one note. Up to 100 notes sharing the same deck and model. Duplicates are skipped individually; validation errors (empty required fields, bad tags) reject the batch. The notes and their cards are added to the user's collection and appear in their reviews.",
parameters: zod_1.z.object({
deckName: zod_1.z.string().min(1).describe("The deck to add all notes to"),
modelName: zod_1.z
.string()
.min(1)
.describe('The note type/model for all notes (e.g., "Basic", "Cloze")'),
tags: zod_1.z
.array(zod_1.z.string())
.optional()
.describe("Tags applied to all notes (merged with per-note tags)"),
allowDuplicate: zod_1.z
.boolean()
.optional()
.default(false)
.describe("Whether to allow adding duplicate notes"),
duplicateScope: zod_1.z
.enum(["deck", "collection"])
.optional()
.describe("Scope for duplicate checking"),
notes: zod_1.z
.array(zod_1.z.object({
fields: zod_1.z
.record(zod_1.z.string(), zod_1.z.string())
.describe('Field values as key-value pairs (e.g., {"Front": "question", "Back": "answer"})'),
tags: zod_1.z
.array(zod_1.z.string())
.optional()
.describe("Additional tags for this specific note (merged with shared tags)"),
}))
.min(1)
.max(100)
.describe("Array of notes to create (1-100)"),
}),
outputSchema: zod_1.z.object({
success: zod_1.z.boolean(),
deckName: zod_1.z.string(),
modelName: zod_1.z.string(),
totalRequested: zod_1.z.number(),
created: zod_1.z.number(),
skipped: zod_1.z.number(),
failed: zod_1.z.number(),
results: zod_1.z.array(zod_1.z.object({
index: zod_1.z.number(),
status: zod_1.z.enum(["created", "skipped", "failed"]),
noteId: zod_1.z.number().optional(),
reason: zod_1.z.string().optional(),
error: zod_1.z.string().optional(),
})),
}),
annotations: {
title: "Batch Add Notes",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
}),
__param(0, (0, microservices_1.Payload)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AddNotesTool.prototype, "addNotes", null);
exports.AddNotesTool = AddNotesTool = AddNotesTool_1 = __decorate([
(0, mcp_nest_1.McpController)(),
__metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient])
], AddNotesTool);
//# sourceMappingURL=add-notes.tool.js.map