Skip to main content
Glama
argotdev

NHL MCP Server

by argotdev
index.ts19.8 kB
#!/usr/bin/env node /** * NHL MCP Server * * Provides tools for querying NHL live data and statistics */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { NHLAPIClient, GameScore, TeamStandings, PlayerStats, GoalieStats } from './nhl-api.js'; const client = new NHLAPIClient(); // Define available tools const TOOLS: Tool[] = [ { name: 'get_live_games', description: 'Get live NHL game scores and status for today or a specific date. Shows current scores, period, game state, and venue information.', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Date in YYYY-MM-DD format (optional, defaults to today)', }, }, }, }, { name: 'get_game_details', description: 'Get detailed information about a specific game including play-by-play data, scoring plays, and period summaries.', inputSchema: { type: 'object', properties: { gameId: { type: 'number', description: 'The NHL game ID', }, }, required: ['gameId'], }, }, { name: 'get_standings', description: 'Get current NHL standings including wins, losses, points, goals for/against, and goal differential. Can filter by division or conference.', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Date in YYYY-MM-DD format (optional, defaults to current standings)', }, division: { type: 'string', description: 'Filter by division (Atlantic, Metropolitan, Central, Pacific)', }, conference: { type: 'string', description: 'Filter by conference (Eastern, Western)', }, }, }, }, { name: 'get_team_stats', description: 'Get detailed statistics for a specific NHL team including roster, season performance, and player stats.', inputSchema: { type: 'object', properties: { teamAbbrev: { type: 'string', description: 'Team abbreviation (e.g., TOR, NYR, BOS, MTL)', }, season: { type: 'string', description: 'Season in format YYYYYYYY (e.g., 20242025), defaults to current season', }, }, required: ['teamAbbrev'], }, }, { name: 'get_player_stats', description: 'Get statistics for top NHL players including goals, assists, points, plus/minus, and other performance metrics.', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Category to sort by: points, goals, assists, plusMinus, shots, shootingPctg (defaults to points)', }, limit: { type: 'number', description: 'Number of players to return (defaults to 20)', }, season: { type: 'string', description: 'Season in format YYYYYYYY (e.g., 20242025), defaults to current season', }, }, }, }, { name: 'get_goalie_stats', description: 'Get statistics for NHL goalies including save percentage, GAA, wins, shutouts, and other goalie-specific metrics.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of goalies to return (defaults to 20)', }, season: { type: 'string', description: 'Season in format YYYYYYYY (e.g., 20242025), defaults to current season', }, }, }, }, { name: 'get_schedule', description: 'Get NHL schedule for upcoming games. Can get schedule for a specific date or team.', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Date in YYYY-MM-DD format (optional, defaults to current week)', }, teamAbbrev: { type: 'string', description: 'Team abbreviation to get specific team schedule (e.g., TOR, NYR)', }, }, }, }, { name: 'get_playoff_bracket', description: 'Get current playoff bracket with series information, matchups, and results.', inputSchema: { type: 'object', properties: { season: { type: 'string', description: 'Season year (e.g., 2024), defaults to current season', }, }, }, }, { name: 'compare_teams', description: 'Compare head-to-head statistics between two NHL teams including recent matchups and historical records.', inputSchema: { type: 'object', properties: { team1: { type: 'string', description: 'First team abbreviation (e.g., TOR)', }, team2: { type: 'string', description: 'Second team abbreviation (e.g., MTL)', }, season: { type: 'string', description: 'Season in format YYYYYYYY (optional, defaults to current)', }, }, required: ['team1', 'team2'], }, }, { name: 'get_team_streak', description: 'Get current winning or losing streak for an NHL team based on recent game results.', inputSchema: { type: 'object', properties: { teamAbbrev: { type: 'string', description: 'Team abbreviation (e.g., TOR, NYR)', }, }, required: ['teamAbbrev'], }, }, { name: 'compare_seasons', description: 'Compare team or player statistics across multiple NHL seasons.', inputSchema: { type: 'object', properties: { teamAbbrev: { type: 'string', description: 'Team abbreviation to compare (optional)', }, seasons: { type: 'array', items: { type: 'string', }, description: 'Array of seasons to compare in format YYYYYYYY (e.g., ["20232024", "20242025"])', }, }, required: ['seasons'], }, }, ]; // Helper functions function formatGameScore(game: GameScore): string { const status = game.gameState === 'LIVE' || game.gameState === 'CRIT' ? `LIVE - Period ${game.period}` : game.gameState === 'FUT' ? 'Scheduled' : game.gameState === 'FINAL' || game.gameState === 'OFF' ? 'Final' : game.gameState; return `${game.awayTeam.abbrev} ${game.awayTeam.score} @ ${game.homeTeam.abbrev} ${game.homeTeam.score} - ${status}\nVenue: ${game.venue}\nDate: ${game.gameDate}\nGame ID: ${game.id}`; } function formatStandings(standings: TeamStandings[]): string { let result = 'Team | GP | W | L | OT | PTS | GF | GA | DIFF | Div\n'; result += '-'.repeat(70) + '\n'; standings.forEach((team) => { const teamName = team.teamAbbrev.default.padEnd(4); const gp = team.gamesPlayed.toString().padStart(3); const w = team.wins.toString().padStart(2); const l = team.losses.toString().padStart(2); const ot = team.otLosses.toString().padStart(2); const pts = team.points.toString().padStart(3); const gf = team.goalFor.toString().padStart(3); const ga = team.goalAgainst.toString().padStart(3); const diff = team.goalDifferential.toString().padStart(4); const div = team.divisionAbbrev; result += `${teamName} | ${gp} | ${w} | ${l} | ${ot} | ${pts} | ${gf} | ${ga} | ${diff} | ${div}\n`; }); return result; } function formatPlayerStats(players: PlayerStats[], category: string): string { let result = `Rank | Player | Team | Pos | ${category.toUpperCase()}\n`; result += '-'.repeat(60) + '\n'; players.forEach((player, index) => { const name = `${player.firstName.default} ${player.lastName.default}`; const displayName = name.substring(0, 25).padEnd(25); const team = player.teamAbbrev.padEnd(4); const pos = player.position.padEnd(2); const value = player.value.toString().padStart(4); const rank = (index + 1).toString().padStart(3); result += `${rank} | ${displayName} | ${team} | ${pos} | ${value}\n`; }); return result; } function formatGoalieStats(goalies: GoalieStats[], category: string): string { let result = `Rank | Goalie | Team | ${category.toUpperCase()}\n`; result += '-'.repeat(60) + '\n'; goalies.forEach((goalie, index) => { const name = `${goalie.firstName.default} ${goalie.lastName.default}`; const displayName = name.substring(0, 25).padEnd(25); const team = goalie.teamAbbrev.padEnd(4); const value = category === 'savePctg' ? goalie.value.toFixed(3) : goalie.value.toString(); const rank = (index + 1).toString().padStart(3); result += `${rank} | ${displayName} | ${team} | ${value}\n`; }); return result; } async function analyzeStreak(teamAbbrev: string): Promise<string> { try { const schedule = await client.getTeamSchedule(teamAbbrev); if (!schedule.games || schedule.games.length === 0) { return `No games found for ${teamAbbrev}`; } // Get completed games sorted by date const completedGames = schedule.games .filter((g: any) => g.gameState === 'OFF' || g.gameState === 'FINAL') .sort((a: any, b: any) => new Date(b.gameDate).getTime() - new Date(a.gameDate).getTime()); if (completedGames.length === 0) { return `No completed games found for ${teamAbbrev} this season`; } let streakCount = 0; let streakType = ''; const recentResults: string[] = []; for (const game of completedGames) { const isHome = game.homeTeam.abbrev === teamAbbrev; const teamScore = isHome ? game.homeTeam.score : game.awayTeam.score; const oppScore = isHome ? game.awayTeam.score : game.homeTeam.score; const oppTeam = isHome ? game.awayTeam.abbrev : game.homeTeam.abbrev; const won = teamScore > oppScore; const result = won ? 'W' : 'L'; recentResults.push(`${result} ${teamScore}-${oppScore} vs ${oppTeam}`); if (streakCount === 0) { streakType = result; streakCount = 1; } else if (result === streakType) { streakCount++; } else { break; } if (recentResults.length >= 10) break; } const streakText = streakType === 'W' ? `${streakCount} game winning streak` : `${streakCount} game losing streak`; return `${teamAbbrev} Current Streak: ${streakText}\n\nLast 10 games:\n${recentResults.join('\n')}`; } catch (error: any) { return `Error analyzing streak: ${error.message}`; } } async function compareTeamsHeadToHead(team1: string, team2: string, season?: string): Promise<string> { try { const schedule1 = await client.getTeamSchedule(team1, season); if (!schedule1.games) { return `No schedule data found for ${team1}`; } // Find games between these two teams const matchups = schedule1.games.filter((game: any) => { return ( (game.homeTeam.abbrev === team1 && game.awayTeam.abbrev === team2) || (game.homeTeam.abbrev === team2 && game.awayTeam.abbrev === team1) ); }); if (matchups.length === 0) { return `No matchups found between ${team1} and ${team2} this season`; } let team1Wins = 0; let team2Wins = 0; const results: string[] = []; matchups.forEach((game: any) => { const isTeam1Home = game.homeTeam.abbrev === team1; const team1Score = isTeam1Home ? game.homeTeam.score : game.awayTeam.score; const team2Score = isTeam1Home ? game.awayTeam.score : game.homeTeam.score; if (game.gameState === 'OFF' || game.gameState === 'FINAL') { if (team1Score > team2Score) { team1Wins++; results.push(`${game.gameDate}: ${team1} ${team1Score}, ${team2} ${team2Score} - ${team1} WIN`); } else { team2Wins++; results.push(`${game.gameDate}: ${team1} ${team1Score}, ${team2} ${team2Score} - ${team2} WIN`); } } else if (game.gameState === 'FUT') { results.push(`${game.gameDate}: Upcoming game`); } else { results.push(`${game.gameDate}: ${team1} ${team1Score}, ${team2} ${team2Score} - IN PROGRESS`); } }); return `Head-to-Head: ${team1} vs ${team2}\n\nSeason Series: ${team1} ${team1Wins}-${team2Wins} ${team2}\n\nGames:\n${results.join('\n')}`; } catch (error: any) { return `Error comparing teams: ${error.message}`; } } async function compareSeasons(seasons: string[], teamAbbrev?: string): Promise<string> { try { if (!teamAbbrev) { // Compare league-wide stats const results: string[] = []; for (const season of seasons) { const standings = await client.getStandingsBySeason(season); const teams = standings.standings || []; const totalGoals = teams.reduce((sum, t) => sum + t.goalFor, 0); const totalGames = teams.reduce((sum, t) => sum + t.gamesPlayed, 0); results.push( `Season ${client.formatSeason(season)}:\n` + ` Total teams: ${teams.length}\n` + ` Total goals: ${totalGoals}\n` + ` Avg goals/game: ${(totalGoals / totalGames).toFixed(2)}` ); } return `Season Comparison:\n\n${results.join('\n\n')}`; } else { // Compare specific team across seasons const results: string[] = []; for (const season of seasons) { const standings = await client.getStandingsBySeason(season); const team = (standings.standings || []).find( (t) => t.teamAbbrev.default === teamAbbrev ); if (team) { results.push( `${client.formatSeason(season)} - ${teamAbbrev}:\n` + ` Record: ${team.wins}-${team.losses}-${team.otLosses}\n` + ` Points: ${team.points}\n` + ` Goals For: ${team.goalFor}\n` + ` Goals Against: ${team.goalAgainst}\n` + ` Goal Diff: ${team.goalDifferential}` ); } else { results.push(`${client.formatSeason(season)} - ${teamAbbrev}: No data found`); } } return `Season Comparison for ${teamAbbrev}:\n\n${results.join('\n\n')}`; } } catch (error: any) { return `Error comparing seasons: ${error.message}`; } } // Create MCP server const server = new Server( { name: 'nhl-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Register tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Ensure args exists, default to empty object const parameters = args || {}; switch (name) { case 'get_live_games': { const games = await client.getTodaysScores(parameters.date as string | undefined); if (games.length === 0) { return { content: [ { type: 'text', text: 'No games scheduled for this date' }, ], }; } const formatted = games.map(formatGameScore).join('\n\n'); return { content: [{ type: 'text', text: formatted }], }; } case 'get_game_details': { const details = await client.getGameDetails(parameters.gameId as number); return { content: [{ type: 'text', text: JSON.stringify(details, null, 2) }], }; } case 'get_standings': { const standings = await client.getStandings(parameters.date as string | undefined); let filtered = standings.standings || []; if (parameters.division) { filtered = filtered.filter( (t) => t.divisionAbbrev.toLowerCase() === (parameters.division as string).toLowerCase() ); } if (parameters.conference) { filtered = filtered.filter( (t) => t.conferenceAbbrev.toLowerCase() === (parameters.conference as string).toLowerCase() ); } const formatted = formatStandings(filtered); return { content: [{ type: 'text', text: formatted }], }; } case 'get_team_stats': { const stats = await client.getTeamStats( parameters.teamAbbrev as string, parameters.season as string | undefined ); return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }], }; } case 'get_player_stats': { const category = (parameters.category as string) || 'points'; const players = await client.getTopSkaters( category, parameters.limit as number | undefined, parameters.season as string | undefined ); const formatted = formatPlayerStats(players, category); return { content: [{ type: 'text', text: formatted }], }; } case 'get_goalie_stats': { const goalies = await client.getTopGoalies( parameters.limit as number | undefined, parameters.season as string | undefined ); const formatted = formatGoalieStats(goalies, 'savePctg'); return { content: [{ type: 'text', text: formatted }], }; } case 'get_schedule': { if (parameters.teamAbbrev) { const schedule = await client.getTeamSchedule( parameters.teamAbbrev as string, parameters.season as string | undefined ); return { content: [{ type: 'text', text: JSON.stringify(schedule, null, 2) }], }; } else { const schedule = await client.getSchedule(parameters.date as string | undefined); return { content: [{ type: 'text', text: JSON.stringify(schedule, null, 2) }], }; } } case 'get_playoff_bracket': { const bracket = await client.getPlayoffBracket(parameters.season as string | undefined); return { content: [{ type: 'text', text: JSON.stringify(bracket, null, 2) }], }; } case 'compare_teams': { const comparison = await compareTeamsHeadToHead( parameters.team1 as string, parameters.team2 as string, parameters.season as string | undefined ); return { content: [{ type: 'text', text: comparison }], }; } case 'get_team_streak': { const streak = await analyzeStreak(parameters.teamAbbrev as string); return { content: [{ type: 'text', text: streak }], }; } case 'compare_seasons': { const comparison = await compareSeasons( parameters.seasons as string[], parameters.teamAbbrev as string | undefined ); return { content: [{ type: 'text', text: comparison }], }; } default: return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error: any) { return { content: [ { type: 'text', text: `Error executing ${name}: ${error.message}`, }, ], isError: true, }; } }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('NHL MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

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/argotdev/nhl-mcp-ts'

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