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).');