tarot-mcp-server
Version:
Model Context Protocol server for Rider-Waite tarot card readings
204 lines (203 loc) • 6.87 kB
JavaScript
/**
* Advanced search functionality for the tarot card database
*/
export class TarotCardSearch {
cards;
constructor(cards) {
this.cards = cards;
}
/**
* Search cards by various criteria
*/
search(options) {
let results = [];
for (const card of this.cards) {
const result = this.evaluateCard(card, options);
if (result.relevanceScore > 0) {
results.push(result);
}
}
// Sort by relevance score (highest first)
return results.sort((a, b) => b.relevanceScore - a.relevanceScore);
}
/**
* Search by keyword in card meanings, keywords, and symbolism
*/
searchByKeyword(keyword) {
return this.search({ keyword });
}
/**
* Find cards by suit
*/
findBySuit(suit) {
return this.cards.filter(card => card.suit === suit);
}
/**
* Find cards by arcana type
*/
findByArcana(arcana) {
return this.cards.filter(card => card.arcana === arcana);
}
/**
* Find cards by element
*/
findByElement(element) {
return this.cards.filter(card => card.element === element);
}
/**
* Get cards with similar meanings
*/
findSimilarCards(cardId, limit = 5) {
const targetCard = this.cards.find(card => card.id === cardId);
if (!targetCard)
return [];
const similarities = [];
for (const card of this.cards) {
if (card.id === cardId)
continue;
const score = this.calculateSimilarity(targetCard, card);
similarities.push({ card, score });
}
return similarities
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(item => item.card);
}
/**
* Get random cards with optional filters
*/
getRandomCards(count = 1, options) {
let filteredCards = this.cards;
if (options) {
const searchResults = this.search(options);
filteredCards = searchResults.map(result => result.card);
}
const shuffled = [...filteredCards].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(count, shuffled.length));
}
evaluateCard(card, options) {
let score = 0;
const matchedFields = [];
// Exact matches get higher scores
if (options.suit && card.suit === options.suit) {
score += 10;
matchedFields.push('suit');
}
if (options.arcana && card.arcana === options.arcana) {
score += 8;
matchedFields.push('arcana');
}
if (options.element && card.element === options.element) {
score += 8;
matchedFields.push('element');
}
if (options.number !== undefined && card.number === options.number) {
score += 10;
matchedFields.push('number');
}
// Keyword search in various fields
if (options.keyword) {
const keyword = options.keyword.toLowerCase();
// Search in card name
if (card.name.toLowerCase().includes(keyword)) {
score += 15;
matchedFields.push('name');
}
// Search in keywords
const orientation = options.orientation || 'upright';
const keywords = card.keywords[orientation];
if (keywords.some(kw => kw.toLowerCase().includes(keyword))) {
score += 12;
matchedFields.push('keywords');
}
// Search in meanings
const meanings = card.meanings[orientation];
for (const [field, meaning] of Object.entries(meanings)) {
if (meaning.toLowerCase().includes(keyword)) {
score += 8;
matchedFields.push(`meaning_${field}`);
}
}
// Search in symbolism
if (card.symbolism.some(symbol => symbol.toLowerCase().includes(keyword))) {
score += 6;
matchedFields.push('symbolism');
}
// Search in description
if (card.description.toLowerCase().includes(keyword)) {
score += 4;
matchedFields.push('description');
}
}
return {
card,
relevanceScore: score,
matchedFields
};
}
calculateSimilarity(card1, card2) {
let score = 0;
// Same suit/arcana
if (card1.suit === card2.suit)
score += 3;
if (card1.arcana === card2.arcana)
score += 2;
if (card1.element === card2.element)
score += 3;
// Similar numbers
if (card1.number !== undefined && card2.number !== undefined) {
const numDiff = Math.abs(card1.number - card2.number);
if (numDiff <= 1)
score += 2;
else if (numDiff <= 2)
score += 1;
}
// Keyword overlap
const keywords1 = [...card1.keywords.upright, ...card1.keywords.reversed];
const keywords2 = [...card2.keywords.upright, ...card2.keywords.reversed];
for (const kw1 of keywords1) {
for (const kw2 of keywords2) {
if (kw1.toLowerCase() === kw2.toLowerCase()) {
score += 2;
}
}
}
return score;
}
/**
* Get statistics about the card database
*/
getStatistics() {
const stats = {
totalCards: this.cards.length,
majorArcana: this.cards.filter(c => c.arcana === 'major').length,
minorArcana: this.cards.filter(c => c.arcana === 'minor').length,
suits: {},
elements: {},
mostCommonKeywords: this.getMostCommonKeywords(10)
};
// Count by suits
for (const card of this.cards) {
if (card.suit) {
stats.suits[card.suit] = (stats.suits[card.suit] || 0) + 1;
}
if (card.element) {
stats.elements[card.element] = (stats.elements[card.element] || 0) + 1;
}
}
return stats;
}
getMostCommonKeywords(limit) {
const keywordCounts = {};
for (const card of this.cards) {
const allKeywords = [...card.keywords.upright, ...card.keywords.reversed];
for (const keyword of allKeywords) {
keywordCounts[keyword] = (keywordCounts[keyword] || 0) + 1;
}
}
return Object.entries(keywordCounts)
.map(([keyword, count]) => ({ keyword, count }))
.sort((a, b) => b.count - a.count)
.slice(0, limit);
}
}