Skip to main content
Glama
index.ts10.6 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import path from 'path'; import { fileURLToPath } from 'url'; import { pokeapi } from './pokeapi.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { natureMultiplier } from './stats.js'; // stdio transport fallback (covers different paths within SDK 0.6.x) let StdioServerTransport: any; try { ({ StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js')); } catch { const alt = '@modelcontextprotocol/sdk/' + 'transports/stdio.js'; // @ts-ignore - alt path only exists on some SDK versions ({ StdioServerTransport } = await import(alt as any)); } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Server (SDK 0.6 needs capabilities) const server = new Server({ name: 'pokemon-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } }); type ToolDef = { name: string; description: string; inputSchema: any; handler: (args:any)=>Promise<any> }; const registry: Record<string, ToolDef> = {}; function registerTool(name:string, description:string, inputSchema:any, handler:(args:any)=>Promise<any>) { registry[name] = { name, description, inputSchema, handler }; } const jsonContent = (obj:any)=>({ content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] }); // tools/list server.setRequestHandler(ListToolsRequestSchema as any, async () => ({ tools: Object.values(registry).map(t => ({ name:t.name, description:t.description, inputSchema:t.inputSchema })) })); // tools/call server.setRequestHandler(CallToolRequestSchema as any, async (req:any) => { const { name, arguments: args } = req.params ?? {}; const tool = registry[name]; if (!tool) return { isError: true, content: [{ type: 'text', text: `Tool not found: ${name}` }] }; try { const res = await tool.handler(args ?? {}); return res && res.content ? res : { content: [{ type: 'json', json: res }] }; } catch (e:any) { return { isError: true, content: [{ type: 'text', text: e?.message || String(e) }] }; } }); // ===== Tools ===== registerTool('search_names', 'Búsqueda en PokeAPI de Pokémon y movimientos (substring).', { type:'object', properties:{ query:{ type:'string' } }, required:['query'] }, async (args:any) => { const q = String(args.query || ''); return jsonContent(await pokeapi.searchNames(q)); }); registerTool('get_pokemon', 'Ficha con tipos, base stats y habilidades (PokeAPI).', { type:'object', properties:{ name:{type:'string'} }, required:['name'] }, async (args:any) => { const name = String(args.name); const core = await pokeapi.pokemonCore(name); return jsonContent(core); }); registerTool('get_learnset', 'Learnset por version_group y método.', { type:'object', properties:{ name:{type:'string'}, version_group:{type:'string', default:'emerald'}, method:{type:'string', default:'all'} }, required:['name'] }, async (args:any) => { const name = String(args.name), vg = String(args.version_group || 'emerald'), method = String(args.method || 'all'); const data = await pokeapi.learnset(name, vg); const filtered = data.filter(e => method==='all' || e.methods.some(m => m.method===method)); return jsonContent({ name, version_group: vg, method, moves: filtered }); }); registerTool('get_evolutions', 'Cadena/grafo de evoluciones desde species->evolution_chain.', { type:'object', properties:{ name:{type:'string'} }, required:['name'] }, async (args:any) => { const name = String(args.name); const chain = await pokeapi.getEvolutionChainBySpeciesName(name); return jsonContent({ name, edges: pokeapi.parseEvolutionChain(chain) }); }); registerTool('type_matchup', 'Multiplicador de daño ofensivo usando PokeAPI/type.', { type:'object', properties:{ attacking:{type:'string'}, defending:{type:'array', items:{type:'string'}, minItems:1, maxItems:2} }, required:['attacking','defending'] }, async (args:any) => { const atk = String(args.attacking), def = (args.defending as string[]); const mult = await pokeapi.typeMultiplier(atk, def); return jsonContent({ attacking: atk, defending: def, multiplier: mult }); }); registerTool('calc_stats', 'Cálculo de stats finales (nivel, IVs, EVs, naturaleza).', { type:'object', properties:{ name:{type:'string'}, level:{type:'integer', default:50}, ivs:{type:'object', default:{}, additionalProperties:{ type:'integer' }}, evs:{type:'object', default:{}, additionalProperties:{ type:'integer' }}, nature:{type:'string', default:'serious'} }, required:['name'] }, async (args:any) => { const core = await pokeapi.pokemonCore(String(args.name)); const level = Number(args.level || 50); const ivs = Object.assign({hp:31,atk:31,def:31,spa:31,spd:31,spe:31}, args.ivs || {}); const evs = Object.assign({hp:0,atk:0,def:0,spa:0,spd:0,spe:0}, args.evs || {}); const nature = String(args.nature || 'serious'); const bs:any = core.base_stats; const stat=(base:number,iv:number,ev:number, mult:number, isHP=false)=>{ if (isHP) return Math.floor(((2*base + iv + Math.floor(ev/4))*level)/100)+level+10; const val = Math.floor(((2*base + iv + Math.floor(ev/4))*level)/100)+5; return Math.floor(val * mult); }; return jsonContent({ name: core.name, level, nature, stats: { hp: stat(bs.hp,ivs.hp,evs.hp,1,true), atk: stat(bs.atk,ivs.atk,evs.atk,natureMultiplier(nature,'atk')), def: stat(bs.def,ivs.def,evs.def,natureMultiplier(nature,'def')), spa: stat(bs.spa,ivs.spa,evs.spa,natureMultiplier(nature,'spa')), spd: stat(bs.spd,ivs.spd,evs.spd,natureMultiplier(nature,'spd')), spe: stat(bs.spe,ivs.spe,evs.spe,natureMultiplier(nature,'spe')) } }); }); registerTool('validate_moveset', 'Valida un moveset contra PokeAPI por version_group.', { type:'object', properties:{ name:{type:'string'}, version_group:{type:'string', default:'emerald'}, moves:{type:'array', items:{type:'string'}, minItems:1, maxItems:4} }, required:['name','moves'] }, async (args:any) => { const name = String(args.name), vg = String(args.version_group || 'emerald'); const moves = (args.moves as string[]).map(String); const ls = await pokeapi.learnset(name, vg); const index = new Map(ls.map(e => [e.move.toLowerCase(), e.methods])); const details:any[] = []; const issues:any[] = []; for (const mv of moves) { const item = index.get(mv.toLowerCase()); if (!item) issues.push({ code:'illegal-move', move: mv, detail: 'not-in-learnset' }); else details.push({ move: mv, methods: item }); } return jsonContent({ ok: issues.length===0, issues, learned: details }); }); registerTool('suggest_moveset', 'Sugiere moveset (STAB + cobertura) usando tipos de cada movimiento.', { type:'object', properties:{ name:{type:'string'}, version_group:{type:'string', default:'emerald'}, role:{type:'string', enum:['sweeper','bulky','support'], default:'sweeper'} }, required:['name'] }, async (args:any) => { const name = String(args.name), vg = String(args.version_group || 'emerald'), role = String(args.role || 'sweeper'); const core = await pokeapi.pokemonCore(name); const ls = await pokeapi.learnset(name, vg); const have = (m:string)=> ls.some(e => e.move.toLowerCase()===m.toLowerCase()); const moves = ls.map(e => e.move); // get move types lazily const typeOf = async (m:string) => (await pokeapi.getMove(m)).type.name as string; const damageClassOf = async (m:string) => (await pokeapi.getMove(m)).damage_class.name as string; // Build STAB and coverage pools const stab = []; for (const m of moves) { try { const t = await typeOf(m); if (core.types.includes(t)) stab.push(m); } catch {} } // Common strong coverage list to prioritize if present in learnset const coveragePreferred = ['earthquake','rock-slide','ice-beam','thunderbolt','flamethrower','psychic','shadow-ball','brick-break','aerial-ace','giga-drain','crunch','ice-punch','fire-punch','thunder-punch','dragon-claw','overheat','hydro-pump','surf']; const coverage = []; for (const m of moves) { const nn = m.toLowerCase(); if (coveragePreferred.includes(nn)) coverage.push(m); } const utilityPreferred = ['swords-dance','dragon-dance','will-o-wisp','thunder-wave','substitute','protect','rapid-spin','spikes','sandstorm']; const utility = []; for (const m of moves) { const nn = m.toLowerCase(); if (utilityPreferred.includes(nn)) utility.push(m); } const pick = (arr:string[], n:number) => Array.from(new Set(arr)).slice(0, n); let chosen:string[] = []; if (role === 'sweeper') chosen = [...pick(stab,2), ...pick(coverage,2)]; else if (role === 'bulky') chosen = [...pick(stab,1), ...pick(utility,2), ...pick(coverage,1)]; else chosen = [...pick(utility,2), ...pick(stab,1), ...pick(coverage,1)]; // Attach minimal legality/method info const index = new Map(ls.map(e => [e.move.toLowerCase(), e.methods])); const legality = await Promise.all(chosen.map(async m => ({ move: m, type: await typeOf(m), damage_class: await damageClassOf(m), methods: index.get(m.toLowerCase()) || [] }))); return jsonContent({ name: core.name, role, version_group: vg, moveset: chosen, details: legality }); }); registerTool('team_analysis', 'Sinergias defensivas del equipo (usa tipos de PokeAPI y tabla de tipos).', { type:'object', properties:{ team:{ type:'array', items:{ type:'object', properties:{ name:{type:'string'} }, required:['name'] }, minItems:1, maxItems:6 } }, required:['team'] }, async (args:any) => { const team = args.team as { name:string }[]; const roster = await Promise.all(team.map(async m => { const core = await pokeapi.pokemonCore(m.name); return { name: core.name, types: core.types }; })); const allTypes = ['normal','fire','water','electric','grass','ice','fighting','poison','ground','flying','psychic','bug','rock','ghost','dragon','dark','steel','fairy']; const totals: Record<string,{weak:number,resist:number,immune:number}> = Object.fromEntries(allTypes.map(t => [t,{weak:0,resist:0,immune:0}])) as any; for (const r of roster) { for (const atk of allTypes) { const mult = await pokeapi.typeMultiplier(atk, r.types); if (mult === 0) totals[atk].immune++; else if (mult > 1) totals[atk].weak++; else if (mult < 1) totals[atk].resist++; } } return jsonContent({ roster, totals }); }); // Connect const transport = new StdioServerTransport(); server.connect(transport); console.error('[pokemon-mcp-server] Ready on stdio (PokeAPI live).');

Implementation Reference

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/EscasanN/MCP_Pokemon'

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