@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/.chirp-data/opponent.json +24 -0
- package/.chirp-data/roster.json +34 -0
- package/.chirp-data/standings.json +17 -0
- package/.nhl-schedule-cache/20262027.json +1 -0
- package/.nhl-schedule-cache/players-20262027-20252026.json +1 -0
- package/README.md +129 -79
- package/build/analyses/BreakoutAnalysis.js +25 -36
- package/build/analyses/DraftPickAnalysis.js +431 -0
- package/build/analyses/GamesInHandAnalysis.js +52 -121
- package/build/analyses/IceAnalysis.js +66 -57
- package/build/analyses/LineupAnalysis.js +26 -46
- package/build/analyses/ScheduleValueAnalysis.js +279 -0
- package/build/analyses/StreamingAnalysis.js +40 -76
- package/build/analyses/WeekendStreamAnalysis.js +130 -66
- package/build/config/tool-metadata.js +54 -0
- package/build/domain/nhl-teams.js +67 -0
- package/build/index.js +561 -653
- package/build/services/LeagueDataService.js +130 -0
- package/build/services/NhlScheduleService.js +357 -0
- package/build/services/NhlStatsService.js +297 -0
- package/build/services/RosterStore.js +233 -0
- package/package.json +12 -10
- package/scripts/preflight.mjs +67 -0
- package/scripts/smoke.mjs +92 -0
- package/.env.example +0 -8
- package/authenticate.js +0 -207
- package/build/experimental/semantic-breakout-tool.js +0 -188
- package/build/experimental/semantic-intent-parser.js +0 -222
- package/build/experimental/semantic-tool-integration.js +0 -146
- package/build/experimental/test-parser.js +0 -61
- package/build/services/YahooApiClient.js +0 -309
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
|
-
|
|
35
|
-
|
|
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(
|
|
72
|
-
const gamesInHandAnalysis = new GamesInHandAnalysis(
|
|
73
|
-
const streamingAnalysis = new StreamingAnalysis(
|
|
74
|
-
const lineupAnalysis = new LineupAnalysis(
|
|
75
|
-
const breakoutAnalysis = new BreakoutAnalysis(
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
const
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
142
|
+
await NHL_STATS.load();
|
|
143
|
+
if (!NHL_STATS.isAvailable()) {
|
|
144
|
+
return { error: 'NHL player data unavailable', reason: NHL_STATS.getUnavailableReason() };
|
|
356
145
|
}
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
398
|
-
async function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
const
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
if (!
|
|
446
|
-
return {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
247
|
+
const won = categories.filter(c => c.edge === 'YOU').length;
|
|
468
248
|
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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.
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
const
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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:
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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 [
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
category_breakdown:
|
|
1103
|
-
|
|
1104
|
-
|
|
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:
|
|
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:
|
|
1326
|
-
description:
|
|
1327
|
-
inputSchema:
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
|
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(
|
|
1671
|
+
console.error(`🏒❄️ Semantic Chirp Intelligence MCP v${readPackageVersion()} - ICE is ON! (Real schedule, real stats)`);
|
|
1764
1672
|
}
|
|
1765
1673
|
main().catch(console.error);
|