Skip to main content
Glama
consolidated.ts39.6 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { ankiClient } from '../utils/ankiClient.js'; /** * MIME type mapping for common media file extensions */ const MIME_TYPES: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', mp4: 'video/mp4', webm: 'video/webm', pdf: 'application/pdf', }; /** * Register consolidated, high-level tools with the MCP server * Following MCP best practices: domain-aware, intentional tool design * instead of exposing every low-level API endpoint */ export function registerConsolidatedTools(server: McpServer) { /** * TOOL 1: manage_flashcards * High-level flashcard (note/card) management * Covers: create, update, delete, find, tag operations */ 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)}` ); } } ); /** * TOOL 2: study_session * Interactive quiz and review operations * Covers: finding cards to study, answering cards, reviewing */ server.tool( 'study_session', { operation: z .enum(['find_due', 'answer', 'suspend', 'unsuspend', 'check_status', 'forget', 'relearn']) .describe('Study session operation'), query: z.string().optional().describe('Search query to find cards (for find_due)'), cardIds: z.array(z.number()).optional().describe('Card IDs to operate on'), answers: z .array( z.object({ cardId: z.number(), ease: z.number().min(1).max(4).describe('1=Again, 2=Hard, 3=Good, 4=Easy'), }) ) .optional() .describe('Card answers (for answer operation)'), days: z.string().optional().describe('Days from today for due date (for reschedule)'), }, async ({ operation, query, cardIds, answers }) => { try { switch (operation) { case 'find_due': { if (!query) { throw new Error('find_due requires query parameter'); } const foundCardIds = await ankiClient.card.findCards({ query }); if (foundCardIds.length > 0) { const dueStatus = await ankiClient.card.areDue({ cards: foundCardIds }); const dueCards = foundCardIds.filter((_, idx) => dueStatus[idx]); return { content: [ { type: 'text', text: `Found ${foundCardIds.length} cards matching "${query}"\n${dueCards.length} are due for review now\nDue card IDs: [${dueCards.join(', ')}]`, }, ], }; } return { content: [ { type: 'text', text: `No cards found matching "${query}"`, }, ], }; } case 'answer': { if (!answers || answers.length === 0) { throw new Error('answer requires answers array'); } const results = await ankiClient.card.answerCards({ answers }); const successCount = results.filter(Boolean).length; return { content: [ { type: 'text', text: `✓ Answered ${successCount}/${answers.length} cards successfully`, }, ], }; } case 'suspend': { if (!cardIds || cardIds.length === 0) { throw new Error('suspend requires cardIds'); } await ankiClient.card.suspend({ cards: cardIds }); return { content: [ { type: 'text', text: `✓ Suspended ${cardIds.length} card(s)`, }, ], }; } case 'unsuspend': { if (!cardIds || cardIds.length === 0) { throw new Error('unsuspend requires cardIds'); } await ankiClient.card.unsuspend({ cards: cardIds }); return { content: [ { type: 'text', text: `✓ Unsuspended ${cardIds.length} card(s)`, }, ], }; } case 'check_status': { if (!cardIds || cardIds.length === 0) { throw new Error('check_status requires cardIds'); } const [dueStatus, suspendedStatus] = await Promise.all([ ankiClient.card.areDue({ cards: cardIds }), ankiClient.card.areSuspended({ cards: cardIds }), ]); const statusInfo = cardIds.map((id, idx) => ({ cardId: id, isDue: dueStatus[idx], isSuspended: suspendedStatus[idx], })); return { content: [ { type: 'text', text: `Card status:\n${JSON.stringify(statusInfo, null, 2)}`, }, ], }; } case 'forget': { if (!cardIds || cardIds.length === 0) { throw new Error('forget requires cardIds'); } await ankiClient.card.forgetCards({ cards: cardIds }); return { content: [ { type: 'text', text: `✓ Reset ${cardIds.length} card(s) to new status`, }, ], }; } case 'relearn': { if (!cardIds || cardIds.length === 0) { throw new Error('relearn requires cardIds'); } await ankiClient.card.relearnCards({ cards: cardIds }); return { content: [ { type: 'text', text: `✓ Set ${cardIds.length} card(s) to relearn`, }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `study_session failed: ${error instanceof Error ? error.message : String(error)}` ); } } ); /** * TOOL 3: manage_decks * Deck creation, configuration, and organization */ server.tool( 'manage_decks', { operation: z .enum(['create', 'delete', 'list', 'get_stats', 'move_cards', 'get_config', 'set_config']) .describe('Deck management operation'), deckName: z.string().optional().describe('Deck name'), deckNames: z.array(z.string()).optional().describe('Deck names (for batch operations)'), deleteCards: z.boolean().optional().describe('Delete cards when deleting deck'), cardIds: z.array(z.number()).optional().describe('Card IDs to move'), targetDeck: z.string().optional().describe('Target deck for moving cards'), configId: z.number().optional().describe('Config ID to apply'), }, async ({ operation, deckName, deckNames, deleteCards, cardIds, targetDeck, configId }) => { try { switch (operation) { case 'create': { if (!deckName) { throw new Error('create requires deckName'); } const result = await ankiClient.deck.createDeck({ deck: deckName }); return { content: [ { type: 'text', text: `✓ Created deck "${deckName}" (ID: ${result})`, }, ], }; } case 'delete': { if (!deckNames || deckNames.length === 0) { throw new Error('delete requires deckNames'); } await ankiClient.deck.deleteDecks({ decks: deckNames, cardsToo: (deleteCards ?? true) as true, }); return { content: [ { type: 'text', text: `✓ Deleted deck(s): ${deckNames.join(', ')}${deleteCards !== false ? ' (including cards)' : ''}`, }, ], }; } case 'list': { const decks = await ankiClient.deck.deckNamesAndIds(); const deckList = Object.entries(decks) .map(([name, id]) => ` • ${name} (ID: ${id})`) .join('\n'); return { content: [ { type: 'text', text: `Decks (${Object.keys(decks).length}):\n${deckList}`, }, ], }; } case 'get_stats': { if (!deckName) { throw new Error('get_stats requires deckName'); } const stats = await ankiClient.deck.getDeckStats({ decks: [deckName] }); return { content: [ { type: 'text', text: `Statistics for "${deckName}":\n${JSON.stringify(stats, null, 2)}`, }, ], }; } case 'move_cards': { if (!cardIds || !targetDeck) { throw new Error('move_cards requires cardIds and targetDeck'); } await ankiClient.deck.changeDeck({ cards: cardIds, deck: targetDeck }); return { content: [ { type: 'text', text: `✓ Moved ${cardIds.length} card(s) to "${targetDeck}"`, }, ], }; } case 'get_config': { if (!deckName) { throw new Error('get_config requires deckName'); } const config = await ankiClient.deck.getDeckConfig({ deck: deckName }); return { content: [ { type: 'text', text: `Config for "${deckName}":\n${JSON.stringify(config, null, 2)}`, }, ], }; } case 'set_config': { if (!deckNames || !configId) { throw new Error('set_config requires deckNames and configId'); } await ankiClient.deck.setDeckConfigId({ configId, decks: deckNames }); return { content: [ { type: 'text', text: `✓ Applied config ${configId} to: ${deckNames.join(', ')}`, }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `manage_decks failed: ${error instanceof Error ? error.message : String(error)}` ); } } ); /** * TOOL 4: get_analytics * Statistics and learning insights */ server.tool( 'get_analytics', { scope: z .enum([ 'deck_stats', 'collection_stats', 'reviews_by_day', 'reviews_today', 'card_reviews', 'card_details', ]) .describe('Analytics scope'), deckName: z.string().optional().describe('Deck name for deck-specific stats'), cardIds: z.array(z.number()).optional().describe('Card IDs for detailed analysis'), startTimestamp: z.number().optional().describe('Start timestamp for review history'), wholeCollection: z .boolean() .optional() .describe('Get whole collection stats vs current deck'), }, async ({ scope, deckName, cardIds, startTimestamp, wholeCollection }) => { try { switch (scope) { case 'deck_stats': { if (!deckName) { throw new Error('deck_stats requires deckName'); } const stats = await ankiClient.deck.getDeckStats({ decks: [deckName] }); return { content: [ { type: 'text', text: `📊 Statistics for "${deckName}":\n${JSON.stringify(stats, null, 2)}`, }, ], }; } case 'collection_stats': { const statsHTML = await ankiClient.statistic.getCollectionStatsHTML({ wholeCollection: wholeCollection ?? true, }); return { content: [ { type: 'text', text: `📊 Collection Statistics (${wholeCollection ? 'all decks' : 'current deck'}):\n${statsHTML}`, }, ], }; } case 'reviews_by_day': { const reviewsByDay = await ankiClient.statistic.getNumCardsReviewedByDay(); return { content: [ { type: 'text', text: `📊 Reviews by day (${reviewsByDay.length} days):\n${JSON.stringify(reviewsByDay, null, 2)}`, }, ], }; } case 'reviews_today': { const count = await ankiClient.statistic.getNumCardsReviewedToday(); return { content: [ { type: 'text', text: `📊 Cards reviewed today: ${count}`, }, ], }; } case 'card_reviews': { if (!deckName || startTimestamp === undefined) { throw new Error('card_reviews requires deckName and startTimestamp'); } const reviews = await ankiClient.statistic.cardReviews({ deck: deckName, startID: startTimestamp, }); return { content: [ { type: 'text', text: `📊 Found ${reviews.length} reviews for "${deckName}" after ${startTimestamp}:\n${JSON.stringify(reviews, null, 2)}`, }, ], }; } case 'card_details': { if (!cardIds || cardIds.length === 0) { throw new Error('card_details requires cardIds'); } const [easeFactors, intervals] = await Promise.all([ ankiClient.card.getEaseFactors({ cards: cardIds }), ankiClient.card.getIntervals({ cards: cardIds, complete: false }), ]); const details = cardIds.map((id, idx) => ({ cardId: id, easeFactor: easeFactors[idx], currentInterval: intervals[idx], })); return { content: [ { type: 'text', text: `📊 Card details:\n${JSON.stringify(details, null, 2)}`, }, ], }; } default: throw new Error(`Unknown scope: ${scope}`); } } catch (error) { throw new Error( `get_analytics failed: ${error instanceof Error ? error.message : String(error)}` ); } } ); /** * TOOL 5: manage_models * Note type (model) configuration */ server.tool( 'manage_models', { operation: z .enum([ 'list', 'create', 'add_field', 'remove_field', 'rename_field', 'add_template', 'remove_template', 'update_styling', ]) .describe('Model management operation'), modelName: z.string().optional().describe('Model/note type name'), // For create fields: z.array(z.string()).optional().describe('Field names in order (for create)'), templates: z .array( z.object({ Front: z.string(), Back: z.string(), }) ) .optional() .describe('Card templates (for create)'), css: z.string().optional().describe('CSS styling'), isCloze: z.boolean().optional().describe('Is cloze deletion type'), // For field operations fieldName: z.string().optional().describe('Field name'), newFieldName: z.string().optional().describe('New field name (for rename)'), fieldIndex: z.number().optional().describe('Field index position'), // For template operations templateName: z.string().optional().describe('Template name'), template: z .object({ Front: z.string(), Back: z.string(), }) .optional() .describe('Template content'), }, async ({ operation, modelName, fields, templates, css, isCloze, fieldName, newFieldName, fieldIndex, templateName, template, }) => { try { switch (operation) { case 'list': { const models = await ankiClient.model.modelNamesAndIds(); const modelList = Object.entries(models) .map(([name, id]) => ` • ${name} (ID: ${id})`) .join('\n'); return { content: [ { type: 'text', text: `Note Types (${Object.keys(models).length}):\n${modelList}`, }, ], }; } case 'create': { if (!modelName || !fields || !templates) { throw new Error('create requires modelName, fields, and templates'); } const modelParams: any = { modelName, inOrderFields: fields, cardTemplates: templates, }; if (css) modelParams.css = css; if (isCloze !== undefined) modelParams.isCloze = isCloze; const result = await ankiClient.model.createModel(modelParams); return { content: [ { type: 'text', text: `✓ Created note type "${modelName}" (ID: ${result.id})`, }, ], }; } case 'add_field': { if (!modelName || !fieldName || fieldIndex === undefined) { throw new Error('add_field requires modelName, fieldName, and fieldIndex'); } await ankiClient.model.modelFieldAdd({ modelName, fieldName, index: fieldIndex }); return { content: [ { type: 'text', text: `✓ Added field "${fieldName}" to "${modelName}" at position ${fieldIndex}`, }, ], }; } case 'remove_field': { if (!modelName || !fieldName) { throw new Error('remove_field requires modelName and fieldName'); } await ankiClient.model.modelFieldRemove({ modelName, fieldName }); return { content: [ { type: 'text', text: `✓ Removed field "${fieldName}" from "${modelName}"`, }, ], }; } case 'rename_field': { if (!modelName || !fieldName || !newFieldName) { throw new Error('rename_field requires modelName, fieldName, and newFieldName'); } await ankiClient.model.modelFieldRename({ modelName, oldFieldName: fieldName, newFieldName, }); return { content: [ { type: 'text', text: `✓ Renamed field "${fieldName}" to "${newFieldName}" in "${modelName}"`, }, ], }; } case 'add_template': { if (!modelName || !template) { throw new Error('add_template requires modelName and template'); } await ankiClient.model.modelTemplateAdd({ modelName, template }); return { content: [ { type: 'text', text: `✓ Added template to "${modelName}"`, }, ], }; } case 'remove_template': { if (!modelName || !templateName) { throw new Error('remove_template requires modelName and templateName'); } await ankiClient.model.modelTemplateRemove({ modelName, templateName }); return { content: [ { type: 'text', text: `✓ Removed template "${templateName}" from "${modelName}"`, }, ], }; } case 'update_styling': { if (!modelName || !css) { throw new Error('update_styling requires modelName and css'); } await ankiClient.model.updateModelStyling({ model: { name: modelName, css } }); return { content: [ { type: 'text', text: `✓ Updated styling for "${modelName}"`, }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `manage_models failed: ${error instanceof Error ? error.message : String(error)}` ); } } ); /** * TOOL 6: anki_operations * Utility operations: sync, media, export/import, version */ server.tool( 'anki_operations', { operation: z .enum([ 'sync', 'version', 'export_deck', 'import_package', 'store_media', 'retrieve_media', 'delete_media', 'list_media', 'get_profiles', ]) .describe('Anki utility operation'), // Export/import deckName: z.string().optional().describe('Deck name to export'), filePath: z.string().optional().describe('File path for export/import'), includeSched: z.boolean().optional().describe('Include scheduling in export'), // Media filename: z.string().optional().describe('Media filename'), mediaData: z.string().optional().describe('Base64 encoded media data'), mediaUrl: z.string().optional().describe('URL to download media from'), pattern: z.string().optional().describe('Pattern to match media files'), }, async ({ operation, deckName, filePath, includeSched, filename, mediaData, mediaUrl, pattern, }) => { try { switch (operation) { case 'sync': { await ankiClient.miscellaneous.sync(); return { content: [ { type: 'text', text: '✓ Sync initiated successfully', }, ], }; } case 'version': { const version = await ankiClient.miscellaneous.version(); return { content: [ { type: 'text', text: `AnkiConnect version: ${version}`, }, ], }; } case 'export_deck': { if (!deckName || !filePath) { throw new Error('export_deck requires deckName and filePath'); } const params: any = { deck: deckName, path: filePath }; if (includeSched !== undefined) params.includeSched = includeSched; await ankiClient.miscellaneous.exportPackage(params); return { content: [ { type: 'text', text: `✓ Exported deck "${deckName}" to ${filePath}`, }, ], }; } case 'import_package': { if (!filePath) { throw new Error('import_package requires filePath'); } await ankiClient.miscellaneous.importPackage({ path: filePath }); return { content: [ { type: 'text', text: `✓ Imported package from ${filePath}`, }, ], }; } case 'store_media': { if (!filename) { throw new Error('store_media requires filename'); } if (!mediaData && !mediaUrl) { throw new Error('store_media requires either mediaData or mediaUrl'); } const params: any = { filename }; if (mediaData) params.data = mediaData; if (mediaUrl) params.url = mediaUrl; const storedName = await ankiClient.media.storeMediaFile(params); return { content: [ { type: 'text', text: `✓ Stored media file as: ${storedName}`, }, ], }; } case 'retrieve_media': { if (!filename) { throw new Error('retrieve_media requires filename'); } const content = await ankiClient.media.retrieveMediaFile({ filename }); if (content === false) { return { content: [ { type: 'text', text: `Media file "${filename}" not found`, }, ], }; } return { content: [ { type: 'text', text: `Retrieved "${filename}" (${content.length} chars)`, }, ], }; } case 'delete_media': { if (!filename) { throw new Error('delete_media requires filename'); } await ankiClient.media.deleteMediaFile({ filename }); return { content: [ { type: 'text', text: `✓ Deleted media file: ${filename}`, }, ], }; } case 'list_media': { if (!pattern) { throw new Error('list_media requires pattern (e.g., "*.jpg", "*")'); } const files = await ankiClient.media.getMediaFilesNames({ pattern }); return { content: [ { type: 'text', text: `Media files matching "${pattern}" (${files.length}):\n${JSON.stringify(files, null, 2)}`, }, ], }; } case 'get_profiles': { const profiles = await ankiClient.miscellaneous.getProfiles(); return { content: [ { type: 'text', text: `Available profiles:\n${JSON.stringify(profiles, null, 2)}`, }, ], }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { throw new Error( `anki_operations failed: ${error instanceof Error ? error.message : String(error)}` ); } } ); /** * TOOL 7: get_media_file * Retrieve media files from Anki collection as base64-encoded data * This enables AI assistants to view and work with images/audio referenced in cards */ server.tool( 'get_media_file', { filename: z.string().describe('Media filename referenced in Anki cards (e.g., "image.png")'), }, async ({ filename }) => { try { // Validate filename (basic security check) if ( !filename || filename.includes('..') || filename.includes('/') || filename.includes('\\') ) { throw new Error('Invalid filename: must be a simple filename without path traversal'); } // Retrieve the media file from AnkiConnect const base64Content = await ankiClient.media.retrieveMediaFile({ filename }); if (base64Content === false) { return { content: [ { type: 'text', text: JSON.stringify({ error: `File not found: ${filename}`, filename, }), }, ], }; } // Detect MIME type from file extension const extension = filename.split('.').pop()?.toLowerCase() || ''; const mimeType = MIME_TYPES[extension] || 'application/octet-stream'; // Calculate size (approximate from base64 length) const size = Math.floor((base64Content.length * 3) / 4); // Return the file data with metadata const response = { filename, mimeType, base64Data: base64Content, size, }; // For images, return as image content that Claude can analyze // Note: Claude can see the image but it won't display to the user if (mimeType.startsWith('image/')) { return { content: [ { type: 'image', data: base64Content, mimeType, }, { type: 'text', text: `📷 Image: ${filename} Format: ${mimeType} Size: ${(size / 1024).toFixed(1)} KB Note: I can see and analyze this image, but it won't display in the UI. I can describe what I see or answer questions about it.`, }, ], }; } // For non-images, return as text with metadata return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: `Failed to retrieve media file: ${error instanceof Error ? error.message : String(error)}`, filename, }), }, ], }; } } ); }

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

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