Skip to main content
Glama
drewsungg

Pokemon Showdown MCP Server

by drewsungg
pokemon_server.py18.2 kB
#!/usr/bin/env python3 """ Pokemon Data MCP Server Provides tools for looking up Pokemon, moves, abilities, items, and type effectiveness. Designed to help LLMs make informed decisions during Pokemon battles. Tools: - get_pokemon: Look up Pokemon stats, types, and abilities - get_move: Look up move details and effects - get_ability: Look up ability descriptions - get_item: Look up held item effects - get_type_effectiveness: Calculate type matchup multipliers - search_priority_moves: Find all priority moves - search_pokemon_by_ability: Find Pokemon with a specific ability """ import json from typing import Any from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import TextContent, Tool try: from mcpkmn_showdown.data_loader import get_loader except ImportError: from .data_loader import get_loader # Create server instance server = Server("pokemon-data") def format_pokemon_response(pokemon: dict) -> str: """Format Pokemon data into a readable response.""" name = pokemon.get("name", "Unknown") types = pokemon.get("types", []) stats = pokemon.get("baseStats", {}) abilities = pokemon.get("abilities", {}) weight = pokemon.get("weightkg", 0) tier = pokemon.get("tier", "Unknown") # Get ability descriptions loader = get_loader() ability_details = [] for key, ability_name in abilities.items(): ability_data = loader.get_ability(ability_name) if ability_data: desc = ability_data.get("shortDesc") or ability_data.get("desc", "") slot = "Hidden" if key == "H" else f"Slot {int(key) + 1}" ability_details.append(f" - {ability_name} ({slot}): {desc}") else: slot = "Hidden" if key == "H" else f"Slot {int(key) + 1}" ability_details.append(f" - {ability_name} ({slot})") response = f"""## {name} **Types:** {', '.join(types)} **Tier:** {tier} **Weight:** {weight}kg ### Base Stats - HP: {stats.get('hp', '?')} - Attack: {stats.get('atk', '?')} - Defense: {stats.get('def', '?')} - Sp. Attack: {stats.get('spa', '?')} - Sp. Defense: {stats.get('spd', '?')} - Speed: {stats.get('spe', '?')} - **Total:** {sum(stats.values())} ### Abilities {chr(10).join(ability_details)} """ return response def format_move_response(move: dict) -> str: """Format move data into a readable response.""" name = move.get("name", "Unknown") move_type = move.get("type", "Normal") category = move.get("category", "Status") power = move.get("basePower", 0) accuracy = move.get("accuracy", 100) pp = move.get("pp", 0) priority = move.get("priority", 0) desc = move.get("desc", move.get("shortDesc", "No description.")) # Handle accuracy = true (never misses) if accuracy is True: accuracy_str = "Never misses" else: accuracy_str = f"{accuracy}%" priority_str = "" if priority > 0: priority_str = f"\n**Priority:** +{priority} (moves before normal moves)" elif priority < 0: priority_str = f"\n**Priority:** {priority} (moves after normal moves)" # Secondary effects secondary = move.get("secondary") secondary_str = "" if secondary: chance = secondary.get("chance", 100) effect = [] if secondary.get("status"): effect.append(f"{secondary['status'].upper()}") if secondary.get("boosts"): boosts = [f"{k} {v:+d}" for k, v in secondary["boosts"].items()] effect.append(f"stat change: {', '.join(boosts)}") if secondary.get("volatileStatus"): effect.append(secondary["volatileStatus"]) if effect: secondary_str = f"\n**Secondary Effect ({chance}% chance):** {', '.join(effect)}" # Self effects self_effect = move.get("self", {}) self_str = "" if self_effect.get("boosts"): boosts = [f"{k} {v:+d}" for k, v in self_effect["boosts"].items()] self_str = f"\n**Self Effect:** {', '.join(boosts)}" # Drain/recoil drain_str = "" if move.get("drain"): drain_pct = int(move["drain"][0] / move["drain"][1] * 100) drain_str = f"\n**Drain:** Heals {drain_pct}% of damage dealt" if move.get("recoil"): recoil_pct = int(move["recoil"][0] / move["recoil"][1] * 100) drain_str = f"\n**Recoil:** Takes {recoil_pct}% of damage dealt" response = f"""## {name} **Type:** {move_type} **Category:** {category} **Power:** {power if power > 0 else '-'} **Accuracy:** {accuracy_str} **PP:** {pp}{priority_str}{secondary_str}{self_str}{drain_str} ### Description {desc} """ return response def format_ability_response(ability: dict) -> str: """Format ability data into a readable response.""" name = ability.get("name", "Unknown") desc = ability.get("desc", ability.get("shortDesc", "No description.")) short_desc = ability.get("shortDesc", "") response = f"""## {name} ### Effect {desc} """ if short_desc and short_desc != desc: response += f"\n### Summary\n{short_desc}\n" return response def format_item_response(item: dict) -> str: """Format item data into a readable response.""" name = item.get("name", "Unknown") desc = item.get("desc", item.get("shortDesc", "No description.")) short_desc = item.get("shortDesc", "") response = f"""## {name} ### Effect {desc} """ if short_desc and short_desc != desc: response += f"\n### Summary\n{short_desc}\n" return response @server.list_tools() async def list_tools() -> list[Tool]: """List available Pokemon data tools.""" return [ Tool( name="get_pokemon", description="Look up a Pokemon by name. Returns base stats, types, abilities with descriptions, weight, and competitive tier.", inputSchema={ "type": "object", "properties": { "name": { "type": "string", "description": "Pokemon name (e.g., 'pikachu', 'slaking', 'charizard')" } }, "required": ["name"] } ), Tool( name="get_move", description="Look up a move by name. Returns power, accuracy, type, category, priority, effects, and full description.", inputSchema={ "type": "object", "properties": { "name": { "type": "string", "description": "Move name (e.g., 'thunderbolt', 'earthquake', 'swords-dance')" } }, "required": ["name"] } ), Tool( name="get_ability", description="Look up an ability by name. Returns full description of what the ability does in battle.", inputSchema={ "type": "object", "properties": { "name": { "type": "string", "description": "Ability name (e.g., 'truant', 'intimidate', 'levitate')" } }, "required": ["name"] } ), Tool( name="get_item", description="Look up a held item by name. Returns full description of what the item does in battle.", inputSchema={ "type": "object", "properties": { "name": { "type": "string", "description": "Item name (e.g., 'choice-scarf', 'leftovers', 'life-orb')" } }, "required": ["name"] } ), Tool( name="get_type_effectiveness", description="Calculate type effectiveness multiplier for an attack against a Pokemon's types.", inputSchema={ "type": "object", "properties": { "attack_type": { "type": "string", "description": "The attacking move's type (e.g., 'electric', 'fire')" }, "defend_types": { "type": "array", "items": {"type": "string"}, "description": "List of the defending Pokemon's types (e.g., ['water', 'flying'])" } }, "required": ["attack_type", "defend_types"] } ), Tool( name="search_priority_moves", description="Find all moves with priority (moves that go before normal moves). Useful for finding options when you need to outspeed an opponent.", inputSchema={ "type": "object", "properties": { "min_priority": { "type": "integer", "description": "Minimum priority value (default 1)", "default": 1 } } } ), Tool( name="search_pokemon_by_ability", description="Find all Pokemon that can have a specific ability.", inputSchema={ "type": "object", "properties": { "ability": { "type": "string", "description": "Ability name to search for" } }, "required": ["ability"] } ), Tool( name="list_dangerous_abilities", description="List abilities that can significantly affect battle outcomes (immunities, damage reduction, status reflection, etc.)", inputSchema={ "type": "object", "properties": { "category": { "type": "string", "description": "Category of abilities: 'immunity' (type immunities), 'defense' (damage reduction), 'reflect' (status reflection), 'offense' (damage boosts), 'priority' (move order), 'all' (default)", "default": "all" } } } ), ] @server.call_tool() async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """Handle tool calls.""" loader = get_loader() if name == "get_pokemon": pokemon = loader.get_pokemon(arguments["name"]) if pokemon: return [TextContent(type="text", text=format_pokemon_response(pokemon))] return [TextContent(type="text", text=f"Pokemon '{arguments['name']}' not found.")] elif name == "get_move": move = loader.get_move(arguments["name"]) if move: return [TextContent(type="text", text=format_move_response(move))] return [TextContent(type="text", text=f"Move '{arguments['name']}' not found.")] elif name == "get_ability": ability = loader.get_ability(arguments["name"]) if ability: return [TextContent(type="text", text=format_ability_response(ability))] return [TextContent(type="text", text=f"Ability '{arguments['name']}' not found.")] elif name == "get_item": item = loader.get_item(arguments["name"]) if item: return [TextContent(type="text", text=format_item_response(item))] return [TextContent(type="text", text=f"Item '{arguments['name']}' not found.")] elif name == "get_type_effectiveness": attack_type = arguments["attack_type"] defend_types = arguments["defend_types"] multiplier = loader.get_type_effectiveness(attack_type, defend_types) # Describe the effectiveness if multiplier == 0: desc = "No effect (immune)" elif multiplier == 0.25: desc = "Not very effective (0.25x)" elif multiplier == 0.5: desc = "Not very effective (0.5x)" elif multiplier == 1: desc = "Normal effectiveness (1x)" elif multiplier == 2: desc = "Super effective (2x)" elif multiplier == 4: desc = "Super effective (4x)" else: desc = f"{multiplier}x" response = f"""## Type Effectiveness **{attack_type.capitalize()}** vs **{'/'.join(t.capitalize() for t in defend_types)}** **Multiplier:** {multiplier}x **Result:** {desc} """ return [TextContent(type="text", text=response)] elif name == "search_priority_moves": min_priority = arguments.get("min_priority", 1) moves = loader.search_moves_by_priority(min_priority) if not moves: return [TextContent(type="text", text="No priority moves found.")] # Sort by priority descending moves.sort(key=lambda m: m.get("priority", 0), reverse=True) lines = [f"## Priority Moves (priority >= {min_priority})\n"] for move in moves[:30]: # Limit to 30 priority = move.get("priority", 0) power = move.get("basePower", 0) move_type = move.get("type", "") name = move.get("name", move.get("id", "")) lines.append(f"- **{name}** (+{priority}): {move_type}, {power} power") return [TextContent(type="text", text="\n".join(lines))] elif name == "search_pokemon_by_ability": ability = arguments["ability"] pokemon_list = loader.get_pokemon_with_ability(ability) if not pokemon_list: return [TextContent(type="text", text=f"No Pokemon found with ability '{ability}'.")] response = f"""## Pokemon with {ability.title()} Found {len(pokemon_list)} Pokemon: {', '.join(sorted(pokemon_list)[:50])} """ if len(pokemon_list) > 50: response += f"\n... and {len(pokemon_list) - 50} more." return [TextContent(type="text", text=response)] elif name == "list_dangerous_abilities": category = arguments.get("category", "all").lower() DANGEROUS_ABILITIES = { "immunity": { "Levitate": "Immune to Ground moves", "Flash Fire": "Immune to Fire moves, boosts own Fire attacks", "Volt Absorb": "Immune to Electric, heals instead", "Water Absorb": "Immune to Water, heals instead", "Dry Skin": "Immune to Water (heals), weak to Fire", "Lightning Rod": "Immune to Electric, boosts Sp.Atk", "Motor Drive": "Immune to Electric, boosts Speed", "Storm Drain": "Immune to Water, boosts Sp.Atk", "Sap Sipper": "Immune to Grass, boosts Attack", "Earth Eater": "Immune to Ground, heals instead", "Wonder Guard": "Only super effective moves deal damage!", }, "defense": { "Fur Coat": "Doubles Defense (halves physical damage)", "Ice Scales": "Halves Special damage", "Fluffy": "Halves contact damage (but 2x Fire damage)", "Multiscale": "Halves damage at full HP", "Shadow Shield": "Halves damage at full HP", "Sturdy": "Survives any hit at full HP with 1 HP", "Filter": "Reduces super effective damage by 25%", "Solid Rock": "Reduces super effective damage by 25%", "Prism Armor": "Reduces super effective damage by 25%", "Thick Fat": "Halves Fire and Ice damage", "Heatproof": "Halves Fire damage", "Water Bubble": "Halves Fire damage, doubles Water attacks", "Unaware": "Ignores opponent's stat boosts", "Marvel Scale": "1.5x Defense when statused", }, "reflect": { "Magic Bounce": "Reflects status moves (Stealth Rock, Thunder Wave, etc.)", }, "offense": { "Huge Power": "Doubles Attack stat!", "Pure Power": "Doubles Attack stat!", "Adaptability": "STAB becomes 2x instead of 1.5x", "Technician": "1.5x boost to moves with 60 BP or less", "Tinted Lens": "Doubles 'not very effective' damage", "Protean": "Changes type to match used move (always STAB)", "Libero": "Changes type to match used move (always STAB)", }, "priority": { "Prankster": "+1 priority to status moves", "Gale Wings": "+1 priority to Flying moves at full HP", }, "contact": { "Rough Skin": "1/8 damage to attacker on contact", "Iron Barbs": "1/8 damage to attacker on contact", "Flame Body": "30% chance to burn on contact", "Static": "30% chance to paralyze on contact", "Poison Point": "30% chance to poison on contact", } } lines = ["## Dangerous Abilities\n"] categories_to_show = [category] if category != "all" else list(DANGEROUS_ABILITIES.keys()) for cat in categories_to_show: if cat in DANGEROUS_ABILITIES: lines.append(f"### {cat.title()}\n") for ability_name, desc in DANGEROUS_ABILITIES[cat].items(): lines.append(f"- **{ability_name}**: {desc}") lines.append("") if len(lines) == 1: return [TextContent(type="text", text=f"Unknown category: {category}. Use 'immunity', 'defense', 'reflect', 'offense', 'priority', 'contact', or 'all'.")] return [TextContent(type="text", text="\n".join(lines))] return [TextContent(type="text", text=f"Unknown tool: {name}")] async def _async_main(): """Async entry point for the MCP server.""" async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) def main(): """Run the MCP server (synchronous entry point).""" import asyncio asyncio.run(_async_main()) if __name__ == "__main__": main()

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/drewsungg/mcpkmn-showdown'

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