Skip to main content
Glama

Anki MCP Server

by ethangillani
index.ts15.1 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; /** * Configuration for the Anki MCP Server */ const config = { // Anki Connect API URL ankiConnectUrl: "http://localhost:8765", // API version for Anki Connect apiVersion: 6, // Default deck name if none is specified defaultDeckName: "Default", }; /** * Type for Anki Connect API response */ type AnkiResponse<T> = { result: T; error: string | null; }; /** * Make a request to the Anki Connect API * @param action - The action to perform * @param params - Parameters for the action * @returns Promise with the API response */ async function ankiRequest<T>(action: string, params: any = {}): Promise<T> { try { const response = await axios.post(config.ankiConnectUrl, { action, version: config.apiVersion, params, }); const data = response.data as AnkiResponse<T>; if (data.error) { throw new Error(`Anki Connect API error: ${data.error}`); } return data.result; } catch (error) { if (error instanceof Error) { throw error; } else { throw new Error(`Unknown error: ${error}`); } } } /** * Check if Anki is running and connected * @returns Promise<boolean> - True if connected, false otherwise */ async function isAnkiConnected(): Promise<boolean> { try { await ankiRequest<string>("version"); return true; } catch (error) { return false; } } /** * Create the MCP server */ const server = new Server({ name: "anki-mcp", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }); /** * Definition of parameters for creating a note */ const noteParameters = { type: "object", properties: { deckName: { type: "string", description: "Name of the deck to add note to (will be created if it doesn't exist)", }, modelName: { type: "string", description: "Name of the note model/type to use", }, fields: { type: "object", description: "Map of fields to the value in the note model being used", }, tags: { type: "array", items: { type: "string", }, description: "Tags to apply to the note", }, options: { type: "object", description: "Additional options for note creation", properties: { allowDuplicate: { type: "boolean", description: "Allow duplicate notes", }, duplicateScope: { type: "string", description: "Scope for duplicate check", }, duplicateScopeOptions: { type: "object", description: "Additional options for duplicate scope", }, }, }, }, required: ["deckName", "modelName", "fields"], }; /** * Definition of parameters for creating a deck */ const deckParameters = { type: "object", properties: { name: { type: "string", description: "Name of the deck to create", }, options: { type: "object", description: "Additional options for deck creation", }, }, required: ["name"], }; /** * Definition of parameters for adding multiple notes at once */ const bulkNotesParameters = { type: "object", properties: { notes: { type: "array", items: noteParameters, description: "Notes to create", }, }, required: ["notes"], }; /** * Definition of parameters for retrieving a model */ const modelParameters = { type: "object", properties: { modelName: { type: "string", description: "Name of the model to get", }, }, required: ["modelName"], }; /** * Definition of parameters for searching notes */ const searchParameters = { type: "object", properties: { query: { type: "string", description: "Search query in Anki format", }, }, required: ["query"], }; /** * Handler for listing available resources */ server.setRequestHandler(ListResourcesRequestSchema, async () => { // Check if Anki is connected const connected = await isAnkiConnected(); if (!connected) { throw new Error("Cannot connect to Anki. Is Anki running with AnkiConnect plugin installed?"); } // Get all decks const decks = await ankiRequest<Record<string, number>>("deckNamesAndIds"); // Get all note models const models = await ankiRequest<Record<string, number>>("modelNamesAndIds"); // Format decks as resources const deckResources = Object.entries(decks).map(([name, id]) => ({ uri: `anki://decks/${id}`, name, })); // Format models as resources const modelResources = Object.entries(models).map(([name, id]) => ({ uri: `anki://models/${id}`, name, })); return { resources: [...deckResources, ...modelResources], }; }); /** * Handler for reading a specific resource */ server.setRequestHandler(ReadResourceRequestSchema, async (resource) => { const uri = resource.params.uri; // Handle deck resources if (uri.startsWith("anki://decks/")) { const deckId = parseInt(uri.replace("anki://decks/", "")); const decks = await ankiRequest<Record<string, number>>("deckNamesAndIds"); // Find the deck name from ID const deckName = Object.entries(decks).find(([_, id]) => id === deckId)?.[0]; if (!deckName) { throw new Error("Deck not found"); } // Get cards in deck const cardIds = await ankiRequest<number[]>("findCards", { query: `deck:"${deckName}"`, }); // Get notes for those cards const noteIds = await ankiRequest<number[]>("cardsToNotes", { cards: cardIds, }); // Get info about the notes const notes = await ankiRequest<any[]>("notesInfo", { notes: noteIds, }); return { contents: [ { uri, mimeType: "application/json", text: JSON.stringify({ name: deckName, id: deckId, cards: cardIds.length, notes: notes.length, sampleNotes: notes.slice(0, 5), }), }, ], }; } // Handle model resources else if (uri.startsWith("anki://models/")) { const modelId = parseInt(uri.replace("anki://models/", "")); const models = await ankiRequest<Record<string, number>>("modelNamesAndIds"); // Find the model name from ID const modelName = Object.entries(models).find(([_, id]) => id === modelId)?.[0]; if (!modelName) { throw new Error("Model not found"); } // Get model details const modelFields = await ankiRequest<string[]>("modelFieldNames", { modelName, }); const modelTemplates = await ankiRequest<string[]>("modelTemplates", { modelName, }); return { contents: [ { uri, mimeType: "application/json", text: JSON.stringify({ name: modelName, id: modelId, fields: modelFields, templates: modelTemplates, }), }, ], }; } throw new Error("Resource not found"); }); /** * Handler for listing available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { // Check if Anki is connected const connected = await isAnkiConnected(); if (!connected) { throw new Error("Cannot connect to Anki. Is Anki running with AnkiConnect plugin installed?"); } return { tools: [ { name: "listDecks", description: "Get a list of all decks in Anki", inputSchema: { type: "object", properties: {} }, }, { name: "listModels", description: "Get a list of all note models/types in Anki", inputSchema: { type: "object", properties: {} }, }, { name: "createDeck", description: "Create a new deck in Anki", inputSchema: deckParameters, }, { name: "getModel", description: "Get details about a specific note model/type", inputSchema: modelParameters, }, { name: "addNote", description: "Add a single note to a deck", inputSchema: noteParameters, }, { name: "addNotes", description: "Add multiple notes at once", inputSchema: bulkNotesParameters, }, { name: "searchNotes", description: "Search for notes using Anki's search syntax", inputSchema: searchParameters, }, ], }; }); /** * Handler for tool execution */ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "listDecks": { const decks = await ankiRequest<string[]>("deckNames"); return { toolResult: `Decks in Anki: ${decks.join(", ")}`, }; } case "listModels": { const models = await ankiRequest<string[]>("modelNames"); return { toolResult: `Note models in Anki: ${models.join(", ")}`, }; } case "createDeck": { if (!request.params.arguments) { throw new Error("Missing arguments for createDeck"); } const { name } = request.params.arguments; const deckId = await ankiRequest<number>("createDeck", { deck: name, }); return { toolResult: `Created deck "${name}" with ID: ${deckId}`, }; } case "getModel": { if (!request.params.arguments || !request.params.arguments.modelName) { throw new Error("Model name is required"); } const modelName = request.params.arguments.modelName; // Get model fields const fields = await ankiRequest<string[]>("modelFieldNames", { modelName, }); // Get model templates const templates = await ankiRequest<Record<string, Record<string, string>>>("modelTemplates", { modelName, }); // Format the response return { toolResult: JSON.stringify({ name: modelName, fields, templates: Object.keys(templates), }, null, 2), }; } case "addNote": { if (!request.params.arguments) { throw new Error("Missing arguments for addNote"); } const { deckName = config.defaultDeckName, modelName, fields, tags = [], options = {} } = request.params.arguments; // Ensure the deck exists await ankiRequest<number>("createDeck", { deck: deckName, }); // Add the note const noteId = await ankiRequest<number>("addNote", { note: { deckName, modelName, fields, tags, options, }, }); return { toolResult: `Created note with ID: ${noteId} in deck "${deckName}"`, }; } case "addNotes": { if (!request.params.arguments || !request.params.arguments.notes) { throw new Error("Notes array is required"); } const { notes } = request.params.arguments; // Format the notes array for Anki Connect const formattedNotes = notes.map(note => ({ deckName: note.deckName || config.defaultDeckName, modelName: note.modelName, fields: note.fields, tags: note.tags || [], options: note.options || {} })); // Ensure all decks exist first const uniqueDeckNames = [...new Set(formattedNotes.map(note => note.deckName))]; for (const deckName of uniqueDeckNames) { await ankiRequest<number>("createDeck", { deck: deckName, }); } // Add the notes const noteIds = await ankiRequest<number[]>("addNotes", { notes: formattedNotes, }); // Count successful notes (non-null IDs) const successCount = noteIds.filter(id => id !== null).length; return { toolResult: `Successfully added ${successCount} out of ${notes.length} notes`, }; } case "searchNotes": { if (!request.params.arguments || !request.params.arguments.query) { throw new Error("Search query is required"); } const { query } = request.params.arguments; // Search for notes const noteIds = await ankiRequest<number[]>("findNotes", { query, }); if (noteIds.length === 0) { return { toolResult: "No notes found matching the search query", }; } // Get note info (limiting to first 10 to avoid overwhelming responses) const displayLimit = Math.min(noteIds.length, 10); const notesToDisplay = noteIds.slice(0, displayLimit); const notesInfo = await ankiRequest<any[]>("notesInfo", { notes: notesToDisplay, }); // Format the notes for display const formattedNotes = notesInfo.map(note => { const { fields, tags, modelName, cards } = note; // Format fields to be more readable const formattedFields = Object.entries(fields).map(([name, value]) => { // The field value is an object with text and other properties const fieldValue = typeof value === 'object' ? value.value : value; return `${name}: ${fieldValue}`; }).join('\n'); return { modelName, fields: formattedFields, tags, cardCount: cards.length, }; }); return { toolResult: `Found ${noteIds.length} notes matching the query. ${displayLimit < noteIds.length ? `Showing first ${displayLimit}:` : 'Details:'}\n\n${JSON.stringify(formattedNotes, null, 2)}`, }; } default: throw new McpError(`Unknown tool: ${request.params.name}`); } }); /** * Initialize and start the server */ async function startServer() { try { // Check if Anki is connected const connected = await isAnkiConnected(); if (!connected) { console.error("Cannot connect to Anki. Is Anki running with AnkiConnect plugin installed?"); console.error(`Attempted to connect to: ${config.ankiConnectUrl}`); process.exit(1); } // Log available decks and models on startup const decks = await ankiRequest<string[]>("deckNames"); const models = await ankiRequest<string[]>("modelNames"); console.log("Connected to Anki successfully!"); console.log(`Available decks: ${decks.join(", ")}`); console.log(`Available note models: ${models.join(", ")}`); // Start server with stdio transport const transport = new StdioServerTransport(); server.listen(transport); console.log("Anki MCP Server is running..."); } catch (error) { console.error("Failed to start Anki MCP Server:", error); process.exit(1); } } startServer();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ethangillani/Anki-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server