@arielbk/anki-mcp
Version:
MCP server for integrating with Anki flashcards through conversational AI
1,621 lines (1,619 loc) • 108 kB
JavaScript
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { YankiConnect } from 'yanki-connect';
import { z } from 'zod';
var ankiClient = new YankiConnect();
// src/resources/decks.ts
function registerDeckResources(server2) {
server2.resource("deck_names", "anki:///decks/names", async (uri) => {
try {
const deckNames = await ankiClient.deck.deckNames();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(deckNames, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get deck names: ${error instanceof Error ? error.message : String(error)}`
);
}
});
server2.resource("deck_names_and_ids", "anki:///decks/names-and-ids", async (uri) => {
try {
const deckNamesAndIds = await ankiClient.deck.deckNamesAndIds();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(deckNamesAndIds, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get deck names and IDs: ${error instanceof Error ? error.message : String(error)}`
);
}
});
server2.resource(
"deck_config",
new ResourceTemplate("anki:///decks/{deckName}/config", { list: void 0 }),
async (uri, { deckName }) => {
const deckNameString = Array.isArray(deckName) ? deckName[0] : deckName;
if (!deckNameString) {
throw new Error("Deck name is required");
}
try {
const config = await ankiClient.deck.getDeckConfig({ deck: deckNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(config, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get deck config for "${deckNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"deck_stats",
new ResourceTemplate("anki:///decks/{deckNames}/stats", { list: void 0 }),
async (uri, { deckNames }) => {
const deckNamesString = Array.isArray(deckNames) ? deckNames[0] : deckNames;
if (!deckNamesString) {
throw new Error("Deck names are required");
}
const deckNamesArray = deckNamesString.split(",").map((name) => name.trim());
if (deckNamesArray.length === 0) {
throw new Error("At least one deck name is required");
}
try {
const stats = await ankiClient.deck.getDeckStats({ decks: deckNamesArray });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(stats, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get deck stats for "${deckNamesArray.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"decks_by_cards",
new ResourceTemplate("anki:///decks/by-cards/{cardIds}", { list: void 0 }),
async (uri, { cardIds }) => {
const cardIdsString = Array.isArray(cardIds) ? cardIds[0] : cardIds;
if (!cardIdsString) {
throw new Error("Card IDs are required");
}
const cardIdArray = cardIdsString.split(",").map((id) => {
const num = parseInt(id.trim(), 10);
if (isNaN(num)) {
throw new Error(`Invalid card ID: ${id}`);
}
return num;
});
if (cardIdArray.length === 0) {
throw new Error("At least one card ID is required");
}
try {
const decks = await ankiClient.deck.getDecks({ cards: cardIdArray });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(decks, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get decks for cards "${cardIdArray.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function parseCardIds(uri) {
const cardIdsParam = uri.pathname.split("/").slice(-2)[0];
if (!cardIdsParam) {
throw new Error("Card IDs parameter is missing from URI");
}
const cardIds = cardIdsParam.split(",").map((id) => parseInt(id.trim(), 10));
if (cardIds.some((id) => isNaN(id))) {
throw new Error("Invalid card IDs provided");
}
return cardIds;
}
function registerCardResources(server2) {
server2.resource(
"find_cards",
new ResourceTemplate("anki:///cards/search/{query}", { list: void 0 }),
async (uri) => {
try {
const query = decodeURIComponent(uri.pathname.split("/").pop() || "");
if (!query) {
throw new Error("Query parameter is required");
}
const cardIds = await ankiClient.card.findCards({ query });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(cardIds, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to find cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_info",
new ResourceTemplate("anki:///cards/{cardIds}/info", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const cardsInfo = await ankiClient.card.cardsInfo({ cards: cardIds });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(cardsInfo, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get cards info: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_due_status",
new ResourceTemplate("anki:///cards/{cardIds}/due", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const areDue = await ankiClient.card.areDue({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
isDue: areDue[index]
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to check if cards are due: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_suspend_status",
new ResourceTemplate("anki:///cards/{cardIds}/suspended", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const areSuspended = await ankiClient.card.areSuspended({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
isSuspended: areSuspended[index]
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to check if cards are suspended: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_mod_time",
new ResourceTemplate("anki:///cards/{cardIds}/mod-time", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const modTimes = await ankiClient.card.cardsModTime({ cards: cardIds });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(modTimes, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get cards modification time: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_to_notes",
new ResourceTemplate("anki:///cards/{cardIds}/notes", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const noteIds = await ankiClient.card.cardsToNotes({ cards: cardIds });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(noteIds, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get note IDs from cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_ease_factors",
new ResourceTemplate("anki:///cards/{cardIds}/ease-factors", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const easeFactors = await ankiClient.card.getEaseFactors({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
easeFactor: easeFactors[index]
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get ease factors: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_intervals",
new ResourceTemplate("anki:///cards/{cardIds}/intervals", { list: void 0 }),
async (uri) => {
try {
const cardIds = parseCardIds(uri);
const url = new URL(uri.href);
const complete = url.searchParams.get("complete") === "true";
const intervals = await ankiClient.card.getIntervals({ cards: cardIds, complete });
const result = cardIds.map((cardId, index) => ({
cardId,
intervals: intervals[index]
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get intervals: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function parseNoteIds(uri) {
const noteIdsParam = uri.pathname.split("/").slice(-2)[0];
if (!noteIdsParam) {
throw new Error("Note IDs parameter is missing from URI");
}
const noteIds = noteIdsParam.split(",").map((id) => parseInt(id.trim(), 10));
if (noteIds.some((id) => isNaN(id))) {
throw new Error("Invalid note IDs provided");
}
return noteIds;
}
function registerNoteResources(server2) {
server2.resource("note_tags", "anki:///notes/tags", async (uri) => {
try {
const tags = await ankiClient.note.getTags();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
tags: tags.sort(),
count: tags.length,
description: "All available tags in the Anki collection"
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get tags: ${error instanceof Error ? error.message : String(error)}`
);
}
});
server2.resource(
"notes_info",
new ResourceTemplate("anki:///notes/{noteIds}/info", { list: void 0 }),
async (uri) => {
try {
const noteIds = parseNoteIds(uri);
const notesInfo = await ankiClient.note.notesInfo({ notes: noteIds });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
notes: notesInfo,
count: notesInfo.length,
description: "Detailed information about the requested notes"
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get notes info: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"notes_search",
new ResourceTemplate("anki:///notes/search/{query}", { list: void 0 }),
async (uri) => {
try {
const query = decodeURIComponent(uri.pathname.split("/").pop() || "");
if (!query) {
throw new Error("Search query is required");
}
const noteIds = await ankiClient.note.findNotes({ query });
const limitedIds = noteIds.slice(0, 50);
const notesInfo = limitedIds.length > 0 ? await ankiClient.note.notesInfo({ notes: limitedIds }) : [];
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
query,
totalFound: noteIds.length,
displayedCount: limitedIds.length,
noteIds,
notes: notesInfo,
description: `Search results for query: "${query}". Showing detailed info for first 50 notes.`
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to search notes: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"notes_by_tag",
new ResourceTemplate("anki:///notes/tag/{tag}", { list: void 0 }),
async (uri) => {
try {
const tag = decodeURIComponent(uri.pathname.split("/").pop() || "");
if (!tag) {
throw new Error("Tag is required");
}
const query = `tag:${tag}`;
const noteIds = await ankiClient.note.findNotes({ query });
const limitedIds = noteIds.slice(0, 50);
const notesInfo = limitedIds.length > 0 ? await ankiClient.note.notesInfo({ notes: limitedIds }) : [];
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
tag,
totalFound: noteIds.length,
displayedCount: limitedIds.length,
noteIds,
notes: notesInfo,
description: `Notes tagged with "${tag}". Showing detailed info for first 50 notes.`
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get notes by tag: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"notes_recent",
new ResourceTemplate("anki:///notes/recent/{days}", { list: void 0 }),
async (uri) => {
try {
const daysParam = uri.pathname.split("/").pop() || "7";
const days = parseInt(daysParam, 10);
if (isNaN(days) || days <= 0) {
throw new Error("Days must be a positive number");
}
const query = `edited:${days}`;
const noteIds = await ankiClient.note.findNotes({ query });
const limitedIds = noteIds.slice(0, 50);
const notesInfo = limitedIds.length > 0 ? await ankiClient.note.notesInfo({ notes: limitedIds }) : [];
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
days,
totalFound: noteIds.length,
displayedCount: limitedIds.length,
noteIds,
notes: notesInfo,
description: `Notes modified in the last ${days} days. Showing detailed info for first 50 notes.`
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get recent notes: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"notes_mod_times",
new ResourceTemplate("anki:///notes/{noteIds}/mod-times", { list: void 0 }),
async (uri) => {
try {
const noteIds = parseNoteIds(uri);
const modTimes = await ankiClient.note.notesModTime({ notes: noteIds });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
modificationTimes: modTimes,
count: modTimes.length,
description: "Modification timestamps for the requested notes"
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get note modification times: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function registerModelResources(server2) {
server2.resource("model_names", "anki:///models/names", async (uri) => {
try {
const modelNames = await ankiClient.model.modelNames();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(modelNames, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get model names: ${error instanceof Error ? error.message : String(error)}`
);
}
});
server2.resource("model_names_and_ids", "anki:///models/names-and-ids", async (uri) => {
try {
const modelNamesAndIds = await ankiClient.model.modelNamesAndIds();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(modelNamesAndIds, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get model names and IDs: ${error instanceof Error ? error.message : String(error)}`
);
}
});
server2.resource(
"models_by_id",
new ResourceTemplate("anki:///models/by-id/{modelIds}", { list: void 0 }),
async (uri, { modelIds }) => {
const modelIdsString = Array.isArray(modelIds) ? modelIds[0] : modelIds;
if (!modelIdsString) {
throw new Error("Model IDs are required");
}
const modelIdArray = modelIdsString.split(",").map((id) => {
const num = parseInt(id.trim(), 10);
if (isNaN(num)) {
throw new Error(`Invalid model ID: ${id}`);
}
return num;
});
if (modelIdArray.length === 0) {
throw new Error("At least one model ID is required");
}
try {
const models = await ankiClient.model.findModelsById({ modelIds: modelIdArray });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(models, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get models by ID "${modelIdArray.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"models_by_name",
new ResourceTemplate("anki:///models/by-name/{modelNames}", { list: void 0 }),
async (uri, { modelNames }) => {
const modelNamesString = Array.isArray(modelNames) ? modelNames[0] : modelNames;
if (!modelNamesString) {
throw new Error("Model names are required");
}
const modelNamesArray = modelNamesString.split(",").map((name) => name.trim());
if (modelNamesArray.length === 0) {
throw new Error("At least one model name is required");
}
try {
const models = await ankiClient.model.findModelsByName({ modelNames: modelNamesArray });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(models, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get models by name "${modelNamesArray.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"model_field_names",
new ResourceTemplate("anki:///models/{modelName}/fields/names", { list: void 0 }),
async (uri, { modelName }) => {
const modelNameString = Array.isArray(modelName) ? modelName[0] : modelName;
if (!modelNameString) {
throw new Error("Model name is required");
}
try {
const fieldNames = await ankiClient.model.modelFieldNames({ modelName: modelNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(fieldNames, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get field names for model "${modelNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"model_field_fonts",
new ResourceTemplate("anki:///models/{modelName}/fields/fonts", { list: void 0 }),
async (uri, { modelName }) => {
const modelNameString = Array.isArray(modelName) ? modelName[0] : modelName;
if (!modelNameString) {
throw new Error("Model name is required");
}
try {
const fieldFonts = await ankiClient.model.modelFieldFonts({ modelName: modelNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(fieldFonts, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get field fonts for model "${modelNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"model_fields_on_templates",
new ResourceTemplate("anki:///models/{modelName}/fields/templates", { list: void 0 }),
async (uri, { modelName }) => {
const modelNameString = Array.isArray(modelName) ? modelName[0] : modelName;
if (!modelNameString) {
throw new Error("Model name is required");
}
try {
const fieldsOnTemplates = await ankiClient.model.modelFieldsOnTemplates({
modelName: modelNameString
});
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(fieldsOnTemplates, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get fields on templates for model "${modelNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"model_styling",
new ResourceTemplate("anki:///models/{modelName}/styling", { list: void 0 }),
async (uri, { modelName }) => {
const modelNameString = Array.isArray(modelName) ? modelName[0] : modelName;
if (!modelNameString) {
throw new Error("Model name is required");
}
try {
const styling = await ankiClient.model.modelStyling({ modelName: modelNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(styling, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get styling for model "${modelNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"model_templates",
new ResourceTemplate("anki:///models/{modelName}/templates", { list: void 0 }),
async (uri, { modelName }) => {
const modelNameString = Array.isArray(modelName) ? modelName[0] : modelName;
if (!modelNameString) {
throw new Error("Model name is required");
}
try {
const templates = await ankiClient.model.modelTemplates({ modelName: modelNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(templates, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get templates for model "${modelNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function registerStatisticResources(server2) {
server2.resource(
"cards_reviewed_by_day",
"anki:///statistics/cards-reviewed-by-day",
async (uri) => {
try {
const reviewsByDay = await ankiClient.statistic.getNumCardsReviewedByDay();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(reviewsByDay, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get cards reviewed by day: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"cards_reviewed_today",
"anki:///statistics/cards-reviewed-today",
async (uri) => {
try {
const reviewsToday = await ankiClient.statistic.getNumCardsReviewedToday();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({ cardsReviewedToday: reviewsToday }, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get cards reviewed today: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"card_reviews",
new ResourceTemplate("anki:///statistics/decks/{deckName}/reviews/{startID}", {
list: void 0
}),
async (uri, { deckName, startID }) => {
const deckNameString = Array.isArray(deckName) ? deckName[0] : deckName;
const startIDString = Array.isArray(startID) ? startID[0] : startID;
if (!deckNameString) {
throw new Error("Deck name is required");
}
if (!startIDString) {
throw new Error("Start ID is required");
}
const startIDNumber = parseInt(startIDString, 10);
if (isNaN(startIDNumber)) {
throw new Error(`Invalid start ID: ${startIDString}`);
}
try {
const reviews = await ankiClient.statistic.cardReviews({
deck: deckNameString,
startID: startIDNumber
});
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(reviews, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get card reviews for deck "${deckNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"latest_review_id",
new ResourceTemplate("anki:///statistics/decks/{deckName}/latest-review-id", {
list: void 0
}),
async (uri, { deckName }) => {
const deckNameString = Array.isArray(deckName) ? deckName[0] : deckName;
if (!deckNameString) {
throw new Error("Deck name is required");
}
try {
const reviewID = await ankiClient.statistic.getLatestReviewID({ deck: deckNameString });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
deck: deckNameString,
latestReviewID: reviewID,
hasReviews: reviewID > 0
},
null,
2
)
}
]
};
} catch (error) {
throw new Error(
`Failed to get latest review ID for deck "${deckNameString}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"reviews_of_cards",
new ResourceTemplate("anki:///statistics/cards/{cardIds}/reviews", { list: void 0 }),
async (uri, { cardIds }) => {
const cardIdsString = Array.isArray(cardIds) ? cardIds[0] : cardIds;
if (!cardIdsString) {
throw new Error("Card IDs are required");
}
const cardIdArray = cardIdsString.split(",").map((id) => id.trim());
if (cardIdArray.length === 0) {
throw new Error("At least one card ID is required");
}
try {
const reviews = await ankiClient.statistic.getReviewsOfCards({ cards: cardIdArray });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(reviews, null, 2)
}
]
};
} catch (error) {
throw new Error(
`Failed to get reviews for cards "${cardIdArray.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.resource(
"collection_stats_html",
new ResourceTemplate("anki:///statistics/collection/{wholeCollection}", { list: void 0 }),
async (uri, { wholeCollection }) => {
const wholeCollectionString = Array.isArray(wholeCollection) ? wholeCollection[0] : wholeCollection;
if (!wholeCollectionString) {
throw new Error("wholeCollection parameter is required (true/false)");
}
const isWholeCollection = wholeCollectionString.toLowerCase() === "true";
try {
const statsHTML = await ankiClient.statistic.getCollectionStatsHTML({
wholeCollection: isWholeCollection
});
return {
contents: [
{
uri: uri.href,
mimeType: "text/html",
text: statsHTML
}
]
};
} catch (error) {
throw new Error(
`Failed to get collection statistics: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function registerDeckTools(server2) {
server2.tool(
"create_deck",
{
deckName: z.string().describe("Name of the deck to create")
},
async ({ deckName }) => {
try {
const result = await ankiClient.deck.createDeck({ deck: deckName });
return {
content: [
{
type: "text",
text: `Successfully created deck "${deckName}". Result: ${JSON.stringify(result)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to create deck "${deckName}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"delete_decks",
{
deckNames: z.array(z.string()).describe("Array of deck names to delete"),
deleteCards: z.boolean().default(true).describe("Whether to delete the cards in the decks as well")
},
async ({ deckNames, deleteCards }) => {
try {
await ankiClient.deck.deleteDecks({
decks: deckNames,
cardsToo: deleteCards
// Type assertion needed due to yanki-connect's strict typing
});
return {
content: [
{
type: "text",
text: `Successfully deleted decks: ${deckNames.join(", ")}${deleteCards ? " (including cards)" : " (cards preserved)"}`
}
]
};
} catch (error) {
throw new Error(
`Failed to delete decks "${deckNames.join(", ")}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"change_deck",
{
cardIds: z.array(z.number()).describe("Array of card IDs to move"),
targetDeck: z.string().describe("Name of the target deck to move cards to")
},
async ({ cardIds, targetDeck }) => {
try {
await ankiClient.deck.changeDeck({
cards: cardIds,
deck: targetDeck
});
return {
content: [
{
type: "text",
text: `Successfully moved ${cardIds.length} cards to deck "${targetDeck}"`
}
]
};
} catch (error) {
throw new Error(
`Failed to move cards to deck "${targetDeck}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"clone_deck_config",
{
sourceConfigId: z.number().describe("ID of the deck configuration to clone from"),
newConfigName: z.string().describe("Name for the new cloned configuration")
},
async ({ sourceConfigId, newConfigName }) => {
try {
const result = await ankiClient.deck.cloneDeckConfigId({
cloneFrom: sourceConfigId,
name: newConfigName
});
if (result === false) {
throw new Error("Failed to clone deck configuration - operation returned false");
}
return {
content: [
{
type: "text",
text: `Successfully cloned deck configuration. New config ID: ${result}`
}
]
};
} catch (error) {
throw new Error(
`Failed to clone deck configuration: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"remove_deck_config",
{
configId: z.number().describe("ID of the deck configuration to remove")
},
async ({ configId }) => {
try {
const result = await ankiClient.deck.removeDeckConfigId({ configId });
if (!result) {
throw new Error("Failed to remove deck configuration - operation returned false");
}
return {
content: [
{
type: "text",
text: `Successfully removed deck configuration with ID: ${configId}`
}
]
};
} catch (error) {
throw new Error(
`Failed to remove deck configuration: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"save_deck_config",
{
config: z.object({
id: z.number(),
name: z.string(),
autoplay: z.boolean(),
dyn: z.boolean(),
maxTaken: z.number(),
mod: z.number(),
replayq: z.boolean(),
timer: z.number(),
usn: z.number(),
lapse: z.object({
delays: z.array(z.number()),
leechAction: z.number(),
leechFails: z.number(),
minInt: z.number(),
mult: z.number()
}),
new: z.object({
bury: z.boolean(),
delays: z.array(z.number()),
initialFactor: z.number(),
ints: z.array(z.number()),
order: z.number(),
perDay: z.number(),
separate: z.boolean()
}),
rev: z.object({
bury: z.boolean(),
ease4: z.number(),
fuzz: z.number(),
ivlFct: z.number(),
maxIvl: z.number(),
minSpace: z.number(),
perDay: z.number()
})
}).describe("Complete deck configuration object to save")
},
async ({ config }) => {
try {
const result = await ankiClient.deck.saveDeckConfig({ config });
if (!result) {
throw new Error("Failed to save deck configuration - operation returned false");
}
return {
content: [
{
type: "text",
text: `Successfully saved deck configuration "${config.name}" (ID: ${config.id})`
}
]
};
} catch (error) {
throw new Error(
`Failed to save deck configuration: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"set_deck_config",
{
configId: z.number().describe("ID of the configuration to apply"),
deckNames: z.array(z.string()).describe("Array of deck names to apply the configuration to")
},
async ({ configId, deckNames }) => {
try {
const result = await ankiClient.deck.setDeckConfigId({
configId,
decks: deckNames
});
if (!result) {
throw new Error("Failed to set deck configuration - operation returned false");
}
return {
content: [
{
type: "text",
text: `Successfully applied configuration ${configId} to decks: ${deckNames.join(", ")}`
}
]
};
} catch (error) {
throw new Error(
`Failed to set deck configuration: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
}
function registerCardTools(server2) {
server2.tool(
"answer_cards",
{
answers: z.array(
z.object({
cardId: z.number().describe("ID of the card to answer"),
ease: z.number().min(1).max(4).describe("Ease rating: 1 (Again), 2 (Hard), 3 (Good), 4 (Easy)")
})
).describe("Array of card answers")
},
async ({ answers }) => {
try {
const results = await ankiClient.card.answerCards({ answers });
const successCount = results.filter(Boolean).length;
const failureCount = results.length - successCount;
return {
content: [
{
type: "text",
text: `Answered ${successCount} cards successfully, ${failureCount} failed. Results: ${JSON.stringify(results)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to answer cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"find_cards",
{
query: z.string().describe("Anki search query to find cards")
},
async ({ query }) => {
try {
const cardIds = await ankiClient.card.findCards({ query });
return {
content: [
{
type: "text",
text: `Found ${cardIds.length} cards matching query "${query}": ${JSON.stringify(cardIds)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to find cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"get_cards_info",
{
cardIds: z.array(z.number()).describe("Array of card IDs to get information for")
},
async ({ cardIds }) => {
try {
const cardsInfo = await ankiClient.card.cardsInfo({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Card information: ${JSON.stringify(cardsInfo, null, 2)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to get cards info: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"check_cards_due",
{
cardIds: z.array(z.number()).describe("Array of card IDs to check")
},
async ({ cardIds }) => {
try {
const areDue = await ankiClient.card.areDue({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
isDue: areDue[index]
}));
const dueCount = areDue.filter(Boolean).length;
return {
content: [
{
type: "text",
text: `${dueCount} out of ${cardIds.length} cards are due. Details: ${JSON.stringify(result)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to check if cards are due: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"check_cards_suspended",
{
cardIds: z.array(z.number()).describe("Array of card IDs to check")
},
async ({ cardIds }) => {
try {
const areSuspended = await ankiClient.card.areSuspended({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
isSuspended: areSuspended[index]
}));
const suspendedCount = areSuspended.filter((status) => status === true).length;
return {
content: [
{
type: "text",
text: `${suspendedCount} out of ${cardIds.length} cards are suspended. Details: ${JSON.stringify(result)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to check if cards are suspended: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"suspend_cards",
{
cardIds: z.array(z.number()).describe("Array of card IDs to suspend")
},
async ({ cardIds }) => {
try {
const result = await ankiClient.card.suspend({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Successfully suspended ${cardIds.length} cards. Result: ${result}`
}
]
};
} catch (error) {
throw new Error(
`Failed to suspend cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"unsuspend_cards",
{
cardIds: z.array(z.number()).describe("Array of card IDs to unsuspend")
},
async ({ cardIds }) => {
try {
const result = await ankiClient.card.unsuspend({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Successfully unsuspended ${cardIds.length} cards. Result: ${result}`
}
]
};
} catch (error) {
throw new Error(
`Failed to unsuspend cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"check_card_suspended",
{
cardId: z.number().describe("Card ID to check")
},
async ({ cardId }) => {
try {
const isSuspended = await ankiClient.card.suspended({ card: cardId });
return {
content: [
{
type: "text",
text: `Card ${cardId} is ${isSuspended ? "suspended" : "not suspended"}`
}
]
};
} catch (error) {
throw new Error(
`Failed to check if card is suspended: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"forget_cards",
{
cardIds: z.array(z.number()).describe("Array of card IDs to forget")
},
async ({ cardIds }) => {
try {
await ankiClient.card.forgetCards({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Successfully forgot ${cardIds.length} cards, making them new again`
}
]
};
} catch (error) {
throw new Error(
`Failed to forget cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"relearn_cards",
{
cardIds: z.array(z.number()).describe("Array of card IDs to relearn")
},
async ({ cardIds }) => {
try {
await ankiClient.card.relearnCards({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Successfully set ${cardIds.length} cards to relearn`
}
]
};
} catch (error) {
throw new Error(
`Failed to relearn cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"set_cards_due_date",
{
cardIds: z.array(z.number()).describe("Array of card IDs to set due date for"),
days: z.string().describe("Number of days from today (can be negative for past dates)")
},
async ({ cardIds, days }) => {
try {
const result = await ankiClient.card.setDueDate({ cards: cardIds, days });
return {
content: [
{
type: "text",
text: `Successfully set due date for cards. Result: ${result}`
}
]
};
} catch (error) {
throw new Error(
`Failed to set due date for cards: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"get_cards_ease_factors",
{
cardIds: z.array(z.number()).describe("Array of card IDs to get ease factors for")
},
async ({ cardIds }) => {
try {
const easeFactors = await ankiClient.card.getEaseFactors({ cards: cardIds });
const result = cardIds.map((cardId, index) => ({
cardId,
easeFactor: easeFactors[index]
}));
return {
content: [
{
type: "text",
text: `Ease factors: ${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to get ease factors: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"set_cards_ease_factors",
{
cardIds: z.array(z.number()).describe("Array of card IDs to set ease factors for"),
easeFactors: z.array(z.number()).describe("Array of ease factors (must match the length of cardIds)")
},
async ({ cardIds, easeFactors }) => {
try {
if (cardIds.length !== easeFactors.length) {
throw new Error("Number of card IDs must match number of ease factors");
}
const results = await ankiClient.card.setEaseFactors({
cards: cardIds,
easeFactors
});
const successCount = results.filter(Boolean).length;
return {
content: [
{
type: "text",
text: `Successfully set ease factors for ${successCount} out of ${cardIds.length} cards`
}
]
};
} catch (error) {
throw new Error(
`Failed to set ease factors: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"get_cards_intervals",
{
cardIds: z.array(z.number()).describe("Array of card IDs to get intervals for"),
complete: z.boolean().optional().describe("If true, returns all intervals; if false, returns only the most recent")
},
async ({ cardIds, complete = false }) => {
try {
const intervals = await ankiClient.card.getIntervals({ cards: cardIds, complete });
const result = cardIds.map((cardId, index) => ({
cardId,
intervals: intervals[index]
}));
return {
content: [
{
type: "text",
text: `Intervals (${complete ? "complete history" : "most recent"}): ${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to get intervals: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"get_cards_mod_time",
{
cardIds: z.array(z.number()).describe("Array of card IDs to get modification time for")
},
async ({ cardIds }) => {
try {
const modTimes = await ankiClient.card.cardsModTime({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Modification times: ${JSON.stringify(modTimes, null, 2)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to get modification times: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
server2.tool(
"cards_to_notes",
{
cardIds: z.array(z.number()).describe("Array of card IDs to get note IDs for")
},
async ({ cardIds }) => {
try {
const noteIds = await ankiClient.card.cardsToNotes({ cards: cardIds });
return {
content: [
{
type: "text",
text: `Note IDs: ${JSON.stringify(noteIds)}`
}
]
};
} catch (error) {
throw new Error(
`Failed to get note IDs from cards: ${error instanceof Error ? error.message