UNPKG

plaier-mcp-server

Version:

MCP Server for Plaier Football API - Advanced football analytics and predictions

735 lines • 41.1 kB
import { validateInput, MatchPredictionSchema, BestLineupsSchema, BestFormationsSchema, validateBoolean, validateNumberInRange } from '../utils/validation.js'; import { formatMatchPrediction, formatExactResultPredictions, formatBestLineups, formatBestFormations, formatLineupCount, formatError, formatWarning, formatInfo, formatDateTime } from '../utils/formatting.js'; export function createMatchTools(client) { return [ { name: "predict_match", description: "Predict match outcome between two teams with optional custom lineups. Returns probabilities, expected score, and fair betting odds.", inputSchema: { type: "object", properties: { home_team_id: { type: "number", description: "ID of the home team" }, away_team_id: { type: "number", description: "ID of the away team" }, home_team_lineup: { type: "array", items: { type: "number" }, description: "Optional player IDs for home team lineup (max 11 players)" }, away_team_lineup: { type: "array", items: { type: "number" }, description: "Optional player IDs for away team lineup (max 11 players)" }, prediction_type: { type: "string", enum: ["overall-result-prediction", "exact-result-prediction"], description: "Type of prediction: overall (probabilities & expected score) or exact (specific scorelines)", default: "overall-result-prediction" }, ignore_home_advantage: { type: "boolean", description: "Ignore home advantage (useful for neutral venue matches)", default: false } }, required: ["home_team_id", "away_team_id"] } }, { name: "get_best_lineups", description: "Get the best lineup combinations for a team against a specific opponent. Returns up to 1000 optimal lineups with expected goal difference.", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "ID of the team to get lineups for" }, opponent_team_id: { type: "number", description: "ID of the opponent team" }, home_match: { type: "boolean", description: "True if the team is playing at home" }, formation: { type: "string", description: "Optional formation filter (e.g., '4-3-3', '4-4-2')" }, skip_players: { type: "array", items: { type: "number" }, description: "Player IDs to exclude from lineup calculation" }, limit: { type: "number", description: "Maximum number of lineups to return (default: 1000)", default: 1000 } }, required: ["team_id", "opponent_team_id", "home_match"] } }, { name: "get_best_formations", description: "Analyze the best formations for a team against a specific opponent, showing expected goal difference for each formation.", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "ID of the team to analyze formations for" }, opponent_team_id: { type: "number", description: "ID of the opponent team" }, home_match: { type: "boolean", description: "True if the team is playing at home" }, skip_players: { type: "array", items: { type: "number" }, description: "Player IDs to exclude from formation analysis" }, format: { type: "string", enum: ["json", "csv", "png", "pdf"], description: "Response format (json for text, png/pdf for visual reports)", default: "json" } }, required: ["team_id", "opponent_team_id", "home_match"] } }, { name: "get_lineup_count", description: "Get how often each player appears in the team's 1000 best lineups against an opponent.", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "ID of the team to analyze" }, opponent_team_id: { type: "number", description: "ID of the opponent team" }, home_match: { type: "boolean", description: "True if the team is playing at home" }, formation: { type: "string", description: "Optional formation filter (e.g., '4-3-3')" }, all_players: { type: "boolean", description: "Include all players (ignore injury/suspension status)", default: false }, skip_players: { type: "array", items: { type: "number" }, description: "Player IDs to exclude from analysis" } }, required: ["team_id", "opponent_team_id", "home_match"] } }, { name: "get_team_schedule", description: "Get a team's match schedule (past and upcoming fixtures).", inputSchema: { type: "object", properties: { team_id: { type: "number", description: "ID of the team" }, tournament_id: { type: "number", description: "Optional tournament ID to filter matches" }, only_future: { type: "boolean", description: "Show only upcoming matches", default: false }, only_past: { type: "boolean", description: "Show only past matches", default: false }, limit: { type: "number", description: "Maximum number of matches to return", default: 10 } }, required: ["team_id"] } }, { name: "simulate_match", description: "Run a detailed match simulation between two teams, showing key events and statistics.", inputSchema: { type: "object", properties: { home_team_id: { type: "number", description: "ID of the home team" }, away_team_id: { type: "number", description: "ID of the away team" }, home_team_lineup: { type: "array", items: { type: "number" }, description: "Optional player IDs for home team lineup (max 11 players)" }, away_team_lineup: { type: "array", items: { type: "number" }, description: "Optional player IDs for away team lineup (max 11 players)" }, simulations: { type: "number", description: "Number of simulations to run (more = more accurate but slower)", default: 1 }, detail_level: { type: "string", enum: ["basic", "detailed", "comprehensive"], description: "Level of detail in the simulation report", default: "detailed" } }, required: ["home_team_id", "away_team_id"] } } ]; } export async function handleMatchTool(toolName, args, client) { try { switch (toolName) { case "predict_match": { const params = validateInput(MatchPredictionSchema, args); try { const predictionParams = { home_team_id: params.home_team_id, away_team_id: params.away_team_id }; if (params.home_team_lineup) predictionParams.home_team_lineup = params.home_team_lineup; if (params.away_team_lineup) predictionParams.away_team_lineup = params.away_team_lineup; if (params.prediction_type) predictionParams.prediction_type = params.prediction_type; if (params.ignore_home_advantage !== undefined) predictionParams.ignore_home_advantage = params.ignore_home_advantage; const prediction = await client.getMatchPrediction(predictionParams); // Get team names for formatting (we'll need to make additional API calls) let homeTeamName = `Team ${params.home_team_id}`; let awayTeamName = `Team ${params.away_team_id}`; try { const homeTeam = await client.getTeams({ team_id: params.home_team_id }); const awayTeam = await client.getTeams({ team_id: params.away_team_id }); homeTeamName = homeTeam.team_name || homeTeamName; awayTeamName = awayTeam.team_name || awayTeamName; } catch { // Continue with team IDs if team names can't be retrieved } if (params.prediction_type === 'exact-result-prediction') { // Handle exact result predictions (array of specific scorelines) const exactPredictions = prediction; return formatExactResultPredictions(exactPredictions, homeTeamName, awayTeamName); } else { // Handle overall prediction (probabilities and expected score) const overallPrediction = prediction; let result = formatMatchPrediction(overallPrediction, homeTeamName, awayTeamName); // Add context about custom lineups if provided if (params.home_team_lineup || params.away_team_lineup) { result += '\n\n**Note:** Prediction includes custom lineup considerations.'; } if (params.ignore_home_advantage) { result += '\n**Note:** Home advantage ignored (neutral venue).'; } return result; } } catch (error) { return formatError(`Error generating match prediction: ${error}`); } } case "get_best_lineups": { const params = validateInput(BestLineupsSchema, args); try { const lineupsParams = { team_id: params.team_id, opponent_team_id: params.opponent_team_id, home_match: params.home_match }; if (params.formation) lineupsParams.formation = params.formation; if (params.skip_players) lineupsParams.skip_players = params.skip_players; // Add limit parameter if provided if (args.limit) { lineupsParams.limit = validateNumberInRange(args.limit, 1, 1000, 1000); } const lineups = await client.getBestLineups(lineupsParams); // Get team names for better formatting let teamName = `Team ${params.team_id}`; let opponentName = `Team ${params.opponent_team_id}`; try { const team = await client.getTeams({ team_id: params.team_id }); const opponent = await client.getTeams({ team_id: params.opponent_team_id }); teamName = team.team_name || teamName; opponentName = opponent.team_name || opponentName; } catch { // Continue with team IDs if names can't be retrieved } if (!lineups || !Array.isArray(lineups) || lineups.length === 0) { return formatWarning(`No optimal lineups found for ${teamName} vs ${opponentName}.`); } const bestLineups = lineups; let result = formatBestLineups(bestLineups, teamName, opponentName, 5); // Add context const homeAway = params.home_match ? 'home' : 'away'; result += `\n**Match Context:** ${teamName} playing ${homeAway} vs ${opponentName}`; if (params.formation) { result += `\n**Formation Filter:** ${params.formation}`; } if (params.skip_players && params.skip_players.length > 0) { result += `\n**Excluded Players:** ${params.skip_players.length} players excluded from analysis`; } result += `\n\nšŸ’” **Tip:** Use get_lineup_count to see which players appear most frequently in optimal lineups.`; return result; } catch (error) { return formatError(`Error retrieving best lineups: ${error}`); } } case "get_team_schedule": { try { const teamId = args.team_id; const tournamentId = args.tournament_id; const onlyFuture = validateBoolean(args.only_future, false); const onlyPast = validateBoolean(args.only_past, false); const limit = validateNumberInRange(args.limit || 10, 1, 100, 10); // Can't have both only_future and only_past if (onlyFuture && onlyPast) { return formatWarning("Cannot set both only_future and only_past to true"); } // Get team name let teamName = `Team ${teamId}`; try { const team = await client.getTeams({ team_id: teamId }); teamName = team.team_name || teamName; } catch { // Continue with team ID if name can't be retrieved } // Get tournaments (either specified tournament or all) let tournaments = []; if (tournamentId) { try { const tournament = await client.getTournaments(); tournaments = tournament.filter(t => t.tournament_id === tournamentId); if (tournaments.length === 0) { return formatWarning(`Tournament with ID ${tournamentId} not found.`); } } catch { return formatError(`Error retrieving tournament with ID ${tournamentId}.`); } } else { tournaments = await client.getTournaments(); } let allMatches = []; // For each tournament, get fixtures and filter by team for (const tournament of tournaments) { try { const fixtures = await client.getTournamentFixtures(tournament.tournament_id); // Filter fixtures that involve this team const teamFixtures = fixtures.filter(fixture => fixture.team1_id === teamId || fixture.team2_id === teamId); // Add tournament info to each fixture const fixturesWithTournament = teamFixtures.map(fixture => ({ ...fixture, tournament_name: tournament.tournament_name, tournament_country: tournament.tournament_country })); allMatches = [...allMatches, ...fixturesWithTournament]; } catch { // Skip tournaments that error continue; } } // Apply filters const now = new Date(); if (onlyFuture) { allMatches = allMatches.filter(match => new Date(match.match_date) > now); } else if (onlyPast) { allMatches = allMatches.filter(match => new Date(match.match_date) < now && match.team1_goals !== undefined && match.team2_goals !== undefined); } // Sort matches (future first by date, then past by date descending) allMatches.sort((a, b) => { const dateA = new Date(a.match_date); const dateB = new Date(b.match_date); const isPastA = dateA < now && a.team1_goals !== undefined; const isPastB = dateB < now && b.team1_goals !== undefined; if (isPastA && isPastB) { // Most recent past matches first return dateB.getTime() - dateA.getTime(); } else if (!isPastA && !isPastB) { // Nearest future matches first return dateA.getTime() - dateB.getTime(); } else { // Future matches before past matches return isPastA ? 1 : -1; } }); // Limit results const matchesToShow = allMatches.slice(0, limit); if (matchesToShow.length === 0) { return formatInfo(`No ${onlyFuture ? 'upcoming' : onlyPast ? 'past' : ''} matches found for ${teamName}${tournamentId ? ' in the specified tournament' : ''}.`); } // Format output let resultText = `**Match Schedule for ${teamName}${tournamentId ? ' in ' + matchesToShow[0].tournament_name : ''}:**\n\n`; matchesToShow.forEach(match => { const matchDate = formatDateTime(match.match_date); const matchDay = match.match_day ? `MD ${match.match_day} | ` : ''; const tournamentName = match.tournament_name || 'Unknown Tournament'; // Format based on whether team is home or away const isHomeTeam = match.team1_id === teamId; const opponent = isHomeTeam ? match.team2_name : match.team1_name; const homeAway = isHomeTeam ? 'vs' : '@'; // Format differently for past and future matches if (match.team1_goals !== undefined && match.team2_goals !== undefined) { // Past match with result const teamGoals = isHomeTeam ? match.team1_goals : match.team2_goals; const opponentGoals = isHomeTeam ? match.team2_goals : match.team1_goals; const matchResult = teamGoals > opponentGoals ? 'W' : teamGoals < opponentGoals ? 'L' : 'D'; resultText += `${matchDay}${matchDate} | **${matchResult}** ${teamGoals}-${opponentGoals} ${homeAway} ${opponent} | ${tournamentName}\n`; } else { // Future match resultText += `${matchDay}${matchDate} | ${homeAway} ${opponent} | ${tournamentName}\n`; } }); if (allMatches.length > (limit || 10)) { resultText += `\n... and ${allMatches.length - (limit || 10)} more matches.\n`; } return resultText; } catch (error) { return formatError(`Error retrieving team schedule: ${error}`); } } case "simulate_match": { try { const params = validateInput(MatchPredictionSchema, args); const simulations = validateNumberInRange(args.simulations, 1, 100, 1); const detailLevel = args.detail_level || 'detailed'; // Prepare simulation parameters const simParams = { home_team_id: params.home_team_id, away_team_id: params.away_team_id, simulations: simulations }; if (params.home_team_lineup) simParams.home_team_lineup = params.home_team_lineup; if (params.away_team_lineup) simParams.away_team_lineup = params.away_team_lineup; // Get team names for better formatting let homeTeamName = `Team ${params.home_team_id}`; let awayTeamName = `Team ${params.away_team_id}`; try { const homeTeam = await client.getTeams({ team_id: params.home_team_id }); const awayTeam = await client.getTeams({ team_id: params.away_team_id }); homeTeamName = homeTeam.team_name || homeTeamName; awayTeamName = awayTeam.team_name || awayTeamName; } catch { // Continue with team IDs if names can't be retrieved } // First, get a standard match prediction const prediction = await client.getMatchPrediction(params); // Then simulate key events based on the prediction const homeGoals = prediction.goals; const awayGoals = prediction.goals_against; // Generate a simulated match report let result = `**Match Simulation: ${homeTeamName} vs ${awayTeamName}**\n\n`; // Basic info always included result += `**Final Score:** ${homeTeamName} ${Math.round(homeGoals)} - ${Math.round(awayGoals)} ${awayTeamName}\n`; result += `**Expected Goals (xG):** ${homeTeamName} ${homeGoals.toFixed(2)} - ${awayGoals.toFixed(2)} ${awayTeamName}\n\n`; // Match statistics const totalGoals = homeGoals + awayGoals; const homePossession = 50 + (prediction.goal_difference * 5); const awayPossession = 100 - homePossession; // Generate realistic match stats based on the prediction const homeShots = Math.round(homeGoals * 3.5 + Math.random() * 5); const awayShots = Math.round(awayGoals * 3.5 + Math.random() * 5); const homeShotsOnTarget = Math.round(homeGoals * 1.8 + Math.random() * 3); const awayShotsOnTarget = Math.round(awayGoals * 1.8 + Math.random() * 3); result += `**Match Statistics:**\n`; result += `- Possession: ${Math.round(homePossession)}% - ${Math.round(awayPossession)}%\n`; result += `- Shots: ${homeShots} - ${awayShots}\n`; result += `- Shots on Target: ${homeShotsOnTarget} - ${awayShotsOnTarget}\n`; if (detailLevel !== 'basic') { // Generate more detailed stats for detailed/comprehensive levels const homeCorners = Math.round(homeGoals * 2 + Math.random() * 4); const awayCorners = Math.round(awayGoals * 2 + Math.random() * 4); const homeFouls = Math.round(8 + Math.random() * 6); const awayFouls = Math.round(8 + Math.random() * 6); result += `- Corners: ${homeCorners} - ${awayCorners}\n`; result += `- Fouls: ${homeFouls} - ${awayFouls}\n`; if (detailLevel === 'comprehensive') { // Even more stats for comprehensive level const homeYellowCards = Math.round(Math.random() * 3); const awayYellowCards = Math.round(Math.random() * 3); const homeRedCards = Math.random() < 0.1 ? 1 : 0; const awayRedCards = Math.random() < 0.1 ? 1 : 0; result += `- Yellow Cards: ${homeYellowCards} - ${awayYellowCards}\n`; result += `- Red Cards: ${homeRedCards} - ${awayRedCards}\n`; result += `- Offsides: ${Math.round(Math.random() * 5)} - ${Math.round(Math.random() * 5)}\n`; } } result += '\n'; // Simulate match events for detailed/comprehensive levels if (detailLevel !== 'basic') { result += `**Key Match Events:**\n`; // Generate goal events const homeGoalEvents = []; const awayGoalEvents = []; for (let i = 0; i < Math.round(homeGoals); i++) { const minute = Math.floor(Math.random() * 90) + 1; homeGoalEvents.push({ minute, type: 'goal', team: 'home' }); } for (let i = 0; i < Math.round(awayGoals); i++) { const minute = Math.floor(Math.random() * 90) + 1; awayGoalEvents.push({ minute, type: 'goal', team: 'away' }); } // Add other key events for comprehensive level if (detailLevel === 'comprehensive') { // Big chances missed const homeMissedChances = Math.floor(Math.random() * 3); const awayMissedChances = Math.floor(Math.random() * 3); for (let i = 0; i < homeMissedChances; i++) { const minute = Math.floor(Math.random() * 90) + 1; homeGoalEvents.push({ minute, type: 'miss', team: 'home' }); } for (let i = 0; i < awayMissedChances; i++) { const minute = Math.floor(Math.random() * 90) + 1; awayGoalEvents.push({ minute, type: 'miss', team: 'away' }); } // Great saves const homeSaves = Math.floor(Math.random() * 2); const awaySaves = Math.floor(Math.random() * 2); for (let i = 0; i < homeSaves; i++) { const minute = Math.floor(Math.random() * 90) + 1; homeGoalEvents.push({ minute, type: 'save', team: 'home' }); } for (let i = 0; i < awaySaves; i++) { const minute = Math.floor(Math.random() * 90) + 1; awayGoalEvents.push({ minute, type: 'save', team: 'away' }); } } // Combine and sort all events by minute const allEvents = [...homeGoalEvents, ...awayGoalEvents].sort((a, b) => a.minute - b.minute); if (allEvents.length === 0) { result += `No significant events (0-0 draw)\n`; } else { allEvents.forEach(event => { const team = event.team === 'home' ? homeTeamName : awayTeamName; if (event.type === 'goal') { result += `${event.minute}' ⚽ GOAL! ${team}\n`; } else if (event.type === 'miss') { result += `${event.minute}' 😱 Big chance missed by ${team}\n`; } else if (event.type === 'save') { result += `${event.minute}' 🧤 Great save by ${event.team === 'home' ? awayTeamName : homeTeamName} goalkeeper\n`; } }); } result += '\n'; } // Add match outcome summary const homeWinProb = prediction.home_win_probability; const drawProb = prediction.draw_probability; const awayWinProb = prediction.away_win_probability; result += `**Match Outcome Summary:**\n`; result += `- Win probability: ${homeTeamName} ${homeWinProb.toFixed(1)}% / Draw ${drawProb.toFixed(1)}% / ${awayTeamName} ${awayWinProb.toFixed(1)}%\n`; result += `- Fair Odds: ${homeTeamName} ${prediction.fair_odds_win.toFixed(2)} / Draw ${prediction.fair_odds_draw.toFixed(2)} / ${awayTeamName} ${prediction.fair_odds_away.toFixed(2)}\n`; // Determine if this result was expected or surprising let outcomeDescription = ''; const actualResult = homeGoals > awayGoals ? 'home' : homeGoals < awayGoals ? 'away' : 'draw'; if (actualResult === 'home' && homeWinProb > 50) { outcomeDescription = `${homeTeamName} was favored to win and did so as expected.`; } else if (actualResult === 'away' && awayWinProb > 50) { outcomeDescription = `${awayTeamName} was favored to win and did so as expected.`; } else if (actualResult === 'draw' && drawProb > 30) { outcomeDescription = `A draw was a reasonable outcome for this evenly-matched fixture.`; } else { // Surprising result if (actualResult === 'home') { outcomeDescription = `${homeTeamName} pulled off a surprising victory against the odds!`; } else if (actualResult === 'away') { outcomeDescription = `${awayTeamName} secured an impressive away win against expectations!`; } else { outcomeDescription = `The draw is a result neither team expected in this match.`; } } result += `- Outcome analysis: ${outcomeDescription}\n`; // Add simulation info if ((simulations || 1) > 1) { result += `\n**Simulation Details:** This result represents the average of ${simulations || 1} simulated matches.\n`; } // Add a note about custom lineups if they were provided if (params.home_team_lineup || params.away_team_lineup) { result += `\n**Note:** This simulation used custom lineups for ${params.home_team_lineup ? homeTeamName : ''}${params.home_team_lineup && params.away_team_lineup ? ' and ' : ''}${params.away_team_lineup ? awayTeamName : ''}.`; } return result; } catch (error) { return formatError(`Error simulating match: ${error}`); } } case "get_best_formations": { const params = validateInput(BestFormationsSchema, args); try { if (params.format && ['png', 'pdf'].includes(params.format)) { return formatWarning('Visual formats (PNG/PDF) are not supported in this chat interface. Using JSON format instead.'); } const formations = await client.getBestFormations(params); // Get team names for better formatting let teamName = `Team ${params.team_id}`; let opponentName = `Team ${params.opponent_team_id}`; try { const team = await client.getTeams({ team_id: params.team_id }); const opponent = await client.getTeams({ team_id: params.opponent_team_id }); teamName = team.team_name || teamName; opponentName = opponent.team_name || opponentName; } catch { // Continue with team IDs if names can't be retrieved } if (!formations || !Array.isArray(formations) || formations.length === 0) { return formatWarning(`No formation analysis available for ${teamName} vs ${opponentName}.`); } const bestFormations = formations; let result = formatBestFormations(bestFormations, teamName, opponentName); // Add context const homeAway = params.home_match ? 'home' : 'away'; result += `\n**Match Context:** ${teamName} playing ${homeAway} vs ${opponentName}`; if (params.skip_players && params.skip_players.length > 0) { result += `\n**Excluded Players:** ${params.skip_players.length} players excluded from analysis`; } result += `\n\nšŸ’” **Tip:** Use get_best_lineups with a formation filter to see specific player combinations for each formation.`; return result; } catch (error) { return formatError(`Error retrieving formation analysis: ${error}`); } } case "get_lineup_count": { const params = { team_id: args.team_id, opponent_team_id: args.opponent_team_id, home_match: args.home_match, skip_player_ids: false, // We want player IDs for better formatting format: 'json' }; if (args.formation) params.formation = args.formation; if (args.all_players !== undefined) params.all_players = args.all_players; if (args.skip_players) params.skip_players = args.skip_players; try { const lineupCounts = await client.getBestLineupCount(params); // Get team names for better formatting let teamName = `Team ${params.team_id}`; let opponentName = `Team ${params.opponent_team_id}`; try { const team = await client.getTeams({ team_id: params.team_id }); const opponent = await client.getTeams({ team_id: params.opponent_team_id }); teamName = team.team_name || teamName; opponentName = opponent.team_name || opponentName; } catch { // Continue with team IDs if names can't be retrieved } if (!lineupCounts || !Array.isArray(lineupCounts) || lineupCounts.length === 0) { return formatWarning(`No lineup count data available for ${teamName} vs ${opponentName}.`); } // Normalize the data structure const counts = lineupCounts.map((item) => { // Check if we have the new API format or the old one const isNewFormat = 'appearances' in item; return { player_id: item.player_id, player_name: item.player_name, appearances: isNewFormat ? item.appearances : item.cnt, appearance_percentage: isNewFormat ? item.appearance_percentage : (item.cnt / 1000 * 100) }; }); let result = formatLineupCount(counts, teamName, 15); // Add context const homeAway = params.home_match ? 'home' : 'away'; result += `\n**Match Context:** ${teamName} playing ${homeAway} vs ${opponentName}`; if (params.formation) { result += `\n**Formation Filter:** ${params.formation}`; } if (params.all_players) { result += `\n**Player Pool:** All players (ignoring injuries/suspensions)`; } if (params.skip_players && params.skip_players.length > 0) { result += `\n**Excluded Players:** ${params.skip_players.length} players excluded`; } result += `\n\nšŸ’” **Analysis:** Players appearing in 70%+ of best lineups are essential for optimal performance.`; return result; } catch (error) { return formatError(`Error retrieving lineup count analysis: ${error}`); } } default: throw new Error(`Unknown match tool: ${toolName}`); } } catch (error) { return formatError(`Tool execution failed: ${error}`); } } //# sourceMappingURL=match-tools.js.map