Browse Franchise
browse_franchiseList all Disney Lorcana franchises with card counts, or input a franchise name to view its cards and statistics.
Instructions
Browse Disney Lorcana cards by franchise (story). Without a franchise name, lists all franchises with card counts. With a franchise name, shows cards and statistics.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| franchise | No | Franchise/story name (e.g. "Frozen", "Moana"). Omit to list all franchises. |
Implementation Reference
- src/tools/browse-franchise.ts:34-120 (handler)The main handler function that registers and implements the browse_franchise tool. Without a franchise argument it lists all franchises with card counts; with a franchise it shows card details and statistics (ink colors, types, rarities, sets). Also provides partial-match suggestions when a franchise is not found.
export function registerBrowseFranchise(server: McpServer, db: Database.Database): void { server.registerTool( 'browse_franchise', { title: 'Browse Franchise', description: 'Browse Disney Lorcana cards by franchise (story). Without a franchise name, lists all franchises with card counts. With a franchise name, shows cards and statistics.', inputSchema: { franchise: z.string().optional().describe('Franchise/story name (e.g. "Frozen", "Moana"). Omit to list all franchises.'), }, }, async (args) => { if (!args.franchise) { // List all franchises const franchises = listFranchises(db); if (franchises.length === 0) { return { content: [{ type: 'text' as const, text: 'No franchises found in the database.' }], }; } const lines = franchises.map( (f) => ` ${f.story} (${f.count} card${f.count !== 1 ? 's' : ''})`, ); return { content: [ { type: 'text' as const, text: `Found ${franchises.length} franchises:\n\n${lines.join('\n')}`, }, ], }; } // Browse specific franchise const { rows, total } = getCardsByFranchise(db, args.franchise, 1000, 0); if (total === 0) { // Try partial match const allFranchises = listFranchises(db); const matches = allFranchises.filter( (f) => f.story.toLowerCase().includes(args.franchise!.toLowerCase()), ); let text = `Franchise "${args.franchise}" not found.`; if (matches.length > 0) { text += `\n\nDid you mean one of these?\n${matches.map((f) => ` - ${f.story} (${f.count} cards)`).join('\n')}`; } return { content: [{ type: 'text' as const, text }], isError: true, }; } // Compute stats const inkDist = computeDistribution(rows, 'color'); const typeDist = computeDistribution(rows, 'type'); const rarityDist = computeDistribution(rows, 'rarity'); const setDist = computeDistribution(rows, 'set_code'); const header = [ `**${rows[0].story ?? args.franchise}** — ${total} card${total !== 1 ? 's' : ''}`, '', 'Stats:', ` Ink colors: ${formatDistribution(inkDist)}`, ` Types: ${formatDistribution(typeDist)}`, ` Rarities: ${formatDistribution(rarityDist)}`, ` Sets: ${formatDistribution(setDist)}`, '', 'Cards:', ].join('\n'); const cardLines = rows.map(formatCardBrief); return { content: [ { type: 'text' as const, text: header + '\n' + cardLines.join('\n'), }, ], }; }, ); } - src/tools/browse-franchise.ts:37-44 (schema)Input schema for the browse_franchise tool. Defines an optional 'franchise' string parameter to specify the franchise/story name (e.g., 'Frozen', 'Moana'), or omit to list all franchises.
{ title: 'Browse Franchise', description: 'Browse Disney Lorcana cards by franchise (story). Without a franchise name, lists all franchises with card counts. With a franchise name, shows cards and statistics.', inputSchema: { franchise: z.string().optional().describe('Franchise/story name (e.g. "Frozen", "Moana"). Omit to list all franchises.'), }, }, - src/server.ts:9-46 (registration)Import of registerBrowseFranchise from the browse-franchise tool module.
import { registerBrowseFranchise } from './tools/browse-franchise.js'; import { registerAnalyzeInkCurve } from './tools/analyze-ink-curve.js'; import { registerAnalyzeLore } from './tools/analyze-lore.js'; import { registerFindSongSynergies } from './tools/find-song-synergies.js'; import { readFileSync, realpathSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import type Database from 'better-sqlite3'; export interface ServerOptions { db?: Database.Database; dataDir?: string; } export function createServer(options?: ServerOptions): McpServer { const __dirname = path.dirname(fileURLToPath(import.meta.url)); let version = '0.0.0'; try { const pkg = JSON.parse( readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'), ); version = pkg.version; } catch { // Fallback version if package.json not found (e.g., in tests) } const server = new McpServer({ name: 'lorcana-oracle', version, }); const db = options?.db ?? getDatabase(options?.dataDir); // Register tools registerSearchCards(server, db); registerBrowseSets(server, db); registerCharacterVersions(server, db); registerBrowseFranchise(server, db); - src/server.ts:46-46 (registration)Registration call to registerBrowseFranchise(server, db) in the createServer function.
registerBrowseFranchise(server, db); - src/data/db.ts:194-200 (helper)Database helper function listFranchises that queries all distinct stories with card counts, ordered by count descending.
export function listFranchises(db: Database.Database): { story: string; count: number }[] { return db .prepare( "SELECT story, COUNT(*) as count FROM cards WHERE story IS NOT NULL AND story != '' GROUP BY story ORDER BY count DESC", ) .all() as { story: string; count: number }[]; } - src/data/db.ts:202-219 (helper)Database helper function getCardsByFranchise that queries cards for a specific franchise (case-insensitive match) with limit/offset pagination.
export function getCardsByFranchise( db: Database.Database, franchise: string, limit = 20, offset = 0, ): SearchResult { const countRow = db .prepare( 'SELECT COUNT(*) as count FROM cards WHERE LOWER(story) = LOWER(?)', ) .get(franchise) as { count: number }; const rows = db .prepare( 'SELECT * FROM cards WHERE LOWER(story) = LOWER(?) ORDER BY cost ASC, name ASC LIMIT ? OFFSET ?', ) .all(franchise, limit, offset) as CardRow[]; return { rows, total: countRow.count }; } - src/tools/browse-franchise.ts:7-25 (helper)Helper function formatCardBrief that formats a single card row into a display string including name, color, type, cost, stats and rarity.
function formatCardBrief(card: CardRow): string { const stats: string[] = []; if (card.type === 'Character') { stats.push(`${card.strength ?? '—'}/${card.willpower ?? '—'}/${card.lore ?? '—'}`); } else if (card.type === 'Location' && card.lore !== null) { stats.push(`Lore: ${card.lore}`); } const statsStr = stats.length > 0 ? ` | ${stats.join(' ')}` : ''; return ` ${card.full_name ?? card.name} — ${card.color} ${card.type} | Cost ${card.cost ?? '—'}${statsStr} | ${card.rarity ?? '—'}`; } function computeDistribution(cards: CardRow[], key: keyof CardRow): Map<string, number> { const dist = new Map<string, number>(); for (const card of cards) { const val = String(card[key] ?? 'Unknown'); dist.set(val, (dist.get(val) ?? 0) + 1); } return dist; } - src/tools/browse-franchise.ts:18-32 (helper)Helper functions computeDistribution and formatDistribution for computing and formatting statistical distributions of card properties (ink, type, rarity, set).
function computeDistribution(cards: CardRow[], key: keyof CardRow): Map<string, number> { const dist = new Map<string, number>(); for (const card of cards) { const val = String(card[key] ?? 'Unknown'); dist.set(val, (dist.get(val) ?? 0) + 1); } return dist; } function formatDistribution(dist: Map<string, number>): string { return [...dist.entries()] .sort((a, b) => b[1] - a[1]) .map(([key, count]) => `${key}: ${count}`) .join(', '); }