#!/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',
});