@semanticintent/semantic-chirp-intelligence-mcp 4.0.4 → 4.1.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 +1 -1
- package/.chirp-data/roster.json +1 -1
- package/.chirp-data/standings.json +1 -1
- package/.nhl-schedule-cache/players-v2-20262027-20252026.json +1 -0
- package/build/analyses/DraftKitAnalysis.js +333 -0
- package/build/config/tool-metadata.js +12 -0
- package/build/index.js +59 -0
- package/build/services/NhlStatsService.js +26 -1
- package/package.json +1 -1
- package/scripts/smoke.mjs +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📋 Draft Kit Analysis — tiers, flags, and the schedule nobody else knows
|
|
3
|
+
*
|
|
4
|
+
* A conventional draft kit ranks players in the abstract: rankings, tiers,
|
|
5
|
+
* projections, ADP, sleepers. ChirpIQX cannot produce half of that — there is
|
|
6
|
+
* no public source of projections, draft-market consensus, line combinations
|
|
7
|
+
* or injuries, and inventing them would be the same sin as the constants and
|
|
8
|
+
* random numbers this codebase spent v3.2 removing.
|
|
9
|
+
*
|
|
10
|
+
* What it can do is the half that depends on facts: real production, real ages,
|
|
11
|
+
* and the real schedule — including the weeks *your* league plays its playoffs,
|
|
12
|
+
* which no published kit can know.
|
|
13
|
+
*
|
|
14
|
+
* So this tool works two ways, from one engine:
|
|
15
|
+
*
|
|
16
|
+
* • no `rankings` given — builds the board from last season's production
|
|
17
|
+
* • `rankings` pasted — keeps that order as the baseline and annotates it
|
|
18
|
+
*
|
|
19
|
+
* The second mode is the useful one: bring a kit you trust for projections,
|
|
20
|
+
* and let ChirpIQX overlay schedule and flags onto it.
|
|
21
|
+
*
|
|
22
|
+
* 🏛️ Rule 3 (Observable Anchoring): every figure names the season it came from,
|
|
23
|
+
* and the response states plainly what a draft kit normally contains that this
|
|
24
|
+
* one does not.
|
|
25
|
+
*/
|
|
26
|
+
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
27
|
+
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
28
|
+
import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
|
|
29
|
+
import { NHL_STATS, NhlStatsService } from '../services/NhlStatsService.js';
|
|
30
|
+
import { ROSTER_STORE } from '../services/RosterStore.js';
|
|
31
|
+
/** Positions a kit is organised by, in the order people draft them. */
|
|
32
|
+
const KIT_POSITIONS = ['C', 'LW', 'RW', 'D', 'G'];
|
|
33
|
+
/** NHL position codes map to the winger labels fantasy platforms use. */
|
|
34
|
+
const NHL_TO_FANTASY = { C: 'C', L: 'LW', R: 'RW', D: 'D', G: 'G' };
|
|
35
|
+
export class DraftKitAnalysis extends AnalysisTemplate {
|
|
36
|
+
constructor() {
|
|
37
|
+
super('draft_kit', 'draft_pick');
|
|
38
|
+
}
|
|
39
|
+
// ==========================================
|
|
40
|
+
// Hook 1: Fetch
|
|
41
|
+
// ==========================================
|
|
42
|
+
async fetchData(args) {
|
|
43
|
+
await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
// ==========================================
|
|
47
|
+
// Hook 2: Prepare
|
|
48
|
+
// ==========================================
|
|
49
|
+
async prepareData(rawData, args) {
|
|
50
|
+
const window = this.resolvePlayoffWindow(args);
|
|
51
|
+
// Two entry points, one engine.
|
|
52
|
+
const pasted = String(args.rankings ?? '').trim();
|
|
53
|
+
const source = pasted ? 'pasted rankings' : 'NHL production';
|
|
54
|
+
const { players, unresolved } = pasted
|
|
55
|
+
? this.fromPastedRankings(pasted)
|
|
56
|
+
: { players: this.fromProduction(), unresolved: [] };
|
|
57
|
+
return { kitPlayers: players, unresolved, window, source };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a pasted ranked list, preserving its order.
|
|
61
|
+
*
|
|
62
|
+
* The list's own ordering is the baseline rank — that is the whole point of
|
|
63
|
+
* the overlay mode. Someone else did the projection work; this adds what
|
|
64
|
+
* they could not know.
|
|
65
|
+
*/
|
|
66
|
+
fromPastedRankings(text) {
|
|
67
|
+
const report = ROSTER_STORE.parseRoster(text);
|
|
68
|
+
const players = report.resolved
|
|
69
|
+
.map(p => NHL_STATS.getById(p.player_id))
|
|
70
|
+
.filter((p) => p !== null);
|
|
71
|
+
return { players, unresolved: [...report.unresolved, ...report.ambiguous] };
|
|
72
|
+
}
|
|
73
|
+
/** Build a board from last season's production when no list is given. */
|
|
74
|
+
fromProduction() {
|
|
75
|
+
return NHL_STATS.getAll()
|
|
76
|
+
.filter(p => (p.stats?.games_played ?? 0) > 0)
|
|
77
|
+
.sort((a, b) => this.productionValue(b) - this.productionValue(a));
|
|
78
|
+
}
|
|
79
|
+
/** Skaters rank on points, goalies on wins — they are not comparable. */
|
|
80
|
+
productionValue(p) {
|
|
81
|
+
return p.position === 'G' ? (p.stats?.wins ?? 0) : (p.stats?.points ?? 0);
|
|
82
|
+
}
|
|
83
|
+
// ==========================================
|
|
84
|
+
// Hook 3: Analyze
|
|
85
|
+
// ==========================================
|
|
86
|
+
async analyzeData(data, args) {
|
|
87
|
+
const d = data;
|
|
88
|
+
const window = d.window;
|
|
89
|
+
const tierSize = Math.max(3, args.tier_size ?? 6);
|
|
90
|
+
const maxPerPosition = Math.max(5, args.max_per_position ?? 24);
|
|
91
|
+
const wanted = (args.positions?.length ? args.positions : KIT_POSITIONS)
|
|
92
|
+
.map(p => p.toUpperCase());
|
|
93
|
+
const annotated = d.kitPlayers.map((p, index) => this.annotate(p, index + 1, window));
|
|
94
|
+
// Tiers are per position, because "when does C dry up" is the question a
|
|
95
|
+
// draft kit exists to answer.
|
|
96
|
+
const byPosition = {};
|
|
97
|
+
for (const pos of wanted) {
|
|
98
|
+
const group = annotated.filter(p => p.position === pos).slice(0, maxPerPosition);
|
|
99
|
+
if (group.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
byPosition[pos] = {
|
|
102
|
+
count: group.length,
|
|
103
|
+
tiers: this.buildTiers(group, tierSize),
|
|
104
|
+
// Where the position stops being replaceable.
|
|
105
|
+
dries_up_after: group.length >= tierSize ? `tier ${Math.ceil(group.length / tierSize)}` : 'one tier'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
source: d.source,
|
|
110
|
+
stats_season: NHL_STATS.getSeasons().stats,
|
|
111
|
+
playoff_window: window,
|
|
112
|
+
unresolved: d.unresolved,
|
|
113
|
+
positions: byPosition,
|
|
114
|
+
signals: {
|
|
115
|
+
shooting_rebounds: this.shootingRebounds(annotated),
|
|
116
|
+
decline_risk: this.declineRisk(annotated),
|
|
117
|
+
playoff_schedule_winners: this.playoffWinners(annotated, window),
|
|
118
|
+
category_specialists: this.categorySpecialists(annotated)
|
|
119
|
+
},
|
|
120
|
+
cheat_sheet: this.cheatSheet(byPosition),
|
|
121
|
+
not_included: [
|
|
122
|
+
'Projections for the coming season — ChirpIQX reports the last completed season, it does not forecast',
|
|
123
|
+
'ADP or value-vs-market — there is no public source of draft-market consensus',
|
|
124
|
+
'Line combinations and power-play units — the NHL does not publish them',
|
|
125
|
+
'Injury status — no public feed',
|
|
126
|
+
'Whether a player is available in your league — ownership is league-private'
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
annotate(p, rank, window) {
|
|
131
|
+
const s = p.stats;
|
|
132
|
+
const gp = s?.games_played ?? 0;
|
|
133
|
+
const points = s?.points ?? 0;
|
|
134
|
+
const profile = NHL_SCHEDULE.isAvailable() ? NHL_SCHEDULE.getTeamProfile(p.team) : null;
|
|
135
|
+
const playoffGames = window?.resolved && NHL_SCHEDULE.isAvailable()
|
|
136
|
+
? NHL_SCHEDULE.countGamesInRange(p.team, window.start, window.end)
|
|
137
|
+
: null;
|
|
138
|
+
return {
|
|
139
|
+
rank,
|
|
140
|
+
player_id: p.player_id,
|
|
141
|
+
name: p.name,
|
|
142
|
+
team: p.team,
|
|
143
|
+
position: NHL_TO_FANTASY[p.position] ?? p.position,
|
|
144
|
+
age: NhlStatsService.ageOf(p),
|
|
145
|
+
games_played: gp,
|
|
146
|
+
points,
|
|
147
|
+
points_per_game: gp > 0 ? Number((points / gp).toFixed(2)) : 0,
|
|
148
|
+
playoff_games: playoffGames,
|
|
149
|
+
four_game_weeks: profile?.weeks_with_4_plus ?? null,
|
|
150
|
+
flags: this.flagsFor(p, playoffGames)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Short, human-readable notes that survive being read at speed. */
|
|
154
|
+
flagsFor(p, playoffGames) {
|
|
155
|
+
const flags = [];
|
|
156
|
+
const s = p.stats;
|
|
157
|
+
const age = NhlStatsService.ageOf(p);
|
|
158
|
+
const gp = s?.games_played ?? 0;
|
|
159
|
+
if (!s || gp < 20)
|
|
160
|
+
return flags;
|
|
161
|
+
if (p.position !== 'G') {
|
|
162
|
+
const shots = s.shots ?? 0;
|
|
163
|
+
const shPct = shots > 0 ? ((s.goals ?? 0) / shots) * 100 : 0;
|
|
164
|
+
if (age !== null && age <= 25 && shots >= 150 && shPct < 8) {
|
|
165
|
+
flags.push(`shooting ${shPct.toFixed(1)}% on ${shots} shots — volume without conversion`);
|
|
166
|
+
}
|
|
167
|
+
if (age !== null && age >= 33 && (s.time_on_ice_per_game ?? 0) / 60 >= 19) {
|
|
168
|
+
flags.push(`age ${age} on ${((s.time_on_ice_per_game ?? 0) / 60).toFixed(1)} min — usage may not hold`);
|
|
169
|
+
}
|
|
170
|
+
if ((s.penalty_minutes ?? 0) / gp >= 1.2) {
|
|
171
|
+
flags.push(`${((s.penalty_minutes ?? 0) / gp).toFixed(2)} PIM/gm`);
|
|
172
|
+
}
|
|
173
|
+
if ((s.shots ?? 0) / gp >= 3.2) {
|
|
174
|
+
flags.push(`${((s.shots ?? 0) / gp).toFixed(1)} shots/gm`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if ((s.save_percentage ?? 0) >= 0.915 && (s.wins ?? 0) < 28) {
|
|
178
|
+
flags.push(`${(s.save_percentage ?? 0).toFixed(3)} SV% on only ${s.wins} wins — rate beats the record`);
|
|
179
|
+
}
|
|
180
|
+
if (playoffGames !== null && playoffGames >= 11)
|
|
181
|
+
flags.push(`${playoffGames} playoff-window games`);
|
|
182
|
+
if (playoffGames !== null && playoffGames <= 8)
|
|
183
|
+
flags.push(`only ${playoffGames} playoff-window games`);
|
|
184
|
+
return flags;
|
|
185
|
+
}
|
|
186
|
+
buildTiers(group, tierSize) {
|
|
187
|
+
const tiers = [];
|
|
188
|
+
for (let i = 0; i < group.length; i += tierSize) {
|
|
189
|
+
const slice = group.slice(i, i + tierSize);
|
|
190
|
+
tiers.push({
|
|
191
|
+
tier: tiers.length + 1,
|
|
192
|
+
players: slice.map(p => ({
|
|
193
|
+
rank: p.rank,
|
|
194
|
+
name: p.name,
|
|
195
|
+
team: p.team,
|
|
196
|
+
age: p.age,
|
|
197
|
+
ppg: p.points_per_game,
|
|
198
|
+
playoff_games: p.playoff_games,
|
|
199
|
+
flags: p.flags
|
|
200
|
+
}))
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return tiers;
|
|
204
|
+
}
|
|
205
|
+
// ==========================================
|
|
206
|
+
// 🎯 Signals
|
|
207
|
+
// ==========================================
|
|
208
|
+
shootingRebounds(players) {
|
|
209
|
+
return players
|
|
210
|
+
.filter(p => p.flags.some(f => f.includes('volume without conversion')))
|
|
211
|
+
.slice(0, 8)
|
|
212
|
+
.map(p => ({ name: p.name, team: p.team, position: p.position, age: p.age, note: p.flags[0] }));
|
|
213
|
+
}
|
|
214
|
+
declineRisk(players) {
|
|
215
|
+
return players
|
|
216
|
+
.filter(p => p.flags.some(f => f.includes('usage may not hold')))
|
|
217
|
+
.slice(0, 8)
|
|
218
|
+
.map(p => ({ name: p.name, team: p.team, position: p.position, age: p.age, note: p.flags.find(f => f.includes('usage')) }));
|
|
219
|
+
}
|
|
220
|
+
playoffWinners(players, window) {
|
|
221
|
+
if (!window?.resolved)
|
|
222
|
+
return [];
|
|
223
|
+
return players
|
|
224
|
+
.filter(p => (p.playoff_games ?? 0) >= 11)
|
|
225
|
+
.sort((a, b) => b.points_per_game - a.points_per_game)
|
|
226
|
+
.slice(0, 10)
|
|
227
|
+
.map(p => ({
|
|
228
|
+
name: p.name, team: p.team, position: p.position,
|
|
229
|
+
ppg: p.points_per_game, playoff_games: p.playoff_games
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
categorySpecialists(players) {
|
|
233
|
+
const pick = (needle, limit = 5) => players
|
|
234
|
+
.filter(p => p.flags.some(f => f.includes(needle)))
|
|
235
|
+
.slice(0, limit)
|
|
236
|
+
.map(p => ({ name: p.name, team: p.team, position: p.position, note: p.flags.find(f => f.includes(needle)) }));
|
|
237
|
+
return {
|
|
238
|
+
penalty_minutes: pick('PIM/gm'),
|
|
239
|
+
shot_volume: pick('shots/gm'),
|
|
240
|
+
goalie_rate_over_record: pick('rate beats the record')
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** The condensed board — the format people actually take to a draft. */
|
|
244
|
+
cheatSheet(byPosition) {
|
|
245
|
+
const sheet = {};
|
|
246
|
+
for (const [pos, data] of Object.entries(byPosition)) {
|
|
247
|
+
sheet[pos] = data.tiers.map((t) => `T${t.tier}: ` + t.players
|
|
248
|
+
.map((p) => `${p.name}${p.playoff_games !== null ? ` (${p.playoff_games})` : ''}`)
|
|
249
|
+
.join(', '));
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
note: 'Numbers in brackets are games during your playoff window.',
|
|
253
|
+
board: sheet
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// ==========================================
|
|
257
|
+
// Hooks 4 & 5
|
|
258
|
+
// ==========================================
|
|
259
|
+
async generateChirp(analysisResults, semanticContract, data) {
|
|
260
|
+
const contract = this.mergeContractWithDefaults(semanticContract);
|
|
261
|
+
const enhanced = ChirpIntelligence.enhance(this.toolName, analysisResults, contract);
|
|
262
|
+
const winners = analysisResults.signals.playoff_schedule_winners;
|
|
263
|
+
const rebounds = analysisResults.signals.shooting_rebounds;
|
|
264
|
+
const parts = [];
|
|
265
|
+
parts.push(analysisResults.source === 'pasted rankings'
|
|
266
|
+
? 'Working from your list — the order is theirs, the schedule and flags are mine.'
|
|
267
|
+
: `Board built from ${analysisResults.stats_season} production.`);
|
|
268
|
+
if (winners?.length) {
|
|
269
|
+
parts.push(`${winners[0].name} is the best producer on an 11-game playoff club. That is your tiebreaker.`);
|
|
270
|
+
}
|
|
271
|
+
if (rebounds?.length) {
|
|
272
|
+
parts.push(`${rebounds[0].name} is shooting well under his volume — that usually corrects.`);
|
|
273
|
+
}
|
|
274
|
+
if (!analysisResults.playoff_window?.resolved) {
|
|
275
|
+
parts.push('Pass playoff_start_week and playoff_end_week and the schedule half of this becomes real.');
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
...enhanced,
|
|
279
|
+
chirp_intelligence: { ...enhanced.chirp_intelligence, analysis_chirp: parts.join(' ') }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async formatResponse(chirpEnhanced, data) {
|
|
283
|
+
const recommendations = (chirpEnhanced.signals?.playoff_schedule_winners ?? []).slice(0, 5).map((p, i) => ({
|
|
284
|
+
priority: i < 2 ? 'HIGH' : 'MEDIUM',
|
|
285
|
+
action: 'target',
|
|
286
|
+
reasoning: `${p.name} (${p.team} ${p.position}) — ${p.ppg} P/gm and ${p.playoff_games} games in your playoff window`
|
|
287
|
+
}));
|
|
288
|
+
const analysisInsights = {
|
|
289
|
+
source: chirpEnhanced.source,
|
|
290
|
+
stats_season: chirpEnhanced.stats_season,
|
|
291
|
+
schedule_source: NHL_SCHEDULE.isAvailable()
|
|
292
|
+
? `NHL public API (season ${NHL_SCHEDULE.getSeason()})`
|
|
293
|
+
: `UNAVAILABLE - ${NHL_SCHEDULE.getUnavailableReason()}`,
|
|
294
|
+
playoff_window: chirpEnhanced.playoff_window,
|
|
295
|
+
positions: chirpEnhanced.positions,
|
|
296
|
+
signals: chirpEnhanced.signals,
|
|
297
|
+
cheat_sheet: chirpEnhanced.cheat_sheet,
|
|
298
|
+
not_included: chirpEnhanced.not_included,
|
|
299
|
+
...(chirpEnhanced.unresolved?.length ? { rankings_not_matched: chirpEnhanced.unresolved } : {})
|
|
300
|
+
};
|
|
301
|
+
const metadata = {
|
|
302
|
+
analysis_type: this.analysisType,
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
team_context: { team_name: 'Draft Kit' },
|
|
305
|
+
semantic_contract_applied: true
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
analysis_insights: analysisInsights,
|
|
309
|
+
recommendations,
|
|
310
|
+
chirp_intelligence: chirpEnhanced.chirp_intelligence,
|
|
311
|
+
metadata
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** Same anchoring as the other draft tools: week 1 is the NHL opener. */
|
|
315
|
+
resolvePlayoffWindow(args) {
|
|
316
|
+
const startWeek = Number(args?.playoff_start_week ?? 0);
|
|
317
|
+
const endWeek = Number(args?.playoff_end_week ?? 0);
|
|
318
|
+
const seasonStart = NHL_SCHEDULE.getSeasonStartDate();
|
|
319
|
+
if (!startWeek || !endWeek || startWeek > endWeek || !seasonStart) {
|
|
320
|
+
return { resolved: false, start: null, end: null, weeks: [] };
|
|
321
|
+
}
|
|
322
|
+
const week1Monday = NhlScheduleService.weekStart(seasonStart);
|
|
323
|
+
return {
|
|
324
|
+
resolved: true,
|
|
325
|
+
week_1_anchor: `${week1Monday} (NHL season opener)`,
|
|
326
|
+
playoff_start_week: startWeek,
|
|
327
|
+
end_week: endWeek,
|
|
328
|
+
start: NhlScheduleService.addDays(week1Monday, (startWeek - 1) * 7),
|
|
329
|
+
end: NhlScheduleService.addDays(week1Monday, endWeek * 7 - 1),
|
|
330
|
+
weeks: Array.from({ length: endWeek - startWeek + 1 }, (_, i) => NhlScheduleService.addDays(week1Monday, (startWeek - 1 + i) * 7))
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -157,6 +157,18 @@ export const TOOL_METADATA = {
|
|
|
157
157
|
template_version: "1.0.0",
|
|
158
158
|
analysis_type: "schedule_advantage"
|
|
159
159
|
},
|
|
160
|
+
draft_kit: {
|
|
161
|
+
chirp_style: "strategic_advantage",
|
|
162
|
+
discovery_tags: ["draft", "kit", "tiers", "cheat_sheet", "rankings", "sleepers"],
|
|
163
|
+
intent_category: "draft_intelligence",
|
|
164
|
+
hockey_context: "draft_preparation",
|
|
165
|
+
chirp_potential: "draft_board_truth",
|
|
166
|
+
// 🆕 Template Pattern Metadata
|
|
167
|
+
uses_template_pattern: true,
|
|
168
|
+
analysis_class: "DraftKitAnalysis",
|
|
169
|
+
template_version: "1.0.0",
|
|
170
|
+
analysis_type: "draft_pick"
|
|
171
|
+
},
|
|
160
172
|
chirp_draft_pick: {
|
|
161
173
|
chirp_style: "ice_cold_truth",
|
|
162
174
|
discovery_tags: ["draft", "pick", "ADP", "value", "roster_build"],
|
package/build/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { WeekendStreamAnalysis } from './analyses/WeekendStreamAnalysis.js';
|
|
|
24
24
|
import { BreakoutAnalysis } from './analyses/BreakoutAnalysis.js';
|
|
25
25
|
import { ScheduleValueAnalysis } from './analyses/ScheduleValueAnalysis.js';
|
|
26
26
|
import { DraftPickAnalysis } from './analyses/DraftPickAnalysis.js';
|
|
27
|
+
import { DraftKitAnalysis } from './analyses/DraftKitAnalysis.js';
|
|
27
28
|
import { NHL_STATS } from './services/NhlStatsService.js';
|
|
28
29
|
import { ROSTER_STORE } from './services/RosterStore.js';
|
|
29
30
|
import { LEAGUE_DATA, NO_ROSTER_MESSAGE, NO_OPPONENT_MESSAGE } from './services/LeagueDataService.js';
|
|
@@ -60,6 +61,7 @@ const lineupAnalysis = new LineupAnalysis();
|
|
|
60
61
|
const breakoutAnalysis = new BreakoutAnalysis();
|
|
61
62
|
const scheduleValueAnalysis = new ScheduleValueAnalysis();
|
|
62
63
|
const draftPickAnalysis = new DraftPickAnalysis();
|
|
64
|
+
const draftKitAnalysis = new DraftKitAnalysis();
|
|
63
65
|
const weekendStreamAnalysis = new WeekendStreamAnalysis();
|
|
64
66
|
// Helper function to find current matchup by status
|
|
65
67
|
function findCurrentMatchup(matchups) {
|
|
@@ -1191,6 +1193,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1191
1193
|
}
|
|
1192
1194
|
}
|
|
1193
1195
|
},
|
|
1196
|
+
{
|
|
1197
|
+
name: "draft_kit",
|
|
1198
|
+
description: "📋 A full draft kit — positional tiers, a cheat sheet, and flags you cannot get elsewhere: playoff-window schedule per club, shooting-luck rebound candidates, age-based decline risk, and category specialists. Works two ways: call it plain and it builds the board from last season's NHL production, or paste a ranked list (from any published kit) and it keeps that order while annotating it with schedule and flags. States plainly what it does not include — no projections, no ADP, no line combos, no injuries.",
|
|
1199
|
+
inputSchema: {
|
|
1200
|
+
type: "object",
|
|
1201
|
+
properties: {
|
|
1202
|
+
playoff_start_week: {
|
|
1203
|
+
type: "number",
|
|
1204
|
+
description: "First week of your fantasy playoffs. Supply this with playoff_end_week and every player gets their club's playoff-window game count."
|
|
1205
|
+
},
|
|
1206
|
+
playoff_end_week: {
|
|
1207
|
+
type: "number",
|
|
1208
|
+
description: "Final week of your fantasy playoffs."
|
|
1209
|
+
},
|
|
1210
|
+
rankings: {
|
|
1211
|
+
type: "string",
|
|
1212
|
+
description: "Optional. Paste a ranked player list — from NHL.com, Dobber, FantasyPros, anywhere — one per line. Its order becomes the baseline rank and CHIRP annotates it rather than replacing it. Omit to have the board built from NHL production instead."
|
|
1213
|
+
},
|
|
1214
|
+
positions: {
|
|
1215
|
+
type: "array",
|
|
1216
|
+
items: { type: "string" },
|
|
1217
|
+
description: "Limit to positions, e.g. [\"C\", \"G\"] (default: all)"
|
|
1218
|
+
},
|
|
1219
|
+
tier_size: { type: "number", description: "Players per tier (default 6)", default: 6 },
|
|
1220
|
+
max_per_position: { type: "number", description: "How deep to go per position (default 24)", default: 24 },
|
|
1221
|
+
...baseChirpSchema
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1194
1225
|
{
|
|
1195
1226
|
name: "chirp_draft_pick",
|
|
1196
1227
|
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.",
|
|
@@ -1633,6 +1664,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1633
1664
|
};
|
|
1634
1665
|
}
|
|
1635
1666
|
}
|
|
1667
|
+
case "draft_kit": {
|
|
1668
|
+
try {
|
|
1669
|
+
const result = await draftKitAnalysis.executeAnalysis({
|
|
1670
|
+
playoff_start_week: args?.playoff_start_week,
|
|
1671
|
+
playoff_end_week: args?.playoff_end_week,
|
|
1672
|
+
rankings: args?.rankings,
|
|
1673
|
+
positions: args?.positions,
|
|
1674
|
+
tier_size: args?.tier_size,
|
|
1675
|
+
max_per_position: args?.max_per_position
|
|
1676
|
+
}, {
|
|
1677
|
+
chirp_intensity: args?.chirp_intensity || 'standard',
|
|
1678
|
+
personality_mode: args?.personality_mode || 'analytical',
|
|
1679
|
+
enable_chirp: args?.enable_chirp !== false,
|
|
1680
|
+
semantic_intent: 'user_requested'
|
|
1681
|
+
});
|
|
1682
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1683
|
+
}
|
|
1684
|
+
catch (error) {
|
|
1685
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1686
|
+
return {
|
|
1687
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1688
|
+
error: errorMessage,
|
|
1689
|
+
note: "Draft kit failed - it needs only the NHL public API, so retry shortly"
|
|
1690
|
+
}, null, 2) }],
|
|
1691
|
+
isError: true
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1636
1695
|
case "chirp_draft_pick": {
|
|
1637
1696
|
try {
|
|
1638
1697
|
const semanticContract = {
|
|
@@ -25,6 +25,15 @@ const __dirname = path.dirname(__filename);
|
|
|
25
25
|
const NHL_API_BASE = 'https://api-web.nhle.com/v1';
|
|
26
26
|
const GAME_TYPE_REGULAR_SEASON = 2;
|
|
27
27
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
/**
|
|
29
|
+
* Bump whenever the cached player shape changes.
|
|
30
|
+
*
|
|
31
|
+
* The cache is keyed by season, so adding a field to NhlPlayer would otherwise
|
|
32
|
+
* keep serving records without it until the TTL expired — silently, and looking
|
|
33
|
+
* exactly like the field is unavailable from the NHL. Including the version in
|
|
34
|
+
* the filename makes a shape change invalidate the cache immediately.
|
|
35
|
+
*/
|
|
36
|
+
const CACHE_SCHEMA_VERSION = 2;
|
|
28
37
|
export class NhlStatsService {
|
|
29
38
|
byId = new Map();
|
|
30
39
|
byNameKey = new Map();
|
|
@@ -113,6 +122,7 @@ export class NhlStatsService {
|
|
|
113
122
|
name: `${p.firstName?.default ?? ''} ${p.lastName?.default ?? ''}`.trim(),
|
|
114
123
|
team,
|
|
115
124
|
position: p.positionCode ?? (group === 'goalies' ? 'G' : '?'),
|
|
125
|
+
birth_date: p.birthDate,
|
|
116
126
|
stats: statsById.get(id)
|
|
117
127
|
});
|
|
118
128
|
}
|
|
@@ -235,6 +245,21 @@ export class NhlStatsService {
|
|
|
235
245
|
getByTeam(team) {
|
|
236
246
|
return this.getAll().filter(p => p.team === team);
|
|
237
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Age in years at a given date, or null when the birth date is unknown.
|
|
250
|
+
*
|
|
251
|
+
* Age drives the breakout and decline signals a draft kit needs, and the NHL
|
|
252
|
+
* publishes it on the roster endpoint, so there is no reason to estimate it.
|
|
253
|
+
*/
|
|
254
|
+
static ageOf(player, asOf = new Date()) {
|
|
255
|
+
if (!player.birth_date)
|
|
256
|
+
return null;
|
|
257
|
+
const born = Date.parse(`${player.birth_date}T00:00:00Z`);
|
|
258
|
+
if (!Number.isFinite(born))
|
|
259
|
+
return null;
|
|
260
|
+
const years = (asOf.getTime() - born) / (365.2425 * 24 * 60 * 60 * 1000);
|
|
261
|
+
return Number(years.toFixed(1));
|
|
262
|
+
}
|
|
238
263
|
getPlayerCount() {
|
|
239
264
|
return this.byId.size;
|
|
240
265
|
}
|
|
@@ -259,7 +284,7 @@ export class NhlStatsService {
|
|
|
259
284
|
// 🎯 Cache
|
|
260
285
|
// ==========================================
|
|
261
286
|
cachePath(rosterSeason, statsSeason) {
|
|
262
|
-
return path.join(this.cacheDir, `players-${rosterSeason}-${statsSeason}.json`);
|
|
287
|
+
return path.join(this.cacheDir, `players-v${CACHE_SCHEMA_VERSION}-${rosterSeason}-${statsSeason}.json`);
|
|
263
288
|
}
|
|
264
289
|
readCache(rosterSeason, statsSeason) {
|
|
265
290
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semanticintent/semantic-chirp-intelligence-mcp",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Universal fantasy hockey intelligence \u2014 a Model Context Protocol server that reads NHL schedule and player data and analyses any roster you paste. No account, no API key, no platform lock-in.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
package/scripts/smoke.mjs
CHANGED
|
@@ -39,6 +39,7 @@ const TOOLS = [
|
|
|
39
39
|
['analyze_trade', { giving: ['Cale Makar'], receiving: ['Quinn Hughes'] }],
|
|
40
40
|
['schedule_value', { teams: ['TOR', 'SEA'], playoff_start_week: 22, playoff_end_week: 24 }],
|
|
41
41
|
['chirp_draft_pick', { pick_number: 5, max_results: 5 }],
|
|
42
|
+
['draft_kit', { playoff_start_week: 22, playoff_end_week: 24, positions: ['C'], max_per_position: 6 }],
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
const proc = spawn(process.execPath, ['build/index.js'], { stdio: ['pipe', 'pipe', 'pipe'] });
|