@arielbk/anki-mcp
Version:
MCP server for integrating with Anki flashcards through conversational AI
1,583 lines (1,579 loc) • 86.5 kB
JavaScript
#!/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 ($