resource-manager.ts•4.57 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ReadResourceResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { YankiConnect } from "yanki-connect";
import { Card } from "./interfaces.js";
import { cleanWithRegex } from "./utils.js";
/**
* Fetches Anki cards based on a query, retrieves their information,
* cleans the content, and sorts them by due date.
* @param client - The YankiConnect client instance.
* @param ankiQuery - The Anki search query
* @returns A promise that resolves to an array of Card objects.
*/
async function findCardsAndOrder(
client: YankiConnect,
ankiQuery: string
): Promise<Card[]> {
console.error(
`[MCP Anki Client] findCardsAndOrder: Processing query='${ankiQuery}'`
);
let allCardIds = await client.card.findCards({ query: ankiQuery });
console.error(
`[MCP Anki Client] findCardsAndOrder: Found ${allCardIds.length} total card IDs for query '${ankiQuery}'.`
);
if (allCardIds.length === 0) {
return [];
}
if (allCardIds.length > 999) {
console.warn(
`[MCP Anki Client] findCardsAndOrder: Query '${ankiQuery}' returned ${allCardIds.length} cards. Limiting to 999.`
);
allCardIds = allCardIds.slice(0, 999);
}
console.error(
`[MCP Anki Client] findCardsAndOrder: Fetching card info for ${allCardIds.length} IDs.`
);
const cardsData = await client.card.cardsInfo({ cards: allCardIds });
const mappedCards: Card[] = cardsData.map(
(card: {
cardId: number;
question: string;
answer: string;
due: number;
}) => ({
cardId: card.cardId,
question: cleanWithRegex(card.question),
answer: cleanWithRegex(card.answer),
due: card.due,
})
);
const sortedCards = mappedCards.sort((a: Card, b: Card) => a.due - b.due);
console.error(
`[MCP Anki Client] findCardsAndOrder: Returning ${sortedCards.length} cards, sorted by due date.`
);
return sortedCards;
}
/**
* Formats a simplified query keyword (e.g., "isdue", "isnew") or a deck name
* into a full Anki search query string.
* @param simplifiedQuery - The simplified query or deck name.
* @returns A full Anki search query string.
*/
function formatQuery(simplifiedQuery: string): string {
let ankiQuery = "";
if (simplifiedQuery.toLowerCase() === "isdue") {
ankiQuery = "is:due";
} else if (simplifiedQuery.toLowerCase() === "isnew") {
ankiQuery = "is:new";
} else if (simplifiedQuery.toLowerCase() === "deckcurrent") {
ankiQuery = "deck:current";
} else if (simplifiedQuery.includes(":")) {
ankiQuery = simplifiedQuery;
} else {
ankiQuery = simplifiedQuery;
}
return ankiQuery;
}
export function registerResourceHandlers(
server: Server,
getClient: () => YankiConnect
) {
/**
* Handles requests to list available resources (e.g., predefined card searches).
* These resources can then be read to get card data.
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "anki://search/deckcurrent",
mimeType: "application/json",
name: "Current Deck",
description: "Current Anki deck",
},
{
uri: "anki://search/isdue",
mimeType: "application/json",
name: "Due cards",
description: "Cards in review and learning waiting to be studied",
},
{
uri: "anki://search/isnew",
mimeType: "application/json",
name: "New cards",
description: "All unseen cards",
},
],
};
});
/**
* Handles requests to read the content of a specific resource (e.g., fetch cards for "is:due").
* It uses the findCardsAndOrder function to get and process the cards.
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
const queryParts = url.pathname.split("/");
const simplifiedQuery = queryParts[queryParts.length - 1];
if (!simplifiedQuery) {
throw new Error(
"Invalid resource URI: unable to extract query from path."
);
}
const client = getClient();
const ankiQuery = formatQuery(simplifiedQuery);
const cards = await findCardsAndOrder(client, ankiQuery);
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(cards),
},
],
};
});
}