@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
209 lines • 8.37 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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 AnkiConnectClient_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnkiConnectClient = exports.ReadOnlyModeError = exports.AnkiConnectError = void 0;
exports.__resetAnkiQueueForTests = __resetAnkiQueueForTests;
const common_1 = require("@nestjs/common");
const async_mutex_1 = require("async-mutex");
const ky_1 = __importStar(require("ky"));
const anki_config_interface_1 = require("../config/anki-config.interface");
const ankiRequestMutex = new async_mutex_1.Mutex();
const MAX_QUEUE_DEPTH = 50;
let pendingRequests = 0;
function __resetAnkiQueueForTests() {
pendingRequests = 0;
}
const WRITE_ACTIONS = new Set([
"addNote",
"updateNoteFields",
"deleteNotes",
"createDeck",
"changeDeck",
"addTags",
"removeTags",
"clearUnusedTags",
"replaceTags",
"storeMediaFile",
"deleteMediaFile",
"createModel",
"updateModelStyling",
"updateModelTemplates",
"modelFieldAdd",
"modelFieldRemove",
"modelFieldRename",
"modelFieldReposition",
"forgetCards",
"setDueDate",
"guiUndo",
"guiAddCards",
"guiEditNote",
]);
class AnkiConnectError extends Error {
action;
originalError;
constructor(message, action, originalError) {
super(message);
this.action = action;
this.originalError = originalError;
this.name = "AnkiConnectError";
}
}
exports.AnkiConnectError = AnkiConnectError;
class ReadOnlyModeError extends Error {
action;
constructor(action) {
super(`Action "${action}" is blocked: server is running in read-only mode. ` +
`Write operations are disabled. Remove the --read-only flag to enable writes.`);
this.action = action;
this.name = "ReadOnlyModeError";
}
}
exports.ReadOnlyModeError = ReadOnlyModeError;
let AnkiConnectClient = AnkiConnectClient_1 = class AnkiConnectClient {
config;
client;
apiVersion;
apiKey;
readOnly;
logger = new common_1.Logger(AnkiConnectClient_1.name);
constructor(config) {
this.config = config;
this.apiVersion = config.ankiConnectApiVersion;
this.apiKey = config.ankiConnectApiKey;
this.readOnly = config.readOnly ?? false;
this.client = ky_1.default.create({
prefix: config.ankiConnectUrl,
timeout: config.ankiConnectTimeout,
headers: {
"Content-Type": "application/json",
},
retry: {
limit: 2,
methods: ["POST"],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
backoffLimit: 3000,
},
hooks: {
beforeRequest: [
({ request }) => {
this.logger.debug(`AnkiConnect request: ${request.method} ${request.url}`);
},
],
afterResponse: [
({ response }) => {
this.logger.debug(`AnkiConnect response: ${response.status} ${response.statusText}`);
},
],
},
});
}
async invoke(action, params) {
if (this.readOnly && WRITE_ACTIONS.has(action)) {
this.logger.warn(`Blocked write action "${action}" in read-only mode`);
throw new ReadOnlyModeError(action);
}
const request = {
action,
version: this.apiVersion,
params,
};
if (this.apiKey) {
request.key = this.apiKey;
}
if (pendingRequests >= MAX_QUEUE_DEPTH) {
this.logger.warn(`Rejecting action "${action}": ${pendingRequests} requests already pending (max ${MAX_QUEUE_DEPTH})`);
throw new AnkiConnectError(`Too many concurrent requests queued for AnkiConnect (max ${MAX_QUEUE_DEPTH}). ` +
`For bulk note creation, use the addNotes batch tool instead of many parallel addNote calls.`, action);
}
pendingRequests++;
try {
const result = await ankiRequestMutex.runExclusive(async () => {
this.logger.log(`Invoking AnkiConnect action: ${action}`);
const response = await this.client
.post("", {
json: request,
})
.json();
if (response.error) {
throw new AnkiConnectError(`AnkiConnect error: ${response.error}`, action, response.error);
}
this.logger.log(`AnkiConnect action successful: ${action}`);
return response.result;
});
return result;
}
catch (error) {
if (error instanceof ReadOnlyModeError) {
throw error;
}
if (error instanceof ky_1.HTTPError) {
if (error.response.status === 403) {
throw new AnkiConnectError("Permission denied. Please check AnkiConnect configuration and API key.", action);
}
throw new AnkiConnectError(`HTTP error ${error.response.status}: ${error.message}`, action);
}
if (error instanceof Error && error.message.includes("fetch")) {
throw new AnkiConnectError("Cannot connect to Anki. Please ensure Anki is running and AnkiConnect plugin is installed.", action);
}
if (error instanceof AnkiConnectError) {
throw error;
}
throw new AnkiConnectError(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`, action);
}
finally {
pendingRequests--;
}
}
};
exports.AnkiConnectClient = AnkiConnectClient;
exports.AnkiConnectClient = AnkiConnectClient = AnkiConnectClient_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(anki_config_interface_1.ANKI_CONFIG)),
__metadata("design:paramtypes", [Object])
], AnkiConnectClient);
//# sourceMappingURL=anki-connect.client.js.map