@semanticintent/semantic-chirp-intelligence-mcp 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -7,41 +7,28 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  // Node.js
10
- import * as fs from "fs";
11
10
  import * as path from "path";
11
+ import { readFileSync } from "fs";
12
12
  import { fileURLToPath } from "url";
13
- import * as dotenv from "dotenv";
14
- import https from "https";
15
- import { parseString } from "xml2js";
16
- import { promisify } from "util";
17
13
  import { GOVERNANCE_MONITOR, validateSemanticChirpContract, auditSemanticContract, checkGovernanceHealth } from './domain/governance.js';
18
14
  // Config layer
19
15
  import { CHIRP_STYLES } from './config/chirp-styles.js';
20
16
  import { PERSONALITY_MODES } from './config/personality-modes.js';
21
17
  import { TOOL_METADATA } from './config/tool-metadata.js';
22
- // Services layer
23
- import { YahooApiClient } from './services/YahooApiClient.js';
24
18
  // Analysis layer
25
19
  import { IceAnalysis } from './analyses/IceAnalysis.js';
26
20
  import { GamesInHandAnalysis } from './analyses/GamesInHandAnalysis.js';
27
21
  import { StreamingAnalysis } from './analyses/StreamingAnalysis.js';
28
22
  import { LineupAnalysis } from './analyses/LineupAnalysis.js';
29
23
  import { WeekendStreamAnalysis } from './analyses/WeekendStreamAnalysis.js';
30
- // Experimental: Semantic Intent Parser
31
- import { SEMANTIC_PLAYER_COMPARISON, getPlayerComparisonInputSchema, executePlayerComparison } from './experimental/semantic-tool-integration.js';
32
- import { SEMANTIC_BREAKOUT_ANALYSIS, getBreakoutAnalysisInputSchema, executeBreakoutAnalysis } from './experimental/semantic-breakout-tool.js';
33
24
  import { BreakoutAnalysis } from './analyses/BreakoutAnalysis.js';
34
- dotenv.config();
35
- const parseXML = promisify(parseString);
25
+ import { ScheduleValueAnalysis } from './analyses/ScheduleValueAnalysis.js';
26
+ import { DraftPickAnalysis } from './analyses/DraftPickAnalysis.js';
27
+ import { NHL_STATS } from './services/NhlStatsService.js';
28
+ import { ROSTER_STORE } from './services/RosterStore.js';
29
+ import { LEAGUE_DATA, NO_ROSTER_MESSAGE, NO_OPPONENT_MESSAGE } from './services/LeagueDataService.js';
30
+ import { NHL_SCHEDULE, NhlScheduleService } from './services/NhlScheduleService.js';
36
31
  // Configuration
37
- const YAHOO_CLIENT_ID = process.env.YAHOO_CLIENT_ID;
38
- const YAHOO_CLIENT_SECRET = process.env.YAHOO_CLIENT_SECRET;
39
- const LEAGUE_ID = process.env.YAHOO_LEAGUE_ID;
40
- const TEAM_ID = process.env.YAHOO_TEAM_ID;
41
- const __filename = fileURLToPath(import.meta.url);
42
- const __dirname = path.dirname(__filename);
43
- const TOKEN_FILE = path.join(__dirname, "..", ".yahoo-oauth.json");
44
- const YAHOO_API_BASE = "https://fantasysports.yahooapis.com/fantasy/v2";
45
32
  // ==========================================
46
33
  // 🔧 MCP Tool Schema Base
47
34
  // ==========================================
