UNPKG

@arielbk/anki-mcp

Version:

MCP server for integrating with Anki flashcards through conversational AI

1,621 lines (1,619 loc) 108 kB
#!/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