Skip to main content
Glama

Anki MCP Server

by nailuoGG
mcpTools.ts21.4 kB
/** * MCP Tool handlers for Anki */ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { AnkiClient } from "./utils.js"; /** * Handles all MCP tool operations for Anki */ export class McpToolHandler { private ankiClient: AnkiClient; constructor() { this.ankiClient = new AnkiClient(); } /** * Get tool schema for all available tools */ async getToolSchema(): Promise<{ tools: { name: string; description: string; inputSchema: Record<string, unknown>; }[]; }> { return { tools: [ { name: "list_decks", description: "List all available Anki decks", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "create_deck", description: "Create a new Anki deck", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the deck to create", }, }, required: ["name"], }, }, { name: "get_note_type_info", description: "Get detailed structure of a note type", inputSchema: { type: "object", properties: { modelName: { type: "string", description: "Name of the note type/model", }, includeCss: { type: "boolean", description: "Whether to include CSS information", }, }, required: ["modelName"], }, }, { name: "create_note", description: "Create a single note. For multiple notes, use batch_create_notes instead (10-20 notes per batch recommended). Always call get_note_type_info first to understand required fields.", inputSchema: { type: "object", properties: { type: { type: "string", description: "Note type. Common: 'Basic' (has Front/Back), 'Cloze' (has Text with {{c1::deletions}})", }, deck: { type: "string", description: "Target deck name", }, fields: { type: "object", description: "Note fields. Basic type: {Front: 'question', Back: 'answer'}. Cloze type: {Text: 'text with {{c1::deletion}}'}. Call get_note_type_info for custom types.", additionalProperties: true, }, allowDuplicate: { type: "boolean", description: "Whether to allow duplicate notes (default: false)", default: false, }, tags: { type: "array", items: { type: "string", }, description: "Optional tags for organization", }, }, required: ["type", "deck", "fields"], }, }, { name: "batch_create_notes", description: "Create multiple notes at once. IMPORTANT: For optimal performance, limit batch size to 10-20 notes at a time. For larger sets, split into multiple batches. Always call get_note_type_info first to understand the required fields.", inputSchema: { type: "object", properties: { notes: { type: "array", description: "Array of notes to create. RECOMMENDED: 10-20 notes per batch for best performance. Maximum: 50 notes.", maxItems: 50, items: { type: "object", properties: { type: { type: "string", description: "Note type. Common types: 'Basic' (Front/Back fields), 'Cloze' (Text field with {{c1::text}} deletions)", enum: ["Basic", "Cloze"], }, deck: { type: "string", description: "Target deck name", }, fields: { type: "object", description: "Note fields. For Basic: {Front: '...', Back: '...'}. For Cloze: {Text: '...with {{c1::deletion}}'}", additionalProperties: true, }, tags: { type: "array", items: { type: "string", }, description: "Optional tags for organization", }, }, required: ["type", "deck", "fields"], }, }, allowDuplicate: { type: "boolean", description: "Whether to allow duplicate notes (default: false)", default: false, }, stopOnError: { type: "boolean", description: "Whether to stop on first error or continue with remaining notes (default: false)", default: false, }, }, required: ["notes"], examples: [ { notes: [ { type: "Basic", deck: "Programming", fields: { Front: "What is a closure?", Back: "A function with access to its outer scope", }, tags: ["javascript", "concepts"], }, { type: "Cloze", deck: "Programming", fields: { Text: "In JavaScript, {{c1::const}} declares a {{c2::block-scoped}} variable", }, tags: ["javascript", "syntax"], }, ], }, ], }, }, { name: "search_notes", description: "Search for notes using Anki query syntax", inputSchema: { type: "object", properties: { query: { type: "string", description: "Anki search query", }, }, required: ["query"], }, }, { name: "get_note_info", description: "Get detailed information about a note", inputSchema: { type: "object", properties: { noteId: { type: "number", description: "Note ID", }, }, required: ["noteId"], }, }, { name: "update_note", description: "Update an existing note", inputSchema: { type: "object", properties: { id: { type: "number", description: "Note ID", }, fields: { type: "object", description: "Fields to update", }, tags: { type: "array", items: { type: "string", }, description: "New tags for the note", }, }, required: ["id", "fields"], }, }, { name: "delete_note", description: "Delete a note", inputSchema: { type: "object", properties: { noteId: { type: "number", description: "Note ID to delete", }, }, required: ["noteId"], }, }, { name: "list_note_types", description: "List all available note types", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "create_note_type", description: "Create a new note type", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the new note type", }, fields: { type: "array", items: { type: "string", }, description: "Field names for the note type", }, css: { type: "string", description: "CSS styling for the note type", }, templates: { type: "array", items: { type: "object", properties: { name: { type: "string", }, front: { type: "string", }, back: { type: "string", }, }, required: ["name", "front", "back"], }, description: "Card templates", }, }, required: ["name", "fields", "templates"], }, }, ], }; } /** * Handle tool execution */ async executeTool( name: string, args: Record<string, unknown> ): Promise<{ content: { type: string; text: string; }[]; isError?: boolean; }> { await this.ankiClient.checkConnection(); try { switch (name) { // Deck tools case "list_decks": return this.listDecks(); case "create_deck": return this.createDeck(args); // Note type tools case "list_note_types": return this.listNoteTypes(); case "create_note_type": return this.createNoteType(args); case "get_note_type_info": return this.getNoteTypeInfo(args); // Note tools case "create_note": return this.createNote(args); case "batch_create_notes": return this.batchCreateNotes(args); case "search_notes": return this.searchNotes(args); case "get_note_info": return this.getNoteInfo(args); case "update_note": return this.updateNote(args); case "delete_note": return this.deleteNote(args); // Dynamic model-specific note creation default: { const typeToolMatch = name.match(/^create_(.+)_note$/); if (typeToolMatch) { const modelName = typeToolMatch[1].replace(/_/g, " "); return this.createModelSpecificNote(modelName, args); } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } } catch (error) { if (error instanceof McpError) { throw error; } return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } /** * List all decks */ private async listDecks(): Promise<{ content: { type: string; text: string; }[]; }> { const decks = await this.ankiClient.getDeckNames(); return { content: [ { type: "text", text: JSON.stringify({ decks, count: decks.length }, null, 2), }, ], }; } /** * Create a new deck */ private async createDeck(args: { name: string }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.name) { throw new McpError(ErrorCode.InvalidParams, "Deck name is required"); } const deckId = await this.ankiClient.createDeck(args.name); return { content: [ { type: "text", text: JSON.stringify({ deckId, name: args.name }, null, 2), }, ], }; } /** * List all note types */ private async listNoteTypes(): Promise<{ content: { type: string; text: string; }[]; }> { const noteTypes = await this.ankiClient.getModelNames(); return { content: [ { type: "text", text: JSON.stringify({ noteTypes, count: noteTypes.length }, null, 2), }, ], }; } /** * Create a new note type */ private async createNoteType(args: { name: string; fields: string[]; css?: string; templates: { name: string; front: string; back: string; }[]; }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.name) { throw new McpError(ErrorCode.InvalidParams, "Note type name is required"); } if (!args.fields || args.fields.length === 0) { throw new McpError(ErrorCode.InvalidParams, "Fields are required"); } if (!args.templates || args.templates.length === 0) { throw new McpError(ErrorCode.InvalidParams, "Templates are required"); } // Check if model already exists const existingModels = await this.ankiClient.getModelNames(); if (existingModels.includes(args.name)) { throw new McpError(ErrorCode.InvalidParams, `Note type already exists: ${args.name}`); } await this.ankiClient.createModel({ modelName: args.name, inOrderFields: args.fields, css: args.css || "", cardTemplates: args.templates, }); return { content: [ { type: "text", text: JSON.stringify( { success: true, modelName: args.name, fields: args.fields, templates: args.templates.length, }, null, 2 ), }, ], }; } /** * Get note type info */ private async getNoteTypeInfo(args: { modelName: string; includeCss?: boolean; }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.modelName) { throw new McpError(ErrorCode.InvalidParams, "Model name is required"); } // Check if model exists const existingModels = await this.ankiClient.getModelNames(); if (!existingModels.includes(args.modelName)) { throw new McpError(ErrorCode.InvalidParams, `Note type not found: ${args.modelName}`); } // Get model information in parallel const [fields, templates] = await Promise.all([ this.ankiClient.getModelFieldNames(args.modelName), this.ankiClient.getModelTemplates(args.modelName), ]); const result: { modelName: string; fields: string[]; templates: Record<string, { Front: string; Back: string }>; css?: string; } = { modelName: args.modelName, fields, templates, }; if (args.includeCss) { const styling = await this.ankiClient.getModelStyling(args.modelName); result.css = styling.css; } return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } /** * Create a new note */ private async createNote(args: { type: string; deck: string; fields: Record<string, string>; allowDuplicate?: boolean; tags?: string[]; }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.type) { throw new McpError(ErrorCode.InvalidParams, "Note type is required"); } if (!args.deck) { throw new McpError(ErrorCode.InvalidParams, "Deck name is required"); } if (!args.fields || Object.keys(args.fields).length === 0) { throw new McpError(ErrorCode.InvalidParams, "Fields are required"); } // Check if deck exists, create if not const decks = await this.ankiClient.getDeckNames(); if (!decks.includes(args.deck)) { await this.ankiClient.createDeck(args.deck); } // Check if model exists const models = await this.ankiClient.getModelNames(); if (!models.includes(args.type)) { throw new McpError(ErrorCode.InvalidParams, `Note type not found: ${args.type}`); } // Normalize field names to match the model const modelFields = await this.ankiClient.getModelFieldNames(args.type); const normalizedFields: Record<string, string> = {}; for (const field of modelFields) { normalizedFields[field] = args.fields[field] || args.fields[field.toLowerCase()] || ""; } const noteId = await this.ankiClient.addNote({ deckName: args.deck, modelName: args.type, fields: normalizedFields, tags: args.tags || [], options: { allowDuplicate: args.allowDuplicate || false, }, }); return { content: [ { type: "text", text: JSON.stringify( { noteId, deck: args.deck, modelName: args.type, }, null, 2 ), }, ], }; } /** * Create a model-specific note */ private async createModelSpecificNote( modelName: string, args: Record<string, unknown> ): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.deck) { throw new McpError(ErrorCode.InvalidParams, "Deck name is required"); } // Check if model exists const models = await this.ankiClient.getModelNames(); if (!models.includes(modelName)) { throw new McpError(ErrorCode.InvalidParams, `Note type not found: ${modelName}`); } // Check if deck exists, create if not const decks = await this.ankiClient.getDeckNames(); if (!decks.includes(args.deck)) { await this.ankiClient.createDeck(args.deck); } // Get model fields const modelFields = await this.ankiClient.getModelFieldNames(modelName); // Normalize fields: all fields can be empty const fields: Record<string, string> = {}; for (const field of modelFields) { fields[field] = args[field.toLowerCase()] || args[field] || ""; } // Extract tags if provided const tags = Array.isArray(args.tags) ? args.tags : []; const noteId = await this.ankiClient.addNote({ deckName: args.deck, modelName: modelName, fields, tags, }); return { content: [ { type: "text", text: JSON.stringify( { noteId, deck: args.deck, modelName, }, null, 2 ), }, ], }; } /** * Create multiple notes at once */ private async batchCreateNotes(args: { notes: { type: string; deck: string; fields: Record<string, string>; tags?: string[]; }[]; allowDuplicate?: boolean; stopOnError?: boolean; }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.notes || !Array.isArray(args.notes) || args.notes.length === 0) { throw new McpError(ErrorCode.InvalidParams, "Notes array is required"); } const results: { success: boolean; noteId?: number | null; error?: string; index: number; }[] = []; const stopOnError = args.stopOnError !== false; // Process each note for (let i = 0; i < args.notes.length; i++) { const note = args.notes[i]; try { // Check if deck exists, create if not const decks = await this.ankiClient.getDeckNames(); if (!decks.includes(note.deck)) { await this.ankiClient.createDeck(note.deck); } // Check if model exists const models = await this.ankiClient.getModelNames(); if (!models.includes(note.type)) { throw new Error(`Note type not found: ${note.type}`); } // Get model fields const modelFields = await this.ankiClient.getModelFieldNames(note.type); // Normalize field names to match the model, all fields can be empty const normalizedFields: Record<string, string> = {}; for (const field of modelFields) { normalizedFields[field] = note.fields[field] || note.fields[field.toLowerCase()] || ""; } const noteId = await this.ankiClient.addNote({ deckName: note.deck, modelName: note.type, fields: normalizedFields, tags: note.tags || [], options: { allowDuplicate: args.allowDuplicate || false, }, }); results.push({ success: true, noteId, index: i, }); } catch (error) { results.push({ success: false, error: error instanceof Error ? error.message : String(error), index: i, }); if (stopOnError) { break; } } } return { content: [ { type: "text", text: JSON.stringify( { results, total: args.notes.length, successful: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length, }, null, 2 ), }, ], }; } /** * Search for notes */ private async searchNotes(args: { query: string }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.query) { throw new McpError(ErrorCode.InvalidParams, "Search query is required"); } const noteIds = await this.ankiClient.findNotes(args.query); let notes: { noteId: number; modelName: string; tags: string[]; fields: Record<string, { value: string; order: number }>; }[] = []; if (noteIds.length > 0) { // Get detailed info for the first 50 notes const limit = Math.min(noteIds.length, 50); const notesInfo = await this.ankiClient.notesInfo(noteIds.slice(0, limit)); notes = notesInfo; } return { content: [ { type: "text", text: JSON.stringify( { query: args.query, total: noteIds.length, notes, limitApplied: noteIds.length > 50, }, null, 2 ), }, ], }; } /** * Get note info */ private async getNoteInfo(args: { noteId: number }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.noteId) { throw new McpError(ErrorCode.InvalidParams, "Note ID is required"); } const notesInfo = await this.ankiClient.notesInfo([args.noteId]); if (!notesInfo || notesInfo.length === 0) { throw new McpError(ErrorCode.InvalidParams, `Note not found: ${args.noteId}`); } return { content: [ { type: "text", text: JSON.stringify(notesInfo[0], null, 2), }, ], }; } /** * Update a note */ private async updateNote(args: { id: number; fields: Record<string, string>; tags?: string[]; }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.id) { throw new McpError(ErrorCode.InvalidParams, "Note ID is required"); } if (!args.fields || Object.keys(args.fields).length === 0) { throw new McpError(ErrorCode.InvalidParams, "Fields are required"); } // Check if note exists const notesInfo = await this.ankiClient.notesInfo([args.id]); if (!notesInfo || notesInfo.length === 0) { throw new McpError(ErrorCode.InvalidParams, `Note not found: ${args.id}`); } // Update fields await this.ankiClient.updateNoteFields({ id: args.id, fields: args.fields, }); return { content: [ { type: "text", text: JSON.stringify( { success: true, noteId: args.id, }, null, 2 ), }, ], }; } /** * Delete a note */ private async deleteNote(args: { noteId: number }): Promise<{ content: { type: string; text: string; }[]; }> { if (!args.noteId) { throw new McpError(ErrorCode.InvalidParams, "Note ID is required"); } await this.ankiClient.deleteNotes([args.noteId]); return { content: [ { type: "text", text: JSON.stringify( { success: true, noteId: args.noteId, }, null, 2 ), }, ], }; } }

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/nailuoGG/anki-mcp-server'

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