UNPKG

@arielbk/anki-mcp

Version:

MCP server for integrating with Anki flashcards through conversational AI

1,583 lines (1,579 loc) 86.5 kB
#!/usr/bin/env node import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import express from 'express'; import { randomUUID } from 'crypto'; import { YankiConnect } from 'yanki-connect'; import { z } from 'zod'; var ankiClient = new YankiConnect(); // src/resources/decks.ts function registerDeckResources(server) { server.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)}` ); } }); server.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)}` ); } }); server.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)}` ); } } ); server.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)}` ); } } ); server.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(server) { server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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(server) { server.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)}` ); } }); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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(server) { server.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)}` ); } }); server.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)}` ); } }); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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(server) { server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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)}` ); } } ); server.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 registerGraphicalTools(server) { server.tool( "gui_add_cards", { deckName: z.string().describe("Name of the deck to add the note to"), modelName: z.string().describe("Name of the note model/type"), fields: z.record(z.string(), z.string()).describe("Object with field names as keys and field content as values"), tags: z.array(z.string()).optional().describe("Array of tags to add to the note") }, async ({ deckName, modelName, fields, tags }) => { try { const note = { deckName, modelName, fields, tags: tags || [] }; const result = await ankiClient.graphical.guiAddCards({ note }); return { content: [ { type: "text", text: `Opened Add Cards dialog and added note with ID: ${result}` } ] }; } catch (error) { throw new Error( `Failed to open Add Cards dialog: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_answer_card", { ease: z.number().min(1).max(4).describe("Ease rating: 1 (Again), 2 (Hard), 3 (Good), 4 (Easy)") }, async ({ ease }) => { try { const result = await ankiClient.graphical.guiAnswerCard({ ease }); return { content: [ { type: "text", text: `Successfully answered current card with ease ${ease}. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to answer card: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_browse", { query: z.string().describe("Search query to browse cards"), reorderCards: z.object({ columnId: z.string().describe("Column to sort by"), order: z.enum(["ascending", "descending"]).describe("Sort order") }).optional().describe("Optional reordering configuration") }, async ({ query, reorderCards }) => { try { const params = { query }; if (reorderCards) { params.reorderCards = reorderCards; } const cardIds = await ankiClient.graphical.guiBrowse(params); return { content: [ { type: "text", text: `Opened browser with query "${query}". Found ${cardIds.length} cards: ${JSON.stringify(cardIds)}` } ] }; } catch (error) { throw new Error( `Failed to open browser: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool("gui_check_database", {}, async () => { try { const result = await ankiClient.graphical.guiCheckDatabase(); return { content: [ { type: "text", text: `Database check completed successfully. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to check database: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_current_card", {}, async () => { try { const cardInfo = await ankiClient.graphical.guiCurrentCard(); return { content: [ { type: "text", text: cardInfo ? `Current card: ${JSON.stringify(cardInfo, null, 2)}` : "No card is currently being reviewed" } ] }; } catch (error) { throw new Error( `Failed to get current card: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_deck_browser", {}, async () => { try { await ankiClient.graphical.guiDeckBrowser(); return { content: [ { type: "text", text: "Successfully opened deck browser" } ] }; } catch (error) { throw new Error( `Failed to open deck browser: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool( "gui_deck_overview", { deckName: z.string().describe("Name of the deck to view overview for") }, async ({ deckName }) => { try { const result = await ankiClient.graphical.guiDeckOverview({ name: deckName }); return { content: [ { type: "text", text: `Successfully opened deck overview for "${deckName}". Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to open deck overview: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_deck_review", { deckName: z.string().describe("Name of the deck to start reviewing") }, async ({ deckName }) => { try { const result = await ankiClient.graphical.guiDeckReview({ name: deckName }); return { content: [ { type: "text", text: `Successfully started review for deck "${deckName}". Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to start deck review: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_edit_note", { noteId: z.number().describe("ID of the note to edit") }, async ({ noteId }) => { try { await ankiClient.graphical.guiEditNote({ note: noteId }); return { content: [ { type: "text", text: `Successfully opened edit dialog for note ${noteId}` } ] }; } catch (error) { throw new Error( `Failed to edit note: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool("gui_exit_anki", {}, async () => { try { await ankiClient.graphical.guiExitAnki(); return { content: [ { type: "text", text: "Successfully sent exit command to Anki" } ] }; } catch (error) { throw new Error( `Failed to exit Anki: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool( "gui_import_file", { filePath: z.string().describe("Path to the file to import") }, async ({ filePath }) => { try { await ankiClient.graphical.guiImportFile({ path: filePath }); return { content: [ { type: "text", text: `Successfully imported file: ${filePath}` } ] }; } catch (error) { throw new Error( `Failed to import file: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_select_card", { cardId: z.number().describe("ID of the card to select") }, async ({ cardId }) => { try { const result = await ankiClient.graphical.guiSelectCard({ card: cardId }); return { content: [ { type: "text", text: `Successfully selected card ${cardId}. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to select card: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool( "gui_select_note", { noteId: z.number().describe("ID of the note to select") }, async ({ noteId }) => { try { const result = await ankiClient.graphical.guiSelectNote({ note: noteId }); return { content: [ { type: "text", text: `Successfully selected note ${noteId}. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to select note: ${error instanceof Error ? error.message : String(error)}` ); } } ); server.tool("gui_selected_notes", {}, async () => { try { const noteIds = await ankiClient.graphical.guiSelectedNotes(); return { content: [ { type: "text", text: `Selected notes: ${JSON.stringify(noteIds)}` } ] }; } catch (error) { throw new Error( `Failed to get selected notes: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_show_answer", {}, async () => { try { const result = await ankiClient.graphical.guiShowAnswer(); return { content: [ { type: "text", text: `Successfully showed answer. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to show answer: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_show_question", {}, async () => { try { const result = await ankiClient.graphical.guiShowQuestion(); return { content: [ { type: "text", text: `Successfully showed question. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to show question: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_start_card_timer", {}, async () => { try { const result = await ankiClient.graphical.guiStartCardTimer(); return { content: [ { type: "text", text: `Successfully started card timer. Result: ${result}` } ] }; } catch (error) { throw new Error( `Failed to start card timer: ${error instanceof Error ? error.message : String(error)}` ); } }); server.tool("gui_undo", {}, async () => { try { const result = await ankiClient.graphical.guiUndo(); return { content: [ { type: "text", text: `Successfully performed undo. Result: ${result}` } ] }; } catch (error) { throw new Error(`Failed to undo: ${error instanceof Error ? error.message : String(error)}`); } }); } var MIME_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", bmp: "image/bmp", ico: "image/x-icon", mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4", webm: "video/webm", pdf: "application/pdf" }; function registerConsolidatedTools(server) { server.tool( "manage_flashcards", "Create, update, delete, search, inspect, and tag Anki flashcards through one high-level note/card workflow. Use this for note lifecycle work and card lookup, not for deck structure, note-type schema changes, or study scheduling. Mutating operations change the local Anki collection immediately; searches are read-only. Choose `operation` first, then provide the operation-specific IDs, query, fields, tags, or pagination inputs described by the schema.", { operation: z.enum([ "create", "create_batch", "update", "delete", "find", "get_info", "add_tags", "remove_tags", "clear_empty" ]).describe("The flashcard operation to perform"), // For create operations deckName: z.string().optional().describe("Deck name (for create operations)"), modelName: z.string().optional().describe("Note type/model name (for create operations)"), fields: z.record(z.string(), z.string()).optional().describe("Field name-value pairs (for create/update)"), tags: z.array(z.string()).optional().describe("Tags to add/remove or set"), // For batch create notes: z.array( z.object({ deckName: z.string(), modelName: z.string(), fields: z.record(z.string(), z.string()), tags: z.array(z.string()).optional() }) ).optional().describe("Array of notes to create (for create_batch)"), // For update/delete operations noteId: z.number().optional().describe("Note ID (for update/delete single note)"), noteIds: z.array(z.number()).optional().describe("Note IDs (for delete multiple)"), // For find operations query: z.string().optional().describe("Anki search query (for find operation)"), includeDetails: z.boolean().optional().describe("Include detailed note info (for find)"), limit: z.number().optional().describe("Max results to return (default: 50, recommended to prevent context overflow)"), offset: z.number().optional().describe("Number of results to skip for pagination (default: 0)"), // For get_info cardIds: z.array(z.number()).optional().describe("Card IDs to get info for") }, async ({ operation, deckName, modelName, fields, tags, notes, noteId, noteIds, query, includeDetails, limit = 50, offset = 0, cardIds }) => { try { switch (operation) { case "create": { if (!deckName || !modelName || !fields) { throw new Error("create requires deckName, modelName, and fields"); } const note = { deckName, modelName, fields, tags: tags || [] }; const newNoteId = await ankiClient.note.addNote({ note }); if (newNoteId === null) { throw new Error("Failed to add note - possibly duplicate or invalid fields"); } return { content: [ { type: "text", text: `\u2713 Created flashcard with ID: ${newNoteId} in deck "${deckName}"` } ] }; } case "create_batch": { if (!notes || notes.length === 0) { throw new Error("create_batch requires notes array"); } const formattedNotes = notes.map((note) => ({ ...note, tags: note.tags || [] })); const results = await ankiClient.note.addNotes({ notes: formattedNotes }); const successCount = results?.filter((result) => result !== null).length || 0; const failureCount = (results?.length || 0) - successCount; return { content: [ { type: "text", text: `\u2713 Batch create: ${successCount} succeeded, ${failureCount} failed Results: ${JSON.stringify(results)}` } ] }; } case "update": { if (!noteId) { throw new Error("update requires noteId"); } if (!fields && !tags) { throw new Error("update requires either fields or tags"); } const updateData = { id: noteId }; if (fields) updateData.fields = fields; if (tags) updateData.tags = tags; await ankiClient.note.updateNote({ note: updateData }); return { content: [ { type: "text", text: `\u2713 Updated flashcard ${noteId}${fields ? " (fields updated)" : ""}${tags ? ` (tags: ${tags.join(", ")})` : ""}` } ] }; } case "delete": { if (!noteIds || noteIds.length === 0) { throw new Error("delete requires noteIds array"); } await ankiClient.note.deleteNotes({ notes: noteIds }); return { content: [ { type: "text", text: `\u2713 Deleted ${noteIds.length} flashcard(s): [${noteIds.join(", ")}]` } ] }; } case "find": { if (!query) { throw new Error("find requires query parameter"); } const foundNoteIds = await ankiClient.note.findNotes({ query }); const totalResults = foundNoteIds.length; const paginatedIds = foundNoteIds.slice(offset, offset + limit); const hasMore = offset + limit < totalResults; let result = `Found ${totalResults} total flashcard(s) matching "${query}"`; result += ` Showing results ${offset + 1}-${offset + paginatedIds.length} of ${totalResults}`; if (hasMore) { result += ` \u26A0\uFE0F More results available. Use offset=${offset + limit} to see next page.`; } if (includeDetails && paginatedIds.length > 0) { const notesInfo = await ankiClient.note.notesInfo({ notes: paginatedIds }); result += ` Details: ${JSON.stringify(notesInfo, null, 2)}`; } else if (paginatedIds.length > 0) { result += ` IDs: [${paginatedIds.join(", ")}]`; } return { content: [ { type: "text", text: result } ] }; } case "get_info": { if (!cardIds || cardIds.length === 0) { throw new Error("get_info requires cardIds array"); } if (cardIds.length > 50) { return { content: [ { type: "text", text: `\u26A0\uFE0F Requesting ${cardIds.length} cards may use excessive context. Recommendation: Request fewer cards (\u226450) or use 'find' with pagination instead.` } ] }; } const cardsInfo = await ankiClient.card.cardsInfo({ cards: cardIds }); return { content: [ { type: "text", text: `Card information ($