Skip to main content
Glama
gregario

lorcana-oracle

Find Song Synergies

find_song_synergies

Find Disney Lorcana characters that can sing a given song, or songs a character can sing, based on ink cost. Also browse all songs with singer counts.

Instructions

Find Disney Lorcana song synergies. Given a Song, find characters that can sing it (cost >= song cost). Given a Character, find songs they can sing (cost <= character cost). With no input, browse all songs with singer counts.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
card_nameNoCard name to find synergies for (Song or Character)
inkNoFilter results by ink color

Implementation Reference

  • The async handler that contains the main tool logic for find_song_synergies.
      async (args) => {
        // Browse mode — no card specified
        if (!args.card_name) {
          const songs = getSongCards(db);
          if (songs.length === 0) {
            return {
              content: [{ type: 'text' as const, text: 'No songs found in the database.' }],
            };
          }
    
          const lines: string[] = [];
          lines.push('## All Songs\n');
          for (const song of songs) {
            const singers = getCharactersByMinCost(db, song.cost ?? 0);
            const filteredSingers = args.ink
              ? singers.filter((c) => c.color.toLowerCase() === args.ink!.toLowerCase())
              : singers;
            lines.push(`${formatSongCard(song)} — ${filteredSingers.length} potential singers`);
          }
    
          return {
            content: [{ type: 'text' as const, text: lines.join('\n') }],
          };
        }
    
        // Look up the card
        const card = getCardByName(db, args.card_name);
        if (!card) {
          // Try partial match for suggestions
          const { rows } = searchCards(db, { query: args.card_name, limit: 5 });
          if (rows.length > 0) {
            const suggestions = rows.map((c) => c.full_name ?? c.name).join(', ');
            return {
              content: [{ type: 'text' as const, text: `Card "${args.card_name}" not found. Did you mean: ${suggestions}?` }],
            };
          }
          return {
            content: [{ type: 'text' as const, text: `Card "${args.card_name}" not found.` }],
          };
        }
    
        // Song mode — find characters that can sing this song
        if (card.type === 'Song') {
          const singers = getCharactersByMinCost(db, card.cost ?? 0);
          let filtered = args.ink
            ? singers.filter((c) => c.color.toLowerCase() === args.ink!.toLowerCase())
            : singers;
    
          const lines: string[] = [];
          lines.push(`## Singers for ${card.full_name ?? card.name} (Cost ${card.cost})\n`);
    
          if (filtered.length === 0) {
            lines.push('No characters found that can sing this song.');
          } else {
            // Group by color
            const byColor = new Map<string, CardRow[]>();
            for (const singer of filtered) {
              const group = byColor.get(singer.color) ?? [];
              group.push(singer);
              byColor.set(singer.color, group);
            }
    
            for (const [color, chars] of [...byColor.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
              lines.push(`### ${color} (${chars.length})`);
              for (const c of chars) {
                lines.push(`  ${formatSingerCard(c)}`);
              }
            }
          }
    
          return {
            content: [{ type: 'text' as const, text: lines.join('\n') }],
          };
        }
    
        // Character mode — find songs this character can sing
        if (card.type === 'Character') {
          const songs = getSongsByMaxCost(db, card.cost ?? 0);
          const filtered = args.ink
            ? songs.filter((s) => s.color.toLowerCase() === args.ink!.toLowerCase())
            : songs;
    
          const lines: string[] = [];
          lines.push(`## Songs for ${card.full_name ?? card.name} (Cost ${card.cost})\n`);
    
          if (filtered.length === 0) {
            lines.push('No songs found that this character can sing.');
          } else {
            for (const song of filtered) {
              lines.push(`  ${formatSongCard(song)}`);
            }
          }
    
          return {
            content: [{ type: 'text' as const, text: lines.join('\n') }],
          };
        }
    
        // Non-song/character
        return {
          content: [{ type: 'text' as const, text: `"${card.full_name ?? card.name}" is a ${card.type}. Song synergies only apply to Songs and Characters. Songs can be sung by Characters with cost >= song cost.` }],
        };
      },
    );
  • The complete registration function including schema definition and handler for find_song_synergies.
    export function registerFindSongSynergies(server: McpServer, db: Database.Database): void {
      server.registerTool(
        'find_song_synergies',
        {
          title: 'Find Song Synergies',
          description:
            'Find Disney Lorcana song synergies. Given a Song, find characters that can sing it (cost >= song cost). Given a Character, find songs they can sing (cost <= character cost). With no input, browse all songs with singer counts.',
          inputSchema: {
            card_name: z.string().optional().describe('Card name to find synergies for (Song or Character)'),
            ink: z.string().optional().describe('Filter results by ink color'),
          },
        },
        async (args) => {
          // Browse mode — no card specified
          if (!args.card_name) {
            const songs = getSongCards(db);
            if (songs.length === 0) {
              return {
                content: [{ type: 'text' as const, text: 'No songs found in the database.' }],
              };
            }
    
            const lines: string[] = [];
            lines.push('## All Songs\n');
            for (const song of songs) {
              const singers = getCharactersByMinCost(db, song.cost ?? 0);
              const filteredSingers = args.ink
                ? singers.filter((c) => c.color.toLowerCase() === args.ink!.toLowerCase())
                : singers;
              lines.push(`${formatSongCard(song)} — ${filteredSingers.length} potential singers`);
            }
    
            return {
              content: [{ type: 'text' as const, text: lines.join('\n') }],
            };
          }
    
          // Look up the card
          const card = getCardByName(db, args.card_name);
          if (!card) {
            // Try partial match for suggestions
            const { rows } = searchCards(db, { query: args.card_name, limit: 5 });
            if (rows.length > 0) {
              const suggestions = rows.map((c) => c.full_name ?? c.name).join(', ');
              return {
                content: [{ type: 'text' as const, text: `Card "${args.card_name}" not found. Did you mean: ${suggestions}?` }],
              };
            }
            return {
              content: [{ type: 'text' as const, text: `Card "${args.card_name}" not found.` }],
            };
          }
    
          // Song mode — find characters that can sing this song
          if (card.type === 'Song') {
            const singers = getCharactersByMinCost(db, card.cost ?? 0);
            let filtered = args.ink
              ? singers.filter((c) => c.color.toLowerCase() === args.ink!.toLowerCase())
              : singers;
    
            const lines: string[] = [];
            lines.push(`## Singers for ${card.full_name ?? card.name} (Cost ${card.cost})\n`);
    
            if (filtered.length === 0) {
              lines.push('No characters found that can sing this song.');
            } else {
              // Group by color
              const byColor = new Map<string, CardRow[]>();
              for (const singer of filtered) {
                const group = byColor.get(singer.color) ?? [];
                group.push(singer);
                byColor.set(singer.color, group);
              }
    
              for (const [color, chars] of [...byColor.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
                lines.push(`### ${color} (${chars.length})`);
                for (const c of chars) {
                  lines.push(`  ${formatSingerCard(c)}`);
                }
              }
            }
    
            return {
              content: [{ type: 'text' as const, text: lines.join('\n') }],
            };
          }
    
          // Character mode — find songs this character can sing
          if (card.type === 'Character') {
            const songs = getSongsByMaxCost(db, card.cost ?? 0);
            const filtered = args.ink
              ? songs.filter((s) => s.color.toLowerCase() === args.ink!.toLowerCase())
              : songs;
    
            const lines: string[] = [];
            lines.push(`## Songs for ${card.full_name ?? card.name} (Cost ${card.cost})\n`);
    
            if (filtered.length === 0) {
              lines.push('No songs found that this character can sing.');
            } else {
              for (const song of filtered) {
                lines.push(`  ${formatSongCard(song)}`);
              }
            }
    
            return {
              content: [{ type: 'text' as const, text: lines.join('\n') }],
            };
          }
    
          // Non-song/character
          return {
            content: [{ type: 'text' as const, text: `"${card.full_name ?? card.name}" is a ${card.type}. Song synergies only apply to Songs and Characters. Songs can be sung by Characters with cost >= song cost.` }],
          };
        },
      );
    }
  • src/server.ts:49-49 (registration)
    Registration of find_song_synergies on the MCP server.
    registerFindSongSynergies(server, db);
  • Helper function used by the handler to find characters that can sing a song.
    export function getCharactersByMinCost(
      db: Database.Database,
      minCost: number,
    ): CardRow[] {
      return db
        .prepare(
          "SELECT * FROM cards WHERE LOWER(type) = 'character' AND cost >= ? ORDER BY cost ASC, name ASC",
        )
        .all(minCost) as CardRow[];
    }
  • Helper function used by the handler to find songs a character can sing.
    export function getSongCards(
      db: Database.Database,
      maxCost?: number,
    ): CardRow[] {
      if (maxCost !== undefined) {
        return db
          .prepare(
            "SELECT * FROM cards WHERE LOWER(type) = 'song' AND cost <= ? ORDER BY cost ASC, name ASC",
          )
          .all(maxCost) as CardRow[];
      }
      return db
        .prepare(
          "SELECT * FROM cards WHERE LOWER(type) = 'song' ORDER BY cost ASC, name ASC",
        )
        .all() as CardRow[];
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full behavioral burden. It explains input-dependent behavior (Song vs Character) and the no-input case. However, it does not disclose what 'singer counts' means or any limitations (e.g., exact match vs fuzzy search). Adequate but not exhaustive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, no wasted words. The description is front-loaded with the purpose, then enumerates scenarios efficiently. Ideal length for quick comprehension.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, the description adequately covers core functionality and parameter usage. It could mention output format (e.g., list of cards) but the phrase 'browse all songs with singer counts' implies a tabular result. Slightly incomplete but sufficient for typical use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with both parameters documented. The description adds value by clarifying that card_name can be a Song or Character and that the tool determines direction automatically. The ink parameter's filtering role is clear.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool finds Disney Lorcana song synergies and explains three specific use cases: given a Song, find characters that can sing it; given a Character, find songs they can sing; with no input, browse all songs with singer counts. This specificity differentiates it from sibling tools like analyze_ink_curve or search_cards.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use each scenario based on input type. It implies alternatives (other analysis tools) but does not explicitly state when not to use this tool. The use cases are clear and actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/gregario/lorcana-oracle'

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