Skip to main content
Glama
index.ts10.1 kB
#!/usr/bin/env node import dotenv from 'dotenv'; import { FastMCP } from 'fastmcp'; import { z } from 'zod'; import { Tool } from './types.js'; dotenv.config(); const server = new FastMCP({ name: 'anki-mcp', version: '1.0.1', }); // Get Anki API configuration from environment const ANKI_BASE_URL = process.env.ANKI_BASE_URL || 'http://localhost:3000'; const ANKI_API_KEY = process.env.ANKI_API_KEY; if (!ANKI_API_KEY) { console.warn('ANKI_API_KEY not set. Anki tools will not work properly.'); } // Helper function to make API requests async function ankiApiRequest( method: string, endpoint: string, body?: Record<string, unknown>, queryParams?: Record<string, boolean | number | string | undefined>, ) { const url = new URL(`${ANKI_BASE_URL}${endpoint}`); // Add query parameters if provided if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { if (value !== undefined && value !== '') { url.searchParams.append(key, String(value)); } }); } const options: RequestInit = { headers: { Authorization: `Bearer ${ANKI_API_KEY}`, 'Content-Type': 'application/json', }, method, }; if (body && method !== 'GET') { options.body = JSON.stringify(body); } const response = await fetch(url.toString(), options); const data = (await response.json()) as unknown; if (!response.ok) { const errorData = data as { error?: { message?: string } }; throw new Error( errorData.error?.message || `API request failed: ${response.statusText}`, ); } return data; } // 1. List Decks Tool server.addTool({ description: 'List all Anki decks with optional pagination', execute: async args => { try { const queryParams: Record<string, boolean | number | string> = {}; if (args.limit !== undefined) queryParams.limit = args.limit; if (args.offset !== undefined) queryParams.offset = args.offset; const data = await ankiApiRequest( 'GET', '/api/v1/decks', undefined, queryParams, ); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'listDecks', parameters: z.object({ limit: z.number().optional().describe('Number of decks to return'), offset: z.number().optional().describe('Number of decks to skip'), }), }); // 2. Create Deck Tool server.addTool({ description: 'Create a new Anki deck', execute: async args => { try { const data = await ankiApiRequest('POST', '/api/v1/decks', { description: args.description, isPublic: args.isPublic || false, name: args.name, }); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'createDeck', parameters: z.object({ description: z.string().describe('Description of the deck'), isPublic: z .boolean() .optional() .describe('Whether the deck should be public'), name: z.string().describe('Name of the deck'), }), }); // 3. Add Cards Batch Tool server.addTool({ description: 'Add multiple cards to a deck in batch', execute: async args => { try { const data = await ankiApiRequest( 'POST', `/api/v1/decks/${args.deckId}/cards/batch`, { cards: args.cards, }, ); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'addCardsBatch', parameters: z.object({ cards: z .array( z.object({ back: z.string().describe('Back side of the card'), cardType: z.enum(['BASIC', 'CLOZE']).describe('Type of card'), clozeText: z .string() .optional() .describe('Cloze text for cloze cards'), front: z.string().describe('Front side of the card'), tags: z.array(z.string()).optional().describe('Tags for the card'), }), ) .describe('Array of cards to add'), deckId: z.string().describe('ID of the deck to add cards to'), }), }); // 4. Add Basic Card Tool (convenience method) server.addTool({ description: 'Add a single basic card to a deck', execute: async args => { try { const data = await ankiApiRequest( 'POST', `/api/v1/decks/${args.deckId}/cards/batch`, { cards: [ { back: args.back, cardType: 'BASIC', front: args.front, tags: args.tags || [], }, ], }, ); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'addBasicCard', parameters: z.object({ back: z.string().describe('Back side of the card'), deckId: z.string().describe('ID of the deck to add card to'), front: z.string().describe('Front side of the card'), tags: z.array(z.string()).optional().describe('Tags for the card'), }), }); // 5. Add Cloze Card Tool (convenience method) server.addTool({ description: 'Add a cloze deletion card to a deck', execute: async args => { try { const data = await ankiApiRequest( 'POST', `/api/v1/decks/${args.deckId}/cards/batch`, { cards: [ { back: args.back, cardType: 'CLOZE', clozeText: args.clozeText, front: args.front, tags: args.tags || [], }, ], }, ); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'addClozeCard', parameters: z.object({ back: z.string().describe('Back side of the card'), clozeText: z.string().describe('Cloze text with {{c1::text}} format'), deckId: z.string().describe('ID of the deck to add card to'), front: z.string().describe('Front side of the card'), tags: z.array(z.string()).optional().describe('Tags for the card'), }), }); // 6. Get Review Queue Tool server.addTool({ description: 'Get cards due for review with filtering options', execute: async args => { try { const queryParams: Record<string, boolean | number | string> = {}; if (args.deckId !== undefined) queryParams.deckId = args.deckId; if (args.includeLearning !== undefined) queryParams.includeLearning = args.includeLearning; if (args.includeNew !== undefined) queryParams.includeNew = args.includeNew; if (args.includeReview !== undefined) queryParams.includeReview = args.includeReview; if (args.limit !== undefined) queryParams.limit = args.limit; const data = await ankiApiRequest( 'GET', '/api/v1/study/queue', undefined, queryParams, ); return JSON.stringify(data, null, 2); } catch (error) { return `Error: ${error instanceof Error ? error.message : String(error)}`; } }, name: 'getReviewQueue', parameters: z.object({ deckId: z.string().optional().describe('Optional deck ID to filter cards'), includeLearning: z.boolean().optional().describe('Include learning cards'), includeNew: z.boolean().optional().describe('Include new cards'), includeReview: z.boolean().optional().describe('Include review cards'), limit: z.number().optional().describe('Maximum number of cards to return'), }), }); // Load additional tools from external API if configured async function loadExternalTools() { const API_URL = process.env.API_URL; const API_KEY = process.env.API_KEY; if (!API_URL || !API_KEY) { return; } try { const response = await fetch(API_URL, { headers: { 'x-api-key': API_KEY, }, method: 'GET', }); if (!response.ok) { console.warn(`Failed to fetch external tools: ${response.statusText}`); return; } const tools = (await response.json()) as Tool[]; // Register external tools tools.forEach(tool => { server.addTool({ description: tool.description, execute: async args => { return tool.prompt.replace(/{(\w+)}/g, (_: string, key: string) => { if (key in args) { if (Array.isArray(args[key])) { return args[key].map((item: string) => `"${item}"`).join(', '); } return args[key]; } return `{${key}}`; }); }, name: tool.name, parameters: z.object( (tool.args || []).reduce( ( acc: Record<string, z.ZodTypeAny>, arg: NonNullable<Tool['args']>[0], ) => { let argType: z.ZodTypeAny; if (arg.type === 'array') { argType = z.array(z.string()).describe(arg.description); } else if (arg.type === 'number') { argType = z.number().describe(arg.description); } else if (arg.type === 'string') { argType = z.string().describe(arg.description); } else { throw new Error(`Unsupported argument type: ${arg.type}`); } acc[arg.name] = argType; return acc; }, {} as Record<string, z.ZodTypeAny>, ), ), }); }); console.error(`Loaded ${tools.length} external tools`); } catch (error) { console.error('Failed to load external tools:', error); } } // Load external tools loadExternalTools(); // Add example resource server.addResource({ async load() { return { text: 'Anki MCP Server - Ready to manage your flashcards!', }; }, mimeType: 'text/plain', name: 'Server Status', uri: 'anki://status', }); // Start server server.start({ transportType: 'stdio', });

Implementation Reference

Latest Blog Posts

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

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