@@ -65,101 +52,15 @@ const baseChirpSchema = {
65
52
  // ==========================================
66
53
  // 🏗️ Service Initialization
67
54
  // ==========================================
68
- // Initialize Yahoo API client
69
- const yahooClient = new YahooApiClient(YAHOO_CLIENT_ID, YAHOO_CLIENT_SECRET, YAHOO_API_BASE);
70
55
  // Initialize analysis instances
71
- const iceAnalysis = new IceAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
72
- const gamesInHandAnalysis = new GamesInHandAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
73
- const streamingAnalysis = new StreamingAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
74
- const lineupAnalysis = new LineupAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
75
- const breakoutAnalysis = new BreakoutAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
76
- const weekendStreamAnalysis = new WeekendStreamAnalysis(yahooClient, LEAGUE_ID, TEAM_ID);
77
- let cachedToken = null;
78
- function loadToken() {
79
- try {
80
- console.error(`[DEBUG] Looking for token at: ${TOKEN_FILE}`);
81
- if (fs.existsSync(TOKEN_FILE)) {
82
- const tokenData = fs.readFileSync(TOKEN_FILE, "utf8");
83
- const token = JSON.parse(tokenData);
84
- console.error(`[DEBUG] Token loaded successfully`);
85
- cachedToken = token;
86
- return token;
87
- }
88
- else {
89
- console.error(`[DEBUG] Token file not found`);
90
- }
91
- }
92
- catch (error) {
93
- console.error("[ERROR] Error loading token:", error);
94
- }
95
- return null;
96
- }
97
- function saveToken(token) {
98
- token.expires_at = Date.now() + (token.expires_in * 1000);
99
- fs.writeFileSync(TOKEN_FILE, JSON.stringify(token, null, 2));
100
- cachedToken = token;
101
- console.error("[DEBUG] Token saved successfully");
102
- }
103
- async function refreshAccessToken() {
104
- const token = cachedToken || loadToken();
105
- if (!token) {
106
- throw new Error("No refresh token available. Please re-authenticate.");
107
- }
108
- console.error("[DEBUG] Refreshing access token...");
109
- const tokenData = new URLSearchParams({
110
- client_id: YAHOO_CLIENT_ID,
111
- client_secret: YAHOO_CLIENT_SECRET,
112
- redirect_uri: "oob",
113
- refresh_token: token.refresh_token,
114
- grant_type: "refresh_token",
115
- });
116
- const options = {
117
- hostname: "api.login.yahoo.com",
118
- port: 443,
119
- path: "/oauth2/get_token",
120
- method: "POST",
121
- headers: {
122
- "Content-Type": "application/x-www-form-urlencoded",
123
- "Content-Length": tokenData.toString().length,
124
- },
125
- };
126
- return new Promise((resolve, reject) => {
127
- const req = https.request(options, (res) => {
128
- let data = "";
129
- res.on("data", (chunk) => {
130
- data += chunk;
131
- });
132
- res.on("end", () => {
133
- try {
134
- const newToken = JSON.parse(data);
135
- saveToken(newToken);
136
- console.error("[DEBUG] Token refreshed successfully");
137
- resolve(newToken.access_token);
138
- }
139
- catch (error) {
140
- reject(new Error(`Failed to parse token response: ${error}`));
141
- }
142
- });
143
- });
144
- req.on("error", (error) => {
145
- reject(error);
146
- });
147
- req.write(tokenData.toString());
148
- req.end();
149
- });
150
- }
151
- async function getValidAccessToken() {
152
- const token = cachedToken || loadToken();
153
- if (!token) {
154
- throw new Error("No authentication token found! Run: node authenticate.js");
155
- }
156
- // Check if token is expired (with 5 minute buffer)
157
- if (token.expires_at && token.expires_at < Date.now() + 300000) {
158
- console.error("[DEBUG] Token expired or expiring soon, refreshing...");
159
- return await refreshAccessToken();
160
- }
161
- return token.access_token;
162
- }
56
+ const iceAnalysis = new IceAnalysis();
57
+ const gamesInHandAnalysis = new GamesInHandAnalysis();
58
+ const streamingAnalysis = new StreamingAnalysis();
59
+ const lineupAnalysis = new LineupAnalysis();
60
+ const breakoutAnalysis = new BreakoutAnalysis();
61
+ const scheduleValueAnalysis = new ScheduleValueAnalysis();
62
+ const draftPickAnalysis = new DraftPickAnalysis();
63
+ const weekendStreamAnalysis = new WeekendStreamAnalysis();
163
64
  // Helper function to find current matchup by status
164
65
  function findCurrentMatchup(matchups) {
165
66
  console.error('[DEBUG] findCurrentMatchup called');
@@ -196,282 +97,167 @@ function findCurrentMatchup(matchups) {
196
97
  return null;
197
98
  return matchups[lastKey].matchup;
198
99
  }
199
- // Yahoo API helper function
200
- async function yahooApiRequest(endpoint, format = "json") {
201
- const accessToken = await getValidAccessToken();
202
- const url = `${YAHOO_API_BASE}${endpoint}${endpoint.includes('?') ? '&' : '?'}format=${format}`;
203
- console.error(`[DEBUG] API Request: ${endpoint}`);
204
- return new Promise((resolve, reject) => {
205
- const urlObj = new URL(url);
206
- const options = {
207
- hostname: urlObj.hostname,
208
- port: 443,
209
- path: urlObj.pathname + urlObj.search,
210
- method: "GET",
211
- headers: {
212
- Authorization: `Bearer ${accessToken}`,
213
- Accept: "application/json",
214
- },
215
- };
216
- const req = https.request(options, (res) => {
217
- let data = "";
218
- res.on("data", (chunk) => {
219
- data += chunk;
220
- });
221
- res.on("end", () => {
222
- if (res.statusCode === 401) {
223
- // Token expired, try to refresh and retry
224
- refreshAccessToken()
225
- .then(() => yahooApiRequest(endpoint, format))
226
- .then(resolve)
227
- .catch(reject);
228
- return;
229
- }
230
- if (res.statusCode !== 200) {
231
- reject(new Error(`API returned status ${res.statusCode}: ${data}`));
232
- return;
233
- }
234
- try {
235
- const parsed = JSON.parse(data);
236
- resolve(parsed);
237
- }
238
- catch (error) {
239
- reject(new Error(`Failed to parse API response: ${error}`));
240
- }
241
- });
242
- });
243
- req.on("error", (error) => {
244
- reject(error);
245
- });
246
- req.end();
247
- });
248
- }
249
100
  // Tool: Get Team Roster
250
101
  async function getTeamRoster() {
251
- const data = await yahooApiRequest(`/team/nhl.l.${LEAGUE_ID}.t.${TEAM_ID}/roster`);
252
- const teamArray = data.fantasy_content.team[0];
253
- const rosterData = data.fantasy_content.team[1].roster["0"].players;
254
- // Extract team info
255
- const teamKey = teamArray.find((item) => item.team_key)?.team_key;
256
- const teamName = teamArray.find((item) => item.name)?.name;
257
- // Parse players
258
- const playerKeys = Object.keys(rosterData).filter(key => key !== 'count');
259
- const players = playerKeys.map(key => {
260
- const playerData = rosterData[key].player[0];
261
- const positionData = rosterData[key].player[1];
262
- // Find attributes in the array
263
- const playerId = playerData.find((item) => item.player_id)?.player_id;
264
- const name = playerData.find((item) => item.name)?.name?.full;
265
- const displayPosition = playerData.find((item) => item.display_position)?.display_position;
266
- const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
267
- const status = playerData.find((item) => item.status)?.status || "";
268
- const selectedPos = positionData?.selected_position?.find((item) => item.position)?.position || "BN";
269
- return {
270
- player_id: playerId,
271
- name: name,
272
- position: displayPosition,
273
- team: team,
274
- status: status,
275
- selected_position: selectedPos,
276
- };
277
- });
102
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
103
+ const roster = LEAGUE_DATA.getRoster();
104
+ if (!roster)
105
+ return { error: NO_ROSTER_MESSAGE };
106
+ const today = NhlScheduleService.today();
107
+ const weekEnd = NhlScheduleService.addDays(today, 6);
278
108
  return {
279
- team_key: teamKey,
280
- team_name: teamName,
281
- roster: players,
109
+ team_name: roster.team_name,
110
+ player_count: roster.players.length,
111
+ players: roster.players.map((p) => ({
112
+ ...p,
113
+ games_next_7_days: NHL_SCHEDULE.isAvailable()
114
+ ? NHL_SCHEDULE.countGamesInRange(p.team, today, weekEnd)
115
+ : null,
116
+ season_stats: p.stats ?? null
117
+ })),
118
+ data_source: `NHL public API (rosters ${NHL_STATS.getSeasons().roster}, stats ${NHL_STATS.getSeasons().stats})`
282
119
  };
283
120
  }
284
121
  // Tool: Get League Standings
285
122
  async function getLeagueStandings() {
286
- const data = await yahooApiRequest(`/league/nhl.l.${LEAGUE_ID}/standings`);
287
- const teams = data.fantasy_content.league[1].standings[0].teams;
288
- const standings = Object.keys(teams)
289
- .filter(key => key !== 'count')
290
- .map(key => {
291
- const team = teams[key].team[0];
292
- const standings = teams[key].team[1].team_standings;
123
+ const standings = LEAGUE_DATA.getStandings();
124
+ if (!standings) {
293
125
  return {
294
- team_name: team[2].name,
295
- rank: standings.rank,
296
- wins: standings.outcome_totals?.wins || 0,
297
- losses: standings.outcome_totals?.losses || 0,
298
- ties: standings.outcome_totals?.ties || 0,
299
- points: standings.points_for,
126
+ error: 'No standings stored. Paste them with `set_standings` — copy the standings ' +
127
+ 'table from your league, one team per line.'
300
128
  };
301
- });
302
- return { standings };
303
- }
304
- // Tool: Get Current Matchup
305
- async function getCurrentMatchup() {
306
- console.error('[DEBUG] getCurrentMatchup called');
307
- const data = await yahooApiRequest(`/team/nhl.l.${LEAGUE_ID}.t.${TEAM_ID}/matchups`);
308
- console.error('[DEBUG] API response received');
309
- const matchups = data.fantasy_content.team[1].matchups;
310
- console.error('[DEBUG] Matchups count:', matchups?.count);
311
- // Debug: log first 3 matchup keys and their structure
312
- if (matchups) {
313
- const keys = Object.keys(matchups).filter(k => k !== 'count').slice(0, 3);
314
- keys.forEach(key => {
315
- const m = matchups[key];
316
- console.error(`[DEBUG] matchups["${key}"] structure:`, JSON.stringify({
317
- hasMatchup: !!m?.matchup,
318
- isArray: Array.isArray(m?.matchup),
319
- week: m?.matchup?.[0]?.week || m?.matchup?.week,
320
- status: m?.matchup?.[0]?.status || m?.matchup?.status
321
- }));
322
- });
323
- }
324
- const currentMatchup = findCurrentMatchup(matchups);
325
- if (!currentMatchup) {
326
- console.error('[DEBUG] No current matchup found');
327
- return { message: "No current matchup (bye week or season not started)" };
328
129
  }
329
- // currentMatchup is the matchup object with structure:
330
- // matchup = { "0": { teams: {...} }, week: "1", status: "postevent", ... }
331
- const matchupData = currentMatchup[0];
332
- const week = currentMatchup.week;
333
- const status = currentMatchup.status;
334
- const teams = matchupData?.teams;
335
- if (!teams) {
336
- console.error('[DEBUG] No teams found in matchup');
337
- return { message: "No teams data in current matchup" };
338
- }
339
- console.error('[DEBUG] Final result:', {
340
- week,
341
- status,
342
- opponent: teams['1']?.team?.[0]?.[2]?.name
343
- });
344
130
  return {
345
- week,
346
- status,
347
- your_team: teams['0']?.team?.[0]?.[2]?.name || 'Unknown',
348
- opponent: teams['1']?.team?.[0]?.[2]?.name || 'Unknown',
131
+ teams: standings.rows.length,
132
+ standings: standings.rows,
133
+ updated_at: standings.updated_at
349
134
  };
350
135
  }
136
+ // Tool: Get Current Matchup
137
+ // Removed in v4: getCurrentMatchup required live fantasy-platform data
138
+ // (live matchup scoring, weekly results, or league ownership) that no
139
+ // public source exposes. See CHANGELOG for the full list.
351
140
  // Tool: Search Players
352
141
  async function searchPlayers(position, count = 25) {
353
- let endpoint = `/league/nhl.l.${LEAGUE_ID}/players;status=A;count=${count}`;
354
- if (position) {
355
- endpoint += `;position=${position}`;
142
+ await NHL_STATS.load();
143
+ if (!NHL_STATS.isAvailable()) {
144
+ return { error: 'NHL player data unavailable', reason: NHL_STATS.getUnavailableReason() };
356
145
  }
357
- const data = await yahooApiRequest(endpoint);
358
- const playersData = data.fantasy_content.league[1].players;
359
- const players = Object.keys(playersData)
360
- .filter(key => key !== 'count')
361
- .map(key => {
362
- const playerData = playersData[key].player[0];
363
- // Find attributes in the array using .find() to handle dynamic structure
364
- const playerId = playerData.find((item) => item.player_id)?.player_id;
365
- const name = playerData.find((item) => item.name)?.name?.full;
366
- const displayPosition = playerData.find((item) => item.display_position)?.display_position;
367
- const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
368
- const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
369
- return {
370
- player_id: playerId,
371
- name: name,
372
- position: displayPosition,
373
- team: team,
374
- percent_owned: percentOwned,
375
- };
376
- });
377
- return { players };
378
- }
379
- // Tool: Get Player Stats
380
- async function getPlayerStats(playerId) {
381
- const data = await yahooApiRequest(`/player/nhl.p.${playerId}/stats`);
382
- const playerData = data.fantasy_content.player[0];
383
- const stats = data.fantasy_content.player[1]?.player_stats?.stats || [];
384
- // Find attributes in the array using .find() to handle dynamic structure
385
- const playerIdResult = playerData.find((item) => item.player_id)?.player_id;
386
- const name = playerData.find((item) => item.name)?.name?.full;
387
- const displayPosition = playerData.find((item) => item.display_position)?.display_position;
388
- const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
146
+ // Fantasy platforms say LW/RW; the NHL says L/R.
147
+ const wanted = String(position ?? '').toUpperCase()
148
+ .replace(/^LW$/, 'L').replace(/^RW$/, 'R');
149
+ const owned = new Set((LEAGUE_DATA.getRoster()?.players ?? []).map((p) => p.player_id));
150
+ const players = NHL_STATS.getAll()
151
+ .filter(p => !wanted || p.position === wanted)
152
+ .map(p => ({
153
+ player_id: p.player_id,
154
+ name: p.name,
155
+ team: p.team,
156
+ position: p.position,
157
+ on_your_roster: owned.has(p.player_id),
158
+ season_stats: p.stats ?? null,
159
+ // Rank skaters by production, goalies by wins.
160
+ rank_value: p.position === 'G' ? (p.stats?.wins ?? 0) : (p.stats?.points ?? 0)
161
+ }))
162
+ .sort((a, b) => b.rank_value - a.rank_value)
163
+ .slice(0, Math.max(1, count));
389
164
  return {
390
- player_id: playerIdResult,
391
- name: name,
392
- position: displayPosition,
393
- team: team,
394
- stats: stats,
165
+ position: position ?? 'all',
166
+ returned: players.length,
167
+ players,
168
+ note: 'Ranked by last season production across all NHL players. Whether a player ' +
169
+ 'is available in your league is league-private and not knowable here — ' +
170
+ 'on_your_roster reflects only the roster you pasted.',
171
+ data_source: `NHL public API (stats ${NHL_STATS.getSeasons().stats})`
395
172
  };
396
173
  }
397
- // Tool: Get Weekly Stats
398
- async function getWeeklyStats() {
399
- const data = await yahooApiRequest(`/team/nhl.l.${LEAGUE_ID}.t.${TEAM_ID}/matchups`);
400
- const matchups = data.fantasy_content.team[1].matchups;
401
- const currentMatchup = findCurrentMatchup(matchups);
402
- if (!currentMatchup) {
403
- return { message: "No current matchup" };
404
- }
405
- // currentMatchup structure: { "0": { teams: {...} }, week: 5, status: "midevent", ... }
406
- const teams = currentMatchup["0"]?.teams;
407
- const week = currentMatchup.week;
408
- const status = currentMatchup.status;
409
- if (!teams) {
410
- return { message: "No teams data in current matchup" };
174
+ // Tool: Get Player Stats
175
+ async function getPlayerStats(playerId) {
176
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
177
+ // Accept an NHL id or, far more usefully, a name a human would type.
178
+ const direct = NHL_STATS.getById(playerId);
179
+ const resolution = direct ? { player: direct } : NHL_STATS.resolve(playerId);
180
+ if (!resolution.player) {
181
+ const r = resolution;
182
+ return {
183
+ error: `Could not resolve "${playerId}" to a single NHL player`,
184
+ reason: r.reason,
185
+ ...(r.ambiguous ? { candidates: r.ambiguous.map((p) => `${p.name} (${p.team} ${p.position})`) } : {})
186
+ };
411
187
  }
412
- // Extract games remaining data
413
- const yourGames = teams['0']?.team?.[1]?.team_remaining_games?.total;
414
- const oppGames = teams['1']?.team?.[1]?.team_remaining_games?.total;
188
+ const p = resolution.player;
189
+ const today = NhlScheduleService.today();
415
190
  return {
416
- week,
417
- status,
418
- your_team: {
419
- name: teams['0']?.team?.[0]?.[2]?.name || 'Unknown',
420
- stats: teams['0']?.team?.[1]?.team_stats?.stats || [],
421
- games_remaining: yourGames?.remaining_games || 0,
422
- games_completed: yourGames?.completed_games || 0,
423
- live_games: yourGames?.live_games || 0,
424
- },
425
- opponent: {
426
- name: teams['1']?.team?.[0]?.[2]?.name || 'Unknown',
427
- stats: teams['1']?.team?.[1]?.team_stats?.stats || [],
428
- games_remaining: oppGames?.remaining_games || 0,
429
- games_completed: oppGames?.completed_games || 0,
430
- live_games: oppGames?.live_games || 0,
431
- },
432
- games_in_hand: {
433
- your_remaining: yourGames?.remaining_games || 0,
434
- opponent_remaining: oppGames?.remaining_games || 0,
435
- difference: (yourGames?.remaining_games || 0) - (oppGames?.remaining_games || 0),
436
- advantage: (yourGames?.remaining_games || 0) > (oppGames?.remaining_games || 0) ? "You" : "Opponent"
437
- }
191
+ player_id: p.player_id,
192
+ name: p.name,
193
+ team: p.team,
194
+ position: p.position,
195
+ season_stats: p.stats ?? null,
196
+ stats_season: NHL_STATS.getSeasons().stats,
197
+ games_next_7_days: NHL_SCHEDULE.isAvailable()
198
+ ? NHL_SCHEDULE.countGamesInRange(p.team, today, NhlScheduleService.addDays(today, 6))
199
+ : null,
200
+ upcoming: NHL_SCHEDULE.isAvailable()
201
+ ? NHL_SCHEDULE.getGamesInRange(p.team, today, NhlScheduleService.addDays(today, 13)).slice(0, 6)
202
+ : []
438
203
  };
439
204
  }
205
+ // Tool: Get Weekly Stats
206
+ // Removed in v4: getWeeklyStats required live fantasy-platform data
207
+ // (live matchup scoring, weekly results, or league ownership) that no
208
+ // public source exposes. See CHANGELOG for the full list.
440
209
  // Tool: Compare Matchup
441
210
  async function compareMatchup() {
442
- const data = await yahooApiRequest(`/team/nhl.l.${LEAGUE_ID}.t.${TEAM_ID}/matchups`);
443
- const matchups = data.fantasy_content.team[1].matchups;
444
- const currentMatchup = findCurrentMatchup(matchups);
445
- if (!currentMatchup) {
446
- return { message: "No current matchup" };
447
- }
448
- // currentMatchup structure: { "0": { teams: {...} }, week: 5, status: "midevent", ... }
449
- const teams = currentMatchup["0"]?.teams;
450
- const week = currentMatchup.week;
451
- if (!teams) {
452
- return { message: "No teams data in current matchup" };
453
- }
454
- const yourStats = teams['0']?.team?.[1]?.team_stats?.stats || [];
455
- const oppStats = teams['1']?.team?.[1]?.team_stats?.stats || [];
456
- const comparison = yourStats.map((stat, idx) => {
457
- const yourVal = parseFloat(stat.stat.value) || 0;
458
- const oppVal = parseFloat(oppStats[idx]?.stat?.value || 0) || 0;
459
- return {
460
- category: stat.stat.display_name,
461
- stat_id: stat.stat.stat_id,
462
- your_value: yourVal,
463
- opp_value: oppVal,
464
- winning: yourVal > oppVal,
465
- };
211
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
212
+ const mine = LEAGUE_DATA.getRoster();
213
+ const theirs = LEAGUE_DATA.getOpponentRoster();
214
+ if (!mine)
215
+ return { error: NO_ROSTER_MESSAGE };
216
+ if (!theirs)
217
+ return { error: NO_OPPONENT_MESSAGE };
218
+ const today = NhlScheduleService.today();
219
+ const weekEnd = NhlScheduleService.addDays(today, 6);
220
+ const summarize = (roster) => {
221
+ const totals = { G: 0, A: 0, P: 0, '+/-': 0, PIM: 0, SOG: 0, PPG: 0, W: 0 };
222
+ let games = 0;
223
+ for (const p of roster.players) {
224
+ if (p.selected_position === 'IR')
225
+ continue;
226
+ const s = p.stats ?? {};
227
+ totals.G += s.goals ?? 0;
228
+ totals.A += s.assists ?? 0;
229
+ totals.P += s.points ?? 0;
230
+ totals['+/-'] += s.plus_minus ?? 0;
231
+ totals.PIM += s.penalty_minutes ?? 0;
232
+ totals.SOG += s.shots ?? 0;
233
+ totals.PPG += s.power_play_goals ?? 0;
234
+ totals.W += s.wins ?? 0;
235
+ games += NHL_SCHEDULE.isAvailable() ? NHL_SCHEDULE.countGamesInRange(p.team, today, weekEnd) : 0;
236
+ }
237
+ return { team_name: roster.team_name, totals, games_this_week: games };
238
+ };
239
+ const you = summarize(mine);
240
+ const them = summarize(theirs);
241
+ const categories = Object.keys(you.totals).map(cat => {
242
+ const a = you.totals[cat];
243
+ const b = them.totals[cat];
244
+ return { category: cat, you: Math.round(a * 10) / 10, opponent: Math.round(b * 10) / 10,
245
+ edge: a === b ? 'EVEN' : a > b ? 'YOU' : 'OPPONENT' };
466
246
  });
467
- const categoriesWinning = comparison.filter((c) => c.winning).length;
247
+ const won = categories.filter(c => c.edge === 'YOU').length;
468
248
  return {
469
- week,
470
- your_team: teams['0']?.team?.[0]?.[2]?.name || 'Unknown',
471
- opponent: teams['1']?.team?.[0]?.[2]?.name || 'Unknown',
472
- categories_winning: categoriesWinning,
473
- categories_total: comparison.length,
474
- category_breakdown: comparison,
249
+ your_team: you.team_name,
250
+ opponent: them.team_name,
251
+ categories,
252
+ category_edge: `${won}-${categories.filter(c => c.edge === 'OPPONENT').length}`,
253
+ schedule: {
254
+ your_games_this_week: you.games_this_week,
255
+ opponent_games_this_week: them.games_this_week,
256
+ advantage: you.games_this_week - them.games_this_week
257
+ },
258
+ basis: `Last season totals (${NHL_STATS.getSeasons().stats}) as a proxy for current strength, ` +
259
+ 'plus real games scheduled this week. Not live scoring — this compares roster ' +
260
+ 'quality and volume, not what has actually happened in your matchup.'
475
261
  };
476
262
  }
477
263
  async function getRosterTransactionRecommendations(lookAheadDays = 7, targetPositions) {
@@ -488,7 +274,7 @@ async function getRosterTransactionRecommendations(lookAheadDays = 7, targetPosi
488
274
  // Find transaction opportunities
489
275
  const recommendations = [];
490
276
  // 1. IMMEDIATE FIXES (injured players)
491
- const injuredActive = roster.roster.filter(p => p.status && p.status !== "" && !p.selected_position.includes("IR"));
277
+ const injuredActive = (roster.players ?? []).filter((p) => p.status && p.status !== "" && !p.selected_position.includes("IR"));
492
278
  for (const player of injuredActive) {
493
279
  recommendations.push({
494
280
  priority: "CRITICAL",
@@ -910,204 +696,159 @@ function findBenchUpgrades(roster, streaming) {
910
696
  // - getStreamingRecommendations() → StreamingAnalysis
911
697
  // ==========================================
912
698
  // Tool: Get Trending Players
913
- async function getTrendingPlayers(trendType = "add", count = 25) {
914
- const sortParam = trendType === "add" ? "AR" : "OR";
915
- const endpoint = `/league/nhl.l.${LEAGUE_ID}/players;status=A;sort=${sortParam};count=${count}`;
916
- const data = await yahooApiRequest(endpoint);
917
- const playersData = data.fantasy_content.league[1].players;
918
- const players = Object.keys(playersData)
919
- .filter(key => key !== 'count')
920
- .map(key => {
921
- const playerData = playersData[key].player[0];
922
- // Find attributes in the array using .find() to handle dynamic structure
923
- const playerId = playerData.find((item) => item.player_id)?.player_id;
924
- const name = playerData.find((item) => item.name)?.name?.full;
925
- const displayPosition = playerData.find((item) => item.display_position)?.display_position;
926
- const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
927
- const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
928
- return {
929
- player_id: playerId,
930
- name: name,
931
- position: displayPosition,
932
- team: team,
933
- percent_owned: percentOwned,
934
- };
935
- });
936
- return { trend_type: trendType, players };
937
- }
699
+ // Removed in v4: getTrendingPlayers required live fantasy-platform data
700
+ // (live matchup scoring, weekly results, or league ownership) that no
701
+ // public source exposes. See CHANGELOG for the full list.
938
702
  // Tool: Chirp Opponent
939
703
  async function chirpOpponent(chirpIntensity = 'savage', personalityMode = 'roast_master') {
940
- const data = await yahooApiRequest(`/team/nhl.l.${LEAGUE_ID}.t.${TEAM_ID}/matchups`);
941
- const matchups = data.fantasy_content.team[1].matchups;
942
- const currentMatchup = findCurrentMatchup(matchups);
943
- if (!currentMatchup) {
944
- return { message: "No active matchup — nobody to chirp right now." };
945
- }
946
- const matchupData = currentMatchup["0"];
947
- const teams = matchupData?.teams;
948
- if (!teams)
949
- return { message: "Could not read matchup teams." };
950
- const opponentTeamArray = teams['1']?.team?.[0];
951
- const opponentTeamKey = opponentTeamArray?.find((item) => item.team_key)?.team_key;
952
- const opponentName = opponentTeamArray?.find((item) => item.name)?.name || 'Unknown';
953
- if (!opponentTeamKey)
954
- return { message: "Could not find opponent team key." };
955
- const rosterData = await yahooApiRequest(`/team/${opponentTeamKey}/roster`);
956
- const rawPlayers = rosterData.fantasy_content.team[1].roster["0"].players;
957
- const playerKeys = Object.keys(rawPlayers).filter(k => k !== 'count');
958
- const players = playerKeys.map(key => {
959
- const pd = rawPlayers[key].player[0];
960
- const posData = rawPlayers[key].player[1];
961
- return {
962
- name: pd.find((item) => item.name)?.name?.full || 'Unknown',
963
- position: pd.find((item) => item.display_position)?.display_position || '?',
964
- team: pd.find((item) => item.editorial_team_abbr)?.editorial_team_abbr || '?',
965
- status: pd.find((item) => item.status)?.status || '',
966
- selected_position: posData?.selected_position?.find((item) => item.position)?.position || 'BN',
967
- };
968
- });
969
- const injured = players.filter(p => p.status && p.status !== '');
970
- const onIR = players.filter(p => p.selected_position === 'IR');
971
- const onBench = players.filter(p => p.selected_position === 'BN');
972
- const injuredActive = injured.filter(p => p.selected_position !== 'IR' && p.selected_position !== 'BN');
973
- const activeCount = players.length - onBench.length - onIR.length;
974
- const style = CHIRP_STYLES[chirpIntensity] || CHIRP_STYLES['savage'];
975
- const personality = PERSONALITY_MODES[personalityMode] || PERSONALITY_MODES['roast_master'];
976
- const chirpLines = [];
977
- if (injured.length >= 3) {
978
- chirpLines.push(`${opponentName} is running a hospital roster with ${injured.length} banged-up players. They should rename the team to "The Walking Wounded."`);
979
- }
980
- else if (injured.length > 0) {
981
- chirpLines.push(`${injured.length} of ${opponentName}'s key players are dinged up. Couldn't happen to a nicer team.`);
982
- }
983
- if (injuredActive.length > 0) {
984
- chirpLines.push(`They've got ${injuredActive.length} injured player${injuredActive.length > 1 ? 's' : ''} still in active slots — not even using their IR correctly. Amateur hour.`);
985
- }
986
- if (onBench.length >= 5) {
987
- chirpLines.push(`${onBench.length} players collecting dust on the bench. That's not a fantasy team, that's a waiting room.`);
988
- }
989
- if (chirpLines.length === 0) {
990
- chirpLines.push(`${opponentName} looks healthy on paper — but paper doesn't win categories. Execution does. That's your edge.`);
991
- }
992
- const weaknesses = [
993
- ...(injured.length >= 3 ? [`${injured.length} players injured`] : []),
994
- ...(injuredActive.length > 0 ? [`${injuredActive.length} injured players in active slots`] : []),
995
- ...(onBench.length >= 5 ? [`Heavy bench (${onBench.length} players)`] : []),
996
- ];
704
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
705
+ const theirs = LEAGUE_DATA.getOpponentRoster();
706
+ if (!theirs)
707
+ return { error: NO_OPPONENT_MESSAGE };
708
+ const today = NhlScheduleService.today();
709
+ const weekEnd = NhlScheduleService.addDays(today, 6);
710
+ const players = theirs.players.map((p) => ({
711
+ name: p.name,
712
+ team: p.team,
713
+ position: p.position,
714
+ slot: p.selected_position,
715
+ points_last_season: p.stats?.points ?? null,
716
+ games_this_week: NHL_SCHEDULE.isAvailable()
717
+ ? NHL_SCHEDULE.countGamesInRange(p.team, today, weekEnd)
718
+ : null
719
+ }));
720
+ const onIr = players.filter(p => p.slot === 'IR');
721
+ const idle = players.filter(p => p.games_this_week === 0);
722
+ const light = players.filter(p => (p.games_this_week ?? 9) <= 2 && p.slot !== 'IR');
723
+ const totalGames = players.reduce((n, p) => n + (p.games_this_week ?? 0), 0);
724
+ const style = CHIRP_STYLES[chirpIntensity] ?? CHIRP_STYLES.standard;
725
+ const persona = PERSONALITY_MODES[personalityMode] ?? PERSONALITY_MODES.analytical;
726
+ const lines = [];
727
+ if (idle.length)
728
+ lines.push(`${idle.length} of their players don't play at all this week. Free real estate.`);
729
+ if (light.length)
730
+ lines.push(`${light.length} more are stuck on two games or fewer.`);
731
+ if (onIr.length)
732
+ lines.push(`${onIr.length} parked on IR — that roster is holding a hospital ward.`);
733
+ if (!lines.length)
734
+ lines.push(`${theirs.team_name} is actually well set up this week. Annoying, but true.`);
997
735
  return {
998
- opponent: opponentName,
999
- week: currentMatchup.week,
1000
- opponent_roster_summary: {
1001
- total_players: players.length,
1002
- active_players: activeCount,
1003
- bench_players: onBench.length,
1004
- ir_players: onIR.length,
1005
- injured_players: injured.length,
1006
- injured_active: injuredActive.length,
1007
- },
1008
- weaknesses: weaknesses.length > 0 ? weaknesses : ["No obvious weaknesses detected — they're running a clean roster."],
1009
- chirp: {
1010
- style: style.tone,
1011
- personality: personality.voice,
1012
- main_chirp: `${style.prefix} ${chirpLines.join(' ')} ${style.suffix}`,
1013
- ice_cold_truth: `❄️ Bottom line: know your enemy. ${opponentName} has gaps — your job is to exploit them.`,
1014
- },
1015
- opponent_roster: players,
736
+ opponent: theirs.team_name,
737
+ roster_size: players.length,
738
+ total_games_this_week: totalGames,
739
+ weaknesses: { idle_players: idle.map(p => p.name), light_schedule: light.map(p => p.name), on_ir: onIr.map(p => p.name) },
740
+ players,
741
+ chirp: lines.join(' '),
742
+ chirp_style: style?.tone,
743
+ personality: persona?.voice,
744
+ basis: 'Real NHL schedule for this week, plus the opponent roster you pasted.'
1016
745
  };
1017
746
  }
1018
747
  // Tool: Analyze Trade
1019
748
  async function analyzeTradeImpact(giving, receiving, chirpIntensity = 'standard') {
1020
- const statIdLabels = {
1021
- '1': 'G', '2': 'A', '3': '+/-', '4': 'PIM', '5': 'SOG',
1022
- '8': 'PPP', '31': 'W', '32': 'GAA', '33': 'SV%',
1023
- };
1024
- const categories = ['G', 'A', '+/-', 'PIM', 'SOG', 'PPP', 'W', 'GAA', 'SV%'];
1025
- const lowerIsBetter = new Set(['GAA']);
1026
- async function findPlayerStats(name) {
1027
- const searchData = await yahooApiRequest(`/league/nhl.l.${LEAGUE_ID}/players;search=${encodeURIComponent(name)};count=1`);
1028
- const playersData = searchData.fantasy_content.league[1].players;
1029
- const playerKeys = Object.keys(playersData).filter(k => k !== 'count');
1030
- if (playerKeys.length === 0)
1031
- return { name, found: false, stats: {}, position: '', team: '' };
1032
- const pd = playersData[playerKeys[0]].player[0];
1033
- const playerId = pd.find((item) => item.player_id)?.player_id;
1034
- const foundName = pd.find((item) => item.name)?.name?.full || name;
1035
- const position = pd.find((item) => item.display_position)?.display_position || '?';
1036
- const team = pd.find((item) => item.editorial_team_abbr)?.editorial_team_abbr || '?';
1037
- if (!playerId)
1038
- return { name, found: false, stats: {}, position, team };
1039
- const statsData = await yahooApiRequest(`/player/nhl.p.${playerId}/stats`);
1040
- const statsArray = statsData.fantasy_content.player[1]?.player_stats?.stats || [];
1041
- const stats = {};
1042
- statsArray.forEach((sw) => {
1043
- const stat = sw.stat;
1044
- if (stat && statIdLabels[stat.stat_id]) {
1045
- stats[statIdLabels[stat.stat_id]] = parseFloat(stat.value) || 0;
1046
- }
1047
- });
1048
- return { name: foundName, found: true, playerId, position, team, stats };
749
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
750
+ if (!NHL_STATS.isAvailable()) {
751
+ return { error: 'NHL player data unavailable', reason: NHL_STATS.getUnavailableReason() };
1049
752
  }
1050
- const [givingPlayers, receivingPlayers] = await Promise.all([
1051
- Promise.all(giving.map(name => findPlayerStats(name))),
1052
- Promise.all(receiving.map(name => findPlayerStats(name))),
1053
- ]);
1054
- function sumStats(players) {
753
+ const categories = ['G', 'A', 'P', '+/-', 'PIM', 'SOG', 'PPG', 'W', 'GAA', 'SV%'];
754
+ const lowerIsBetter = new Set(['GAA']);
755
+ const resolveSide = (names) => names.map(n => {
756
+ const r = NHL_STATS.resolve(n);
757
+ return {
758
+ input: n,
759
+ found: Boolean(r.player),
760
+ name: r.player?.name ?? n,
761
+ team: r.player?.team ?? '?',
762
+ position: r.player?.position ?? '?',
763
+ stats: r.player?.stats ?? null,
764
+ ...(r.ambiguous ? { candidates: r.ambiguous.map(p => `${p.name} (${p.team} ${p.position})`) } : {}),
765
+ ...(r.player ? {} : { reason: r.reason })
766
+ };
767
+ });
768
+ const sum = (side) => {
1055
769
  const totals = {};
1056
- players.filter(p => p.found).forEach(p => {
1057
- categories.forEach(cat => {
1058
- totals[cat] = (totals[cat] || 0) + (p.stats[cat] || 0);
1059
- });
1060
- });
1061
- return totals;
1062
- }
1063
- const givingStats = sumStats(givingPlayers);
1064
- const receivingStats = sumStats(receivingPlayers);
1065
- let receivingWins = 0;
1066
- let givingWins = 0;
1067
- const categoryBreakdown = categories
1068
- .filter(cat => (givingStats[cat] || 0) !== 0 || (receivingStats[cat] || 0) !== 0)
1069
- .map(cat => {
1070
- const gVal = givingStats[cat] || 0;
1071
- const rVal = receivingStats[cat] || 0;
1072
- const lowerBetter = lowerIsBetter.has(cat);
1073
- let winner;
1074
- if (Math.abs(rVal - gVal) < 0.01) {
1075
- winner = 'push';
770
+ const rates = {};
771
+ for (const p of side.filter(x => x.found && x.stats)) {
772
+ const s = p.stats;
773
+ totals.G = (totals.G ?? 0) + (s.goals ?? 0);
774
+ totals.A = (totals.A ?? 0) + (s.assists ?? 0);
775
+ totals.P = (totals.P ?? 0) + (s.points ?? 0);
776
+ totals['+/-'] = (totals['+/-'] ?? 0) + (s.plus_minus ?? 0);
777
+ totals.PIM = (totals.PIM ?? 0) + (s.penalty_minutes ?? 0);
778
+ totals.SOG = (totals.SOG ?? 0) + (s.shots ?? 0);
779
+ totals.PPG = (totals.PPG ?? 0) + (s.power_play_goals ?? 0);
780
+ totals.W = (totals.W ?? 0) + (s.wins ?? 0);
781
+ // Rate stats average rather than sum, and only across goalies who have them.
782
+ if (s.goals_against_average !== undefined)
783
+ (rates.GAA ??= []).push(s.goals_against_average);
784
+ if (s.save_percentage !== undefined)
785
+ (rates['SV%'] ??= []).push(s.save_percentage);
1076
786
  }
1077
- else if (lowerBetter ? rVal < gVal : rVal > gVal) {
1078
- winner = 'receiving';
1079
- receivingWins++;
787
+ for (const [cat, values] of Object.entries(rates)) {
788
+ if (values.length)
789
+ totals[cat] = values.reduce((a, b) => a + b, 0) / values.length;
1080
790
  }
1081
- else {
1082
- winner = 'giving';
1083
- givingWins++;
1084
- }
1085
- return { category: cat, giving: gVal, receiving: rVal, winner };
1086
- });
1087
- const verdict = receivingWins > givingWins ? 'ACCEPT' : receivingWins < givingWins ? 'DECLINE' : 'PUSH';
1088
- const style = CHIRP_STYLES[chirpIntensity] || CHIRP_STYLES['standard'];
1089
- const chirpText = verdict === 'ACCEPT'
1090
- ? `${style.prefix} you're gaining ${receivingWins} categories vs ${givingWins}. That's not a trade — that's a heist. Pull the trigger. ${style.suffix}`
1091
- : verdict === 'DECLINE'
1092
- ? `${style.prefix} you'd be handing away ${givingWins} categories for only ${receivingWins} back. Your opponent is praying you say yes. Hard pass. ${style.suffix}`
1093
- : `${style.prefix} dead even at ${receivingWins} categories each. Unless this fixes a positional need, don't bother. ${style.suffix}`;
1094
- const iceColdTruth = verdict === 'ACCEPT'
1095
- ? '❄️ ICE Cold Truth: Smart managers see value before their opponent does. This is that moment.'
1096
- : verdict === 'DECLINE'
1097
- ? '🔥 Savage Reality: This trade has "I got played" written all over it. Close the chat.'
1098
- : '💡 Real Talk: Push trades only make sense when they fix a roster hole. Otherwise, pass.';
791
+ return totals;
792
+ };
793
+ const givingPlayers = resolveSide(giving);
794
+ const receivingPlayers = resolveSide(receiving);
795
+ const out = sum(givingPlayers);
796
+ const inn = sum(receivingPlayers);
797
+ let wins = 0, losses = 0;
798
+ const breakdown = categories.map(cat => {
799
+ const g = out[cat], r = inn[cat];
800
+ if (g === undefined && r === undefined)
801
+ return null;
802
+ const gv = g ?? 0, rv = r ?? 0;
803
+ const better = lowerIsBetter.has(cat) ? rv < gv : rv > gv;
804
+ const same = Math.abs(rv - gv) < 1e-9;
805
+ if (!same)
806
+ better ? wins++ : losses++;
807
+ const round = (n) => Math.round(n * 1000) / 1000;
808
+ return { category: cat, giving_up: round(gv), receiving: round(rv),
809
+ net: round(rv - gv), verdict: same ? 'PUSH' : better ? 'GAIN' : 'LOSS' };
810
+ }).filter(Boolean);
811
+ const verdict = wins > losses ? 'ACCEPT' : wins < losses ? 'DECLINE' : 'PUSH';
812
+ const unresolved = [...givingPlayers, ...receivingPlayers].filter(p => !p.found);
1099
813
  return {
1100
- trade: { giving, receiving },
1101
- players: { giving: givingPlayers, receiving: receivingPlayers },
1102
- category_breakdown: categoryBreakdown,
1103
- summary: { receiving_wins: receivingWins, giving_wins: givingWins, verdict },
1104
- chirp: { verdict_chirp: chirpText, ice_cold_truth: iceColdTruth },
814
+ giving: givingPlayers,
815
+ receiving: receivingPlayers,
816
+ category_breakdown: breakdown,
817
+ categories_gained: wins,
818
+ categories_lost: losses,
819
+ verdict,
820
+ chirp: verdict === 'ACCEPT'
821
+ ? `You win ${wins} categories to ${losses}. Take it before they think twice.`
822
+ : verdict === 'DECLINE'
823
+ ? `You lose ${losses} categories to ${wins}. That's not a trade, that's a donation.`
824
+ : `Dead even at ${wins}-${losses}. Decide on need, not numbers.`,
825
+ ...(unresolved.length ? { unresolved } : {}),
826
+ basis: `Last full season totals (${NHL_STATS.getSeasons().stats}) from the NHL public API. ` +
827
+ 'GAA and SV% are averaged across goalies; counting stats are summed. ' +
828
+ 'This measures past production, not your league\'s scoring settings.',
829
+ chirp_intensity: chirpIntensity
1105
830
  };
1106
831
  }
832
+ /**
833
+ * Version reported in the MCP handshake.
834
+ *
835
+ * Read from package.json rather than hardcoded — this string had drifted to
836
+ * 3.0.0 while the package was on 3.2.0, so clients were told the wrong version.
837
+ */
838
+ function readPackageVersion() {
839
+ try {
840
+ const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
841
+ const pkg = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
842
+ return pkg.version ?? "0.0.0";
843
+ }
844
+ catch {
845
+ return "0.0.0";
846
+ }
847
+ }
1107
848
  // Initialize MCP Server
1108
849
  const server = new Server({
1109
850
  name: "semantic-chirp-intelligence-mcp",
1110
- version: "3.0.0",
851
+ version: readPackageVersion(),
1111
852
  }, {
1112
853
  capabilities: {
1113
854
  tools: {},
@@ -1133,14 +874,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1133
874
  properties: {},
1134
875
  },
1135
876
  },
1136
- {
1137
- name: "get_current_matchup",
1138
- description: "Get information about your current week's matchup",
1139
- inputSchema: {
1140
- type: "object",
1141
- properties: {},
1142
- },
1143
- },
1144
877
  {
1145
878
  name: "search_players",
1146
879
  description: "Search for available players (free agents) by position. Returns top available players.",
@@ -1173,14 +906,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1173
906
  required: ["player_id"],
1174
907
  },
1175
908
  },
1176
- {
1177
- name: "get_weekly_stats",
1178
- description: "Get your team's current week statistics and compare with opponent",
1179
- inputSchema: {
1180
- type: "object",
1181
- properties: {},
1182
- },
1183
- },
1184
909
  {
1185
910
  name: "compare_matchup",
1186
911
  description: "Get detailed category-by-category comparison with your current opponent",
@@ -1197,40 +922,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1197
922
  properties: {},
1198
923
  },
1199
924
  },
1200
- {
1201
- name: "get_trending_players",
1202
- description: "Get trending players (most added or most owned) to identify hot pickups",
1203
- inputSchema: {
1204
- type: "object",
1205
- properties: {
1206
- trend_type: {
1207
- type: "string",
1208
- description: "Type of trending: 'add' for most added, 'own' for most owned",
1209
- enum: ["add", "own"],
1210
- default: "add",
1211
- },
1212
- count: {
1213
- type: "number",
1214
- description: "Number of players to return (default 25)",
1215
- default: 25,
1216
- },
1217
- },
1218
- },
1219
- },
1220
- {
1221
- name: "debug_api_call",
1222
- description: "Debug tool to see raw Yahoo API responses for troubleshooting",
1223
- inputSchema: {
1224
- type: "object",
1225
- properties: {
1226
- endpoint: {
1227
- type: "string",
1228
- description: "API endpoint to call (e.g., '/team/nhl.l.{LEAGUE_ID}.t.{TEAM_ID}/roster')",
1229
- },
1230
- },
1231
- required: ["endpoint"],
1232
- },
1233
- },
1234
925
  {
1235
926
  name: "get_streaming_recommendations",
1236
927
  description: "Get AI-powered streaming recommendations based on team schedules, player trends, and ownership. Identifies players on teams with favorable schedules (more games remaining this week) for optimal waiver pickups.",
@@ -1322,14 +1013,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1322
1013
  }
1323
1014
  },
1324
1015
  {
1325
- name: SEMANTIC_PLAYER_COMPARISON.name,
1326
- description: `${SEMANTIC_PLAYER_COMPARISON.description} - Auto-configured from semantic intent!`,
1327
- inputSchema: getPlayerComparisonInputSchema()
1328
- },
1329
- {
1330
- name: SEMANTIC_BREAKOUT_ANALYSIS.name,
1331
- description: `${SEMANTIC_BREAKOUT_ANALYSIS.description} - 🏒 Comprehensive breakout analysis with data-driven scoring (40% recent, 30% projections, 20% opportunity, 10% risk)`,
1332
- inputSchema: getBreakoutAnalysisInputSchema()
1016
+ name: "analyze_breakout_players",
1017
+ description: "📈 Find breakout candidates among NHL players not on the rosters you have provided, scored on real season production, opportunity and risk. Availability in your league is league-private and cannot be determined here — treat these as candidates to check.",
1018
+ inputSchema: {
1019
+ type: "object",
1020
+ properties: {
1021
+ position_filter: { type: "array", items: { type: "string" }, description: "Limit to positions, e.g. [\"C\", \"D\"]" },
1022
+ breakout_age_max: { type: "number", description: "Maximum age for a breakout candidate (default 26)" },
1023
+ min_score: { type: "number", description: "Minimum breakout score to include" },
1024
+ max_results: { type: "number", description: "How many candidates to return (default 10)" },
1025
+ ...baseChirpSchema
1026
+ }
1027
+ }
1333
1028
  },
1334
1029
  {
1335
1030
  name: "analyze_weekend_streams",
@@ -1413,6 +1108,123 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1413
1108
  required: ["giving", "receiving"],
1414
1109
  },
1415
1110
  },
1111
+ {
1112
+ name: "set_roster",
1113
+ description: "📋 Paste your team's roster to teach CHIRP who you own. Works with text copied from any fantasy platform — Yahoo, ESPN, Sleeper, a spreadsheet, or just a list of names. Player names are resolved against live NHL rosters, so team and position fill themselves in. Anything that can't be resolved to exactly one player is reported back rather than guessed. No account or API key needed.",
1114
+ inputSchema: {
1115
+ type: "object",
1116
+ properties: {
1117
+ roster_text: {
1118
+ type: "string",
1119
+ description: "The pasted roster. One player per line, in whatever shape you copied it — \"Auston Matthews\", \"MATTHEWS, Auston\", or a full row like \"C Auston Matthews TOR - C Q\". BN and IR slots are preserved if present."
1120
+ },
1121
+ team_name: {
1122
+ type: "string",
1123
+ description: "What to call this team (default: \"My Team\")"
1124
+ }
1125
+ },
1126
+ required: ["roster_text"]
1127
+ }
1128
+ },
1129
+ {
1130
+ name: "set_opponent_roster",
1131
+ description: "📋 Paste your weekly opponent's roster, so head-to-head tools (games-in-hand, matchup comparison, opponent scouting) can work without a league account. Same forgiving format as set_roster.",
1132
+ inputSchema: {
1133
+ type: "object",
1134
+ properties: {
1135
+ roster_text: { type: "string", description: "The pasted opponent roster, one player per line" },
1136
+ team_name: { type: "string", description: "Opponent's team name (default: \"Opponent\")" }
1137
+ },
1138
+ required: ["roster_text"]
1139
+ }
1140
+ },
1141
+ {
1142
+ name: "set_standings",
1143
+ description: "📊 Paste your league standings to give CHIRP league context. Extracts rank, team name, record and points from rows like \"1. TeamDestroyersz 8-2-1 142 pts\".",
1144
+ inputSchema: {
1145
+ type: "object",
1146
+ properties: {
1147
+ standings_text: { type: "string", description: "The pasted standings, one team per line" }
1148
+ },
1149
+ required: ["standings_text"]
1150
+ }
1151
+ },
1152
+ {
1153
+ name: "show_stored_data",
1154
+ description: "🗂️ Show what CHIRP currently knows about your league — stored roster, opponent roster and standings, with when each was last updated. Use `clear` to forget one of them.",
1155
+ inputSchema: {
1156
+ type: "object",
1157
+ properties: {
1158
+ clear: {
1159
+ type: "string",
1160
+ enum: ["roster", "opponent", "standings"],
1161
+ description: "Optionally forget one stored item instead of showing everything"
1162
+ }
1163
+ }
1164
+ }
1165
+ },
1166
+ {
1167
+ name: "schedule_value",
1168
+ description: "🗓️ Rate all 32 NHL clubs on what their schedule is worth to a fantasy roster — total games, four-game weeks, light weeks, back-to-backs, and games played during YOUR league's playoff weeks (read from your Yahoo league settings, not guessed). The draft tiebreaker when two players are close.",
1169
+ inputSchema: {
1170
+ type: "object",
1171
+ properties: {
1172
+ teams: {
1173
+ type: "array",
1174
+ items: { type: "string" },
1175
+ description: "Limit to specific clubs (NHL or Yahoo abbreviations, e.g. [\"TOR\", \"SJ\"]). Omit to rate all 32."
1176
+ },
1177
+ playoff_start_week: {
1178
+ type: "number",
1179
+ description: "Override the fantasy playoff start week. Defaults to playoff_start_week from your Yahoo league settings."
1180
+ },
1181
+ playoff_end_week: {
1182
+ type: "number",
1183
+ description: "Override the final fantasy week. Defaults to your league's end_week."
1184
+ },
1185
+ top_n: {
1186
+ type: "number",
1187
+ description: "How many clubs to highlight (default 8)",
1188
+ default: 8
1189
+ },
1190
+ ...baseChirpSchema
1191
+ }
1192
+ }
1193
+ },
1194
+ {
1195
+ name: "chirp_draft_pick",
1196
+ description: "❄️ ICE at the draft table — with a pick on the clock, ranks who to take against YOUR draft: who is already gone, what your roster still needs, Yahoo's ADP (so 'value' means the market is wrong here), and each club's schedule during your league's playoff weeks. Pass already_drafted if Yahoo's draft results lag your live draft.",
1197
+ inputSchema: {
1198
+ type: "object",
1199
+ properties: {
1200
+ pick_number: {
1201
+ type: "number",
1202
+ description: "The pick currently on the clock. Inferred from Yahoo draft results if omitted."
1203
+ },
1204
+ already_drafted: {
1205
+ type: "array",
1206
+ items: { type: "string" },
1207
+ description: "Player names already off the board. Merged with Yahoo's draft results — use this when Yahoo's API lags a fast live draft."
1208
+ },
1209
+ roster_needs: {
1210
+ type: "array",
1211
+ items: { type: "string" },
1212
+ description: "Positions you still need, e.g. [\"RW\", \"G\"]. Inferred from your roster if omitted."
1213
+ },
1214
+ max_results: {
1215
+ type: "number",
1216
+ description: "How many candidates to return (default 8)",
1217
+ default: 8
1218
+ },
1219
+ pool_size: {
1220
+ type: "number",
1221
+ description: "How deep to pull the player pool (default 150, max 300)",
1222
+ default: 150
1223
+ },
1224
+ ...baseChirpSchema
1225
+ }
1226
+ }
1227
+ },
1416
1228
  ],
1417
1229
  };
1418
1230
  });
@@ -1433,12 +1245,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1433
1245
  content: [{ type: "text", text: JSON.stringify(standings, null, 2) }],
1434
1246
  };
1435
1247
  }
1436
- case "get_current_matchup": {
1437
- const matchup = await getCurrentMatchup();
1438
- return {
1439
- content: [{ type: "text", text: JSON.stringify(matchup, null, 2) }],
1440
- };
1441
- }
1442
1248
  case "search_players": {
1443
1249
  const position = args?.position;
1444
1250
  const count = args?.count || 25;
@@ -1457,12 +1263,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1457
1263
  content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
1458
1264
  };
1459
1265
  }
1460
- case "get_weekly_stats": {
1461
- const stats = await getWeeklyStats();
1462
- return {
1463
- content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
1464
- };
1465
- }
1466
1266
  case "compare_matchup": {
1467
1267
  const comparison = await compareMatchup();
1468
1268
  return {
@@ -1483,24 +1283,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1483
1283
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1484
1284
  };
1485
1285
  }
1486
- case "get_trending_players": {
1487
- const trendType = args?.trend_type || "add";
1488
- const count = args?.count || 25;
1489
- const trending = await getTrendingPlayers(trendType, count);
1490
- return {
1491
- content: [{ type: "text", text: JSON.stringify(trending, null, 2) }],
1492
- };
1493
- }
1494
- case "debug_api_call": {
1495
- const endpoint = args?.endpoint;
1496
- if (!endpoint) {
1497
- throw new Error("endpoint is required");
1498
- }
1499
- const rawData = await yahooApiRequest(endpoint);
1500
- return {
1501
- content: [{ type: "text", text: JSON.stringify(rawData, null, 2) }],
1502
- };
1503
- }
1504
1286
  case "get_streaming_recommendations": {
1505
1287
  // 🎯 Template Method Pattern: Use StreamingAnalysis class
1506
1288
  const semanticContract = {
@@ -1659,27 +1441,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1659
1441
  content: [{ type: "text", text: JSON.stringify(reportData, null, 2) }]
1660
1442
  };
1661
1443
  }
1662
- case "semantic_player_comparison": {
1663
- try {
1664
- const result = await executePlayerComparison(args, getPlayerStats, searchPlayers);
1665
- return {
1666
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1667
- };
1668
- }
1669
- catch (error) {
1670
- const errorMessage = error instanceof Error ? error.message : String(error);
1671
- return {
1672
- content: [{ type: "text", text: JSON.stringify({
1673
- error: errorMessage,
1674
- note: "This is an experimental semantic intent-driven tool"
1675
- }, null, 2) }],
1676
- isError: true
1677
- };
1678
- }
1679
- }
1680
1444
  case "analyze_breakout_players": {
1681
1445
  try {
1682
- const result = await executeBreakoutAnalysis(args, breakoutAnalysis);
1446
+ const result = await breakoutAnalysis.executeAnalysis({
1447
+ position_filter: args?.position_filter,
1448
+ breakout_age_max: args?.breakout_age_max,
1449
+ min_score: args?.min_score,
1450
+ max_results: args?.max_results
1451
+ }, {
1452
+ chirp_intensity: args?.chirp_intensity || 'standard',
1453
+ personality_mode: args?.personality_mode || 'analytical',
1454
+ enable_chirp: args?.enable_chirp !== false,
1455
+ semantic_intent: 'user_requested'
1456
+ });
1683
1457
  return {
1684
1458
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1685
1459
  };
@@ -1745,6 +1519,140 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1745
1519
  const result = await analyzeTradeImpact(giving, receiving, intensity);
1746
1520
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1747
1521
  }
1522
+ case "set_roster":
1523
+ case "set_opponent_roster": {
1524
+ const key = name === "set_roster" ? "roster" : "opponent";
1525
+ const text = args?.roster_text;
1526
+ if (!text || !text.trim()) {
1527
+ throw new Error("roster_text is required — paste your roster, one player per line.");
1528
+ }
1529
+ await NHL_STATS.load();
1530
+ if (!NHL_STATS.isAvailable()) {
1531
+ return { content: [{ type: "text", text: JSON.stringify({
1532
+ error: "NHL player data unavailable",
1533
+ reason: NHL_STATS.getUnavailableReason(),
1534
+ note: "Names are resolved against live NHL rosters; retry shortly."
1535
+ }, null, 2) }], isError: true };
1536
+ }
1537
+ const report = ROSTER_STORE.parseRoster(text);
1538
+ const label = args?.team_name || (key === "roster" ? "My Team" : "Opponent");
1539
+ if (report.resolved.length === 0) {
1540
+ return { content: [{ type: "text", text: JSON.stringify({
1541
+ saved: false,
1542
+ reason: "No player names could be resolved from that text.",
1543
+ lines_read: report.lines_read,
1544
+ unresolved: report.unresolved,
1545
+ ambiguous: report.ambiguous
1546
+ }, null, 2) }], isError: true };
1547
+ }
1548
+ const stored = ROSTER_STORE.saveRoster(key, report.resolved, label);
1549
+ return { content: [{ type: "text", text: JSON.stringify({
1550
+ saved: true,
1551
+ team: label,
1552
+ players_resolved: report.resolved.length,
1553
+ lines_read: report.lines_read,
1554
+ roster: stored.players,
1555
+ // Surfaced, never silently dropped - the user decides what to do.
1556
+ needs_attention: {
1557
+ unresolved: report.unresolved,
1558
+ ambiguous: report.ambiguous
1559
+ },
1560
+ note: report.unresolved.length || report.ambiguous.length
1561
+ ? "Some lines could not be matched to exactly one NHL player. Re-run set_roster with corrected names to include them."
1562
+ : "All lines resolved.",
1563
+ data_source: `NHL public API (rosters ${NHL_STATS.getSeasons().roster}, stats ${NHL_STATS.getSeasons().stats})`
1564
+ }, null, 2) }] };
1565
+ }
1566
+ case "set_standings": {
1567
+ const text = args?.standings_text;
1568
+ if (!text || !text.trim()) {
1569
+ throw new Error("standings_text is required — paste your league standings, one team per line.");
1570
+ }
1571
+ const rows = ROSTER_STORE.parseStandings(text);
1572
+ if (rows.length === 0) {
1573
+ return { content: [{ type: "text", text: JSON.stringify({
1574
+ saved: false,
1575
+ reason: "No standings rows could be read from that text."
1576
+ }, null, 2) }], isError: true };
1577
+ }
1578
+ ROSTER_STORE.saveStandings(rows);
1579
+ return { content: [{ type: "text", text: JSON.stringify({
1580
+ saved: true, teams: rows.length, standings: rows
1581
+ }, null, 2) }] };
1582
+ }
1583
+ case "show_stored_data": {
1584
+ const clearKey = args?.clear;
1585
+ if (clearKey) {
1586
+ const removed = ROSTER_STORE.clear(clearKey);
1587
+ return { content: [{ type: "text", text: JSON.stringify({
1588
+ cleared: clearKey, existed: removed
1589
+ }, null, 2) }] };
1590
+ }
1591
+ const roster = ROSTER_STORE.getRoster('roster');
1592
+ const opponent = ROSTER_STORE.getRoster('opponent');
1593
+ const standings = ROSTER_STORE.getStandings();
1594
+ return { content: [{ type: "text", text: JSON.stringify({
1595
+ roster: roster ?? "not set — paste yours with set_roster",
1596
+ opponent: opponent ?? "not set — paste one with set_opponent_roster",
1597
+ standings: standings ?? "not set — paste them with set_standings",
1598
+ storage: ROSTER_STORE.getDataDir()
1599
+ }, null, 2) }] };
1600
+ }
1601
+ case "schedule_value": {
1602
+ try {
1603
+ const semanticContract = {
1604
+ chirp_intensity: args?.chirp_intensity || 'standard',
1605
+ personality_mode: args?.personality_mode || 'analytical',
1606
+ enable_chirp: args?.enable_chirp !== false,
1607
+ semantic_intent: 'user_requested'
1608
+ };
1609
+ const result = await scheduleValueAnalysis.executeAnalysis({
1610
+ teams: args?.teams,
1611
+ playoff_start_week: args?.playoff_start_week,
1612
+ playoff_end_week: args?.playoff_end_week,
1613
+ top_n: args?.top_n
1614
+ }, semanticContract);
1615
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1616
+ }
1617
+ catch (error) {
1618
+ const errorMessage = error instanceof Error ? error.message : String(error);
1619
+ return {
1620
+ content: [{ type: "text", text: JSON.stringify({
1621
+ error: errorMessage,
1622
+ note: "Schedule value analysis failed - rates NHL club schedules against your league's playoff weeks"
1623
+ }, null, 2) }],
1624
+ isError: true
1625
+ };
1626
+ }
1627
+ }
1628
+ case "chirp_draft_pick": {
1629
+ try {
1630
+ const semanticContract = {
1631
+ chirp_intensity: args?.chirp_intensity || 'ice_cold',
1632
+ personality_mode: args?.personality_mode || 'championship_coach',
1633
+ enable_chirp: args?.enable_chirp !== false,
1634
+ semantic_intent: 'user_requested'
1635
+ };
1636
+ const result = await draftPickAnalysis.executeAnalysis({
1637
+ pick_number: args?.pick_number,
1638
+ already_drafted: args?.already_drafted,
1639
+ roster_needs: args?.roster_needs,
1640
+ max_results: args?.max_results,
1641
+ pool_size: args?.pool_size
1642
+ }, semanticContract);
1643
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1644
+ }
1645
+ catch (error) {
1646
+ const errorMessage = error instanceof Error ? error.message : String(error);
1647
+ return {
1648
+ content: [{ type: "text", text: JSON.stringify({
1649
+ error: errorMessage,
1650
+ note: "Draft pick analysis failed - pass already_drafted explicitly if Yahoo draft results are unavailable"
1651
+ }, null, 2) }],
1652
+ isError: true
1653
+ };
1654
+ }
1655
+ }
1748
1656
  default:
1749
1657
  throw new Error(`Unknown tool: ${name}`);
1750
1658
  }
@@ -1760,6 +1668,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1760
1668
  async function main() {
1761
1669
  const transport = new StdioServerTransport();
1762
1670
  await server.connect(transport);
1763
- console.error("🏒❄️ Semantic Chirp Intelligence MCP v3.0 - ICE is ON! (Official API)");
1671
+ console.error(`🏒❄️ Semantic Chirp Intelligence MCP v${readPackageVersion()} - ICE is ON! (Real schedule, real stats)`);
1764
1672
  }
1765
1673
  main().catch(console.error);