manage_flashcards
Create, update, delete, organize, and search flashcards in Anki decks using operations like batch creation, tagging, and query-based retrieval.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| operation | Yes | The flashcard operation to perform | |
| deckName | No | Deck name (for create operations) | |
| modelName | No | Note type/model name (for create operations) | |
| fields | No | Field name-value pairs (for create/update) | |
| tags | No | Tags to add/remove or set | |
| notes | No | Array of notes to create (for create_batch) | |
| noteId | No | Note ID (for update/delete single note) | |
| noteIds | No | Note IDs (for delete multiple) | |
| query | No | Anki search query (for find operation) | |
| includeDetails | No | Include detailed note info (for find) | |
| limit | No | Max results to return (default: 50, recommended to prevent context overflow) | |
| offset | No | Number of results to skip for pagination (default: 0) | |
| cardIds | No | Card IDs to get info for |
Implementation Reference
- src/tools/consolidated.ts:94-306 (handler)The core handler function for the manage_flashcards tool. It processes the input operation and dispatches to specific AnkiConnect API calls for flashcard management (create, update, delete, search, tags, etc.). This is the exact implementation of the tool logic.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: `✓ 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: `✓ Batch create: ${successCount} succeeded, ${failureCount} failed\nResults: ${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: any = { id: noteId }; if (fields) updateData.fields = fields; if (tags) updateData.tags = tags; await ankiClient.note.updateNote({ note: updateData }); return { content: [ { type: 'text', text: `✓ 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: `✓ 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; // Apply pagination const paginatedIds = foundNoteIds.slice(offset, offset + limit); const hasMore = offset + limit < totalResults; let result = `Found ${totalResults} total flashcard(s) matching "${query}"`; result += `\nShowing results ${offset + 1}-${offset + paginatedIds.length} of ${totalResults}`; if (hasMore) { result += `\n⚠️ 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 += `\n\nDetails:\n${JSON.stringify(notesInfo, null, 2)}`; } else if (paginatedIds.length > 0) { result += `\n\nIDs: [${paginatedIds.join(', ')}]`; } return { content: [ { type: 'text', text: result, }, ], }; } case 'get_info': { if (!cardIds || cardIds.length === 0) { throw new Error('get_info requires cardIds array'); } // Warn if requesting too many cards at once if (cardIds.length > 50) { return { content: [ { type: 'text', text: `⚠️ Requesting ${cardIds.length} cards may use excessive context.\nRecommendation: Request fewer cards (≤50) or use 'find' with pagination instead.`, }, ], }; } const cardsInfo = await ankiClient.card.cardsInfo({ cards: cardIds }); return { content: [ { type: 'text', text: `Card information (${cardsInfo.length} cards):\n${JSON.stringify(cardsInfo, null, 2)}`, }, ], }; } case 'add_tags': { if (!noteIds || !tags) { throw new Error('add_tags requires noteIds and tags'); } await ankiClient.note.addTags({ notes: noteIds, tags: tags.join(' ') }); return { content: [ { type: 'text', text: `✓ Added tags [${tags.join(', ')}] to ${noteIds.length} flashcard(s)`, }, ], }; } case 'remove_tags': { if (!noteIds || !tags) { throw new Error('remove_tags requires noteIds and tags'); } await ankiClient.note.removeTags({ notes: noteIds, tags: tags.join(' ') }); return { content: [ { type: 'text', text: `✓ Removed tags [${tags.join(', ')}] from ${noteIds.length} flashcard(s)`, }, ], }; } case 'clear_empty': { await ankiClient.note.removeEmptyNotes(); return { content: [ { type: 'text', text: '✓ Cleared all empty flashcards from collection', }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `manage_flashcards failed: ${error instanceof Error ? error.message : String(error)}` ); } } );
- src/tools/consolidated.ts:38-93 (schema)Zod input schema defining parameters for the manage_flashcards tool, including operation type and specific fields for each operation like deckName, fields, query, etc.{ 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()) .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()), 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'), },
- src/tools/consolidated.ts:36-307 (registration)MCP server.tool registration for the manage_flashcards tool, passing schema and handler.server.tool( 'manage_flashcards', { 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()) .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()), 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: `✓ 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: `✓ Batch create: ${successCount} succeeded, ${failureCount} failed\nResults: ${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: any = { id: noteId }; if (fields) updateData.fields = fields; if (tags) updateData.tags = tags; await ankiClient.note.updateNote({ note: updateData }); return { content: [ { type: 'text', text: `✓ 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: `✓ 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; // Apply pagination const paginatedIds = foundNoteIds.slice(offset, offset + limit); const hasMore = offset + limit < totalResults; let result = `Found ${totalResults} total flashcard(s) matching "${query}"`; result += `\nShowing results ${offset + 1}-${offset + paginatedIds.length} of ${totalResults}`; if (hasMore) { result += `\n⚠️ 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 += `\n\nDetails:\n${JSON.stringify(notesInfo, null, 2)}`; } else if (paginatedIds.length > 0) { result += `\n\nIDs: [${paginatedIds.join(', ')}]`; } return { content: [ { type: 'text', text: result, }, ], }; } case 'get_info': { if (!cardIds || cardIds.length === 0) { throw new Error('get_info requires cardIds array'); } // Warn if requesting too many cards at once if (cardIds.length > 50) { return { content: [ { type: 'text', text: `⚠️ Requesting ${cardIds.length} cards may use excessive context.\nRecommendation: Request fewer cards (≤50) or use 'find' with pagination instead.`, }, ], }; } const cardsInfo = await ankiClient.card.cardsInfo({ cards: cardIds }); return { content: [ { type: 'text', text: `Card information (${cardsInfo.length} cards):\n${JSON.stringify(cardsInfo, null, 2)}`, }, ], }; } case 'add_tags': { if (!noteIds || !tags) { throw new Error('add_tags requires noteIds and tags'); } await ankiClient.note.addTags({ notes: noteIds, tags: tags.join(' ') }); return { content: [ { type: 'text', text: `✓ Added tags [${tags.join(', ')}] to ${noteIds.length} flashcard(s)`, }, ], }; } case 'remove_tags': { if (!noteIds || !tags) { throw new Error('remove_tags requires noteIds and tags'); } await ankiClient.note.removeTags({ notes: noteIds, tags: tags.join(' ') }); return { content: [ { type: 'text', text: `✓ Removed tags [${tags.join(', ')}] from ${noteIds.length} flashcard(s)`, }, ], }; } case 'clear_empty': { await ankiClient.note.removeEmptyNotes(); return { content: [ { type: 'text', text: '✓ Cleared all empty flashcards from collection', }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `manage_flashcards failed: ${error instanceof Error ? error.message : String(error)}` ); } } );
- src/index.ts:31-31 (registration)Top-level call to registerConsolidatedTools, which registers the manage_flashcards tool among others.registerConsolidatedTools(server);