wc26-mcp 0.2.0 → 0.3.1
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/README.md +75 -20
- package/dist/data/city-guides.d.ts +2 -0
- package/dist/data/city-guides.js +373 -0
- package/dist/data/fan-zones.d.ts +2 -0
- package/dist/data/fan-zones.js +646 -0
- package/dist/data/groups.js +0 -1
- package/dist/data/historical-matchups.d.ts +2 -0
- package/dist/data/historical-matchups.js +556 -0
- package/dist/data/injuries.d.ts +2 -0
- package/dist/data/injuries.js +197 -0
- package/dist/data/matches.js +48 -48
- package/dist/data/news.d.ts +2 -0
- package/dist/data/news.js +171 -0
- package/dist/data/odds.d.ts +2 -0
- package/dist/data/odds.js +53 -0
- package/dist/data/team-profiles.js +47 -48
- package/dist/data/teams.js +0 -1
- package/dist/data/venues.js +0 -1
- package/dist/data/visa-info.d.ts +2 -0
- package/dist/data/visa-info.js +1190 -0
- package/dist/index.js +1006 -25
- package/dist/types/index.d.ts +114 -0
- package/dist/types/index.js +0 -1
- package/package.json +17 -2
- package/dist/data/groups.js.map +0 -1
- package/dist/data/matches.js.map +0 -1
- package/dist/data/team-profiles.js.map +0 -1
- package/dist/data/teams.js.map +0 -1
- package/dist/data/venues.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -7,10 +7,17 @@ import { teams } from "./data/teams.js";
|
|
|
7
7
|
import { groups } from "./data/groups.js";
|
|
8
8
|
import { venues } from "./data/venues.js";
|
|
9
9
|
import { teamProfiles } from "./data/team-profiles.js";
|
|
10
|
+
import { cityGuides } from "./data/city-guides.js";
|
|
11
|
+
import { historicalMatchups } from "./data/historical-matchups.js";
|
|
12
|
+
import { visaInfo } from "./data/visa-info.js";
|
|
13
|
+
import { fanZones } from "./data/fan-zones.js";
|
|
14
|
+
import { news } from "./data/news.js";
|
|
15
|
+
import { injuries } from "./data/injuries.js";
|
|
16
|
+
import { tournamentOdds } from "./data/odds.js";
|
|
10
17
|
// ── Server setup ────────────────────────────────────────────────────
|
|
11
18
|
const server = new McpServer({
|
|
12
19
|
name: "wc26-mcp",
|
|
13
|
-
version: "0.
|
|
20
|
+
version: "0.3.1",
|
|
14
21
|
});
|
|
15
22
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
16
23
|
function json(data) {
|
|
@@ -39,18 +46,27 @@ function haversineDistance(lat1, lng1, lat2, lng2) {
|
|
|
39
46
|
return { miles: Math.round(km * 0.621371), km: Math.round(km) };
|
|
40
47
|
}
|
|
41
48
|
function convertTime(date, timeUtc, timezone) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
try {
|
|
50
|
+
const dt = new Date(`${date}T${timeUtc}:00Z`);
|
|
51
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
52
|
+
timeZone: timezone,
|
|
53
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
54
|
+
hour: "2-digit", minute: "2-digit",
|
|
55
|
+
hour12: false,
|
|
56
|
+
});
|
|
57
|
+
const parts = Object.fromEntries(formatter.formatToParts(dt).map((p) => [p.type, p.value]));
|
|
58
|
+
return {
|
|
59
|
+
date: `${parts.year}-${parts.month}-${parts.day}`,
|
|
60
|
+
time: `${parts.hour}:${parts.minute}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { date, time: timeUtc, error: `Invalid timezone "${timezone}". Use IANA format like "America/New_York" or "Europe/London".` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function resolveTeam(input) {
|
|
68
|
+
const tid = input.toLowerCase();
|
|
69
|
+
return teams.find(t => t.id === tid || t.code.toLowerCase() === tid || t.name.toLowerCase() === tid);
|
|
54
70
|
}
|
|
55
71
|
function enrichMatch(m, timezone) {
|
|
56
72
|
const venue = venues.find((v) => v.id === m.venue_id);
|
|
@@ -120,19 +136,23 @@ const matchStatuses = [
|
|
|
120
136
|
"cancelled",
|
|
121
137
|
];
|
|
122
138
|
server.registerTool("get_matches", {
|
|
139
|
+
annotations: { readOnlyHint: true },
|
|
123
140
|
title: "Get Matches",
|
|
124
141
|
description: "Query FIFA World Cup 2026 matches. Filter by date, date range, team, group, venue, round, or status. Returns enriched match data with team names, venue details, and local venue time. Optionally pass a timezone to convert match times to any IANA timezone.",
|
|
125
142
|
inputSchema: z.object({
|
|
126
143
|
date: z
|
|
127
144
|
.string()
|
|
145
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
128
146
|
.optional()
|
|
129
147
|
.describe("Exact date in YYYY-MM-DD format"),
|
|
130
148
|
date_from: z
|
|
131
149
|
.string()
|
|
150
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
132
151
|
.optional()
|
|
133
152
|
.describe("Start of date range (YYYY-MM-DD), inclusive"),
|
|
134
153
|
date_to: z
|
|
135
154
|
.string()
|
|
155
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
136
156
|
.optional()
|
|
137
157
|
.describe("End of date range (YYYY-MM-DD), inclusive"),
|
|
138
158
|
team: z
|
|
@@ -172,15 +192,34 @@ server.registerTool("get_matches", {
|
|
|
172
192
|
result = result.filter((m) => m.date <= args.date_to);
|
|
173
193
|
}
|
|
174
194
|
if (args.team) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
const team = resolveTeam(args.team);
|
|
196
|
+
if (!team) {
|
|
197
|
+
return json({
|
|
198
|
+
error: `Team '${args.team}' not found.`,
|
|
199
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
result = result.filter((m) => m.home_team_id === team.id || m.away_team_id === team.id);
|
|
179
203
|
}
|
|
180
204
|
if (args.group) {
|
|
181
|
-
|
|
205
|
+
const groupLetter = args.group.toUpperCase();
|
|
206
|
+
const validGroups = [...new Set(matches.map((m) => m.group).filter(Boolean))];
|
|
207
|
+
if (!validGroups.includes(groupLetter)) {
|
|
208
|
+
return json({
|
|
209
|
+
error: `Group '${args.group}' not found.`,
|
|
210
|
+
suggestion: `Valid groups: ${validGroups.sort().join(", ")}. Use the get_groups tool for full group details.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
result = result.filter((m) => m.group?.toUpperCase() === groupLetter);
|
|
182
214
|
}
|
|
183
215
|
if (args.venue) {
|
|
216
|
+
const venueExists = venues.some((v) => v.id === args.venue);
|
|
217
|
+
if (!venueExists) {
|
|
218
|
+
return json({
|
|
219
|
+
error: `Venue '${args.venue}' not found.`,
|
|
220
|
+
suggestion: "Use the get_venues tool to see all available venues and their IDs.",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
184
223
|
result = result.filter((m) => m.venue_id === args.venue);
|
|
185
224
|
}
|
|
186
225
|
if (args.round) {
|
|
@@ -204,6 +243,7 @@ const confederations = [
|
|
|
204
243
|
"OFC",
|
|
205
244
|
];
|
|
206
245
|
server.registerTool("get_teams", {
|
|
246
|
+
annotations: { readOnlyHint: true },
|
|
207
247
|
title: "Get Teams",
|
|
208
248
|
description: "Query FIFA World Cup 2026 teams. Filter by group, confederation, or host status. Returns team details including FIFA ranking and flag emoji.",
|
|
209
249
|
inputSchema: z.object({
|
|
@@ -238,6 +278,7 @@ server.registerTool("get_teams", {
|
|
|
238
278
|
});
|
|
239
279
|
// ── Tool: get_groups ────────────────────────────────────────────────
|
|
240
280
|
server.registerTool("get_groups", {
|
|
281
|
+
annotations: { readOnlyHint: true },
|
|
241
282
|
title: "Get Groups",
|
|
242
283
|
description: "Get FIFA World Cup 2026 group details. Returns teams, venues, and match schedule for each group. Optionally filter to a specific group.",
|
|
243
284
|
inputSchema: z.object({
|
|
@@ -258,6 +299,7 @@ server.registerTool("get_groups", {
|
|
|
258
299
|
});
|
|
259
300
|
// ── Tool: get_venues ────────────────────────────────────────────────
|
|
260
301
|
server.registerTool("get_venues", {
|
|
302
|
+
annotations: { readOnlyHint: true },
|
|
261
303
|
title: "Get Venues",
|
|
262
304
|
description: "Query FIFA World Cup 2026 venues across the USA, Mexico, and Canada. Filter by country, city, or region. Returns full venue details including capacity, coordinates, and notable events.",
|
|
263
305
|
inputSchema: z.object({
|
|
@@ -280,7 +322,7 @@ server.registerTool("get_venues", {
|
|
|
280
322
|
result = result.filter((v) => v.country === args.country);
|
|
281
323
|
}
|
|
282
324
|
if (args.city) {
|
|
283
|
-
result = result.filter((v) => v.city.toLowerCase()
|
|
325
|
+
result = result.filter((v) => v.city.toLowerCase().includes(args.city.toLowerCase()));
|
|
284
326
|
}
|
|
285
327
|
if (args.region) {
|
|
286
328
|
result = result.filter((v) => v.region === args.region);
|
|
@@ -292,15 +334,18 @@ server.registerTool("get_venues", {
|
|
|
292
334
|
});
|
|
293
335
|
// ── Tool: get_schedule ──────────────────────────────────────────────
|
|
294
336
|
server.registerTool("get_schedule", {
|
|
337
|
+
annotations: { readOnlyHint: true },
|
|
295
338
|
title: "Get Schedule",
|
|
296
339
|
description: "Get the full FIFA World Cup 2026 tournament schedule organized by date. Optionally filter to a specific date range. Each day includes all matches with team names, venues, kick-off times (UTC), and local venue time. Optionally pass a timezone to convert all match times.",
|
|
297
340
|
inputSchema: z.object({
|
|
298
341
|
date_from: z
|
|
299
342
|
.string()
|
|
343
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
300
344
|
.optional()
|
|
301
345
|
.describe("Start date (YYYY-MM-DD), defaults to tournament start (2026-06-11)"),
|
|
302
346
|
date_to: z
|
|
303
347
|
.string()
|
|
348
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
304
349
|
.optional()
|
|
305
350
|
.describe("End date (YYYY-MM-DD), defaults to tournament end (2026-07-19)"),
|
|
306
351
|
timezone: z
|
|
@@ -340,6 +385,7 @@ server.registerTool("get_schedule", {
|
|
|
340
385
|
});
|
|
341
386
|
// ── Tool: get_team_profile ───────────────────────────────────────────
|
|
342
387
|
server.registerTool("get_team_profile", {
|
|
388
|
+
annotations: { readOnlyHint: true },
|
|
343
389
|
title: "Get Team Profile",
|
|
344
390
|
description: "Get a detailed profile for any FIFA World Cup 2026 team. Returns coach, playing style, key players with clubs, World Cup history, and qualifying summary. Use team ID or FIFA code (e.g. 'usa', 'BRA', 'arg').",
|
|
345
391
|
inputSchema: z.object({
|
|
@@ -348,8 +394,7 @@ server.registerTool("get_team_profile", {
|
|
|
348
394
|
.describe("Team ID or FIFA code (e.g. 'usa', 'BRA', 'arg'). Case-insensitive."),
|
|
349
395
|
}),
|
|
350
396
|
}, async (args) => {
|
|
351
|
-
const
|
|
352
|
-
const team = teams.find((t) => t.id === tid || t.code.toLowerCase() === tid);
|
|
397
|
+
const team = resolveTeam(args.team);
|
|
353
398
|
if (!team) {
|
|
354
399
|
return json({
|
|
355
400
|
error: `Team '${args.team}' not found.`,
|
|
@@ -374,14 +419,56 @@ server.registerTool("get_team_profile", {
|
|
|
374
419
|
world_cup_history: null,
|
|
375
420
|
qualifying_summary: "No qualifying data available.",
|
|
376
421
|
}),
|
|
422
|
+
injury_report: injuries
|
|
423
|
+
.filter((i) => i.team_id === team.id)
|
|
424
|
+
.map((i) => ({
|
|
425
|
+
player: i.player,
|
|
426
|
+
position: i.position,
|
|
427
|
+
injury: i.injury,
|
|
428
|
+
status: i.status,
|
|
429
|
+
expected_return: i.expected_return,
|
|
430
|
+
last_updated: i.last_updated,
|
|
431
|
+
})),
|
|
432
|
+
tournament_odds: (() => {
|
|
433
|
+
const winnerOdds = tournamentOdds.tournament_winner.find((o) => o.team_id === team.id);
|
|
434
|
+
const groupPred = tournamentOdds.group_predictions.find((g) => g.group === team.group);
|
|
435
|
+
const darkHorse = tournamentOdds.dark_horses.find((d) => d.team_id === team.id);
|
|
436
|
+
return {
|
|
437
|
+
...(winnerOdds ? { winner_odds: winnerOdds.odds, implied_probability: winnerOdds.implied_probability } : {}),
|
|
438
|
+
...(groupPred ? {
|
|
439
|
+
group_prediction: {
|
|
440
|
+
favorites: groupPred.favorites.map((f) => teamName(f)),
|
|
441
|
+
dark_horse: teamName(groupPred.dark_horse),
|
|
442
|
+
narrative: groupPred.narrative,
|
|
443
|
+
},
|
|
444
|
+
} : {}),
|
|
445
|
+
...(darkHorse ? { dark_horse_pick: darkHorse.reason } : {}),
|
|
446
|
+
};
|
|
447
|
+
})(),
|
|
448
|
+
recent_news: news
|
|
449
|
+
.filter((n) => {
|
|
450
|
+
if (n.related_teams.includes(team.id))
|
|
451
|
+
return true;
|
|
452
|
+
// Fallback: text match on team name in title/summary
|
|
453
|
+
const text = `${n.title} ${n.summary}`.toLowerCase();
|
|
454
|
+
return text.includes(team.name.toLowerCase()) ||
|
|
455
|
+
(team.code !== "TBD" && text.includes(team.code.toLowerCase()));
|
|
456
|
+
})
|
|
457
|
+
.slice(0, 5)
|
|
458
|
+
.map((n) => ({ title: n.title, date: n.date, source: n.source, summary: n.summary, url: n.url })),
|
|
377
459
|
related_tools: [
|
|
378
460
|
"Use get_matches with team filter to see this team's match schedule",
|
|
379
461
|
"Use get_groups to see this team's group rivals and venue assignments",
|
|
462
|
+
"Use get_visa_info to check entry requirements for this team's fans traveling to host countries",
|
|
463
|
+
"Use get_injuries for full injury details across all teams",
|
|
464
|
+
"Use get_odds for complete tournament predictions",
|
|
465
|
+
"Use get_news to see all World Cup headlines",
|
|
380
466
|
],
|
|
381
467
|
});
|
|
382
468
|
});
|
|
383
469
|
// ── Tool: get_nearby_venues ──────────────────────────────────────────
|
|
384
470
|
server.registerTool("get_nearby_venues", {
|
|
471
|
+
annotations: { readOnlyHint: true },
|
|
385
472
|
title: "Get Nearby Venues",
|
|
386
473
|
description: "Find venues near a given World Cup venue, sorted by distance. Useful for planning multi-match trips. Returns distance in miles and kilometers between all venue pairs.",
|
|
387
474
|
inputSchema: z.object({
|
|
@@ -430,6 +517,199 @@ server.registerTool("get_nearby_venues", {
|
|
|
430
517
|
],
|
|
431
518
|
});
|
|
432
519
|
});
|
|
520
|
+
// ── Tool: get_city_guide ────────────────────────────────────────────
|
|
521
|
+
server.registerTool("get_city_guide", {
|
|
522
|
+
annotations: { readOnlyHint: true },
|
|
523
|
+
title: "Get City Guide",
|
|
524
|
+
description: "Get a travel guide for any FIFA World Cup 2026 host city. Returns highlights, getting there, food & drink recommendations, things to do, and local tips. Accepts venue ID (e.g. 'metlife'), city name (e.g. 'Miami'), or metro area name (e.g. 'New York'). Case-insensitive.",
|
|
525
|
+
inputSchema: z.object({
|
|
526
|
+
city: z
|
|
527
|
+
.string()
|
|
528
|
+
.describe("Venue ID (e.g. 'metlife'), city name (e.g. 'Miami Gardens'), or metro area (e.g. 'New York'). Case-insensitive."),
|
|
529
|
+
}),
|
|
530
|
+
}, async (args) => {
|
|
531
|
+
const query = args.city.toLowerCase();
|
|
532
|
+
// Find matching venue + guide by venue ID, city name, or metro area
|
|
533
|
+
let venue;
|
|
534
|
+
let guide;
|
|
535
|
+
// Try venue ID first
|
|
536
|
+
venue = venues.find((v) => v.id === query);
|
|
537
|
+
if (venue) {
|
|
538
|
+
guide = cityGuides.find((g) => g.venue_id === venue.id);
|
|
539
|
+
}
|
|
540
|
+
// Try city name
|
|
541
|
+
if (!venue) {
|
|
542
|
+
venue = venues.find((v) => v.city.toLowerCase().includes(query));
|
|
543
|
+
if (venue) {
|
|
544
|
+
guide = cityGuides.find((g) => g.venue_id === venue.id);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Try metro area
|
|
548
|
+
if (!guide) {
|
|
549
|
+
guide = cityGuides.find((g) => g.metro_area.toLowerCase() === query);
|
|
550
|
+
if (guide) {
|
|
551
|
+
venue = venues.find((v) => v.id === guide.venue_id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Try partial match on metro area (e.g. "new york" matches "New York City")
|
|
555
|
+
if (!guide) {
|
|
556
|
+
guide = cityGuides.find((g) => g.metro_area.toLowerCase().includes(query));
|
|
557
|
+
if (guide) {
|
|
558
|
+
venue = venues.find((v) => v.id === guide.venue_id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Try partial match on city name
|
|
562
|
+
if (!guide && !venue) {
|
|
563
|
+
venue = venues.find((v) => v.city.toLowerCase().includes(query));
|
|
564
|
+
if (venue) {
|
|
565
|
+
guide = cityGuides.find((g) => g.venue_id === venue.id);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!venue || !guide) {
|
|
569
|
+
return json({
|
|
570
|
+
error: `City '${args.city}' not found.`,
|
|
571
|
+
suggestion: "Use the get_venues tool to see all available host cities and venue IDs.",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return json({
|
|
575
|
+
venue: {
|
|
576
|
+
id: venue.id,
|
|
577
|
+
name: venue.name,
|
|
578
|
+
city: venue.city,
|
|
579
|
+
state_province: venue.state_province,
|
|
580
|
+
country: venue.country,
|
|
581
|
+
capacity: venue.capacity,
|
|
582
|
+
timezone: venue.timezone,
|
|
583
|
+
weather: venue.weather,
|
|
584
|
+
},
|
|
585
|
+
metro_area: guide.metro_area,
|
|
586
|
+
highlights: guide.highlights,
|
|
587
|
+
getting_there: guide.getting_there,
|
|
588
|
+
food_and_drink: guide.food_and_drink,
|
|
589
|
+
things_to_do: guide.things_to_do,
|
|
590
|
+
local_tips: guide.local_tips,
|
|
591
|
+
related_tools: [
|
|
592
|
+
"Use get_matches with venue filter to see matches at this venue",
|
|
593
|
+
"Use get_nearby_venues to find other stadiums near this city",
|
|
594
|
+
"Use get_visa_info to check entry requirements for fans traveling to this country",
|
|
595
|
+
"Use get_fan_zones to find fan festival locations in this city",
|
|
596
|
+
],
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
// ── Tool: get_historical_matchups ────────────────────────────────────
|
|
600
|
+
server.registerTool("get_historical_matchups", {
|
|
601
|
+
annotations: { readOnlyHint: true },
|
|
602
|
+
title: "Get Historical Matchups",
|
|
603
|
+
description: "Head-to-head World Cup history between any two teams. Returns all tournament meetings, aggregate stats, and a narrative summary. Accepts team ID or FIFA code (e.g. 'bra', 'ARG', 'england'). Also checks for a scheduled 2026 match between the pair.",
|
|
604
|
+
inputSchema: z.object({
|
|
605
|
+
team_a: z
|
|
606
|
+
.string()
|
|
607
|
+
.describe("First team — ID or FIFA code (e.g. 'bra', 'ARG', 'england'). Case-insensitive."),
|
|
608
|
+
team_b: z
|
|
609
|
+
.string()
|
|
610
|
+
.describe("Second team — ID or FIFA code (e.g. 'eng', 'FRA', 'germany'). Case-insensitive."),
|
|
611
|
+
}),
|
|
612
|
+
}, async (args) => {
|
|
613
|
+
const teamA = resolveTeam(args.team_a);
|
|
614
|
+
const teamB = resolveTeam(args.team_b);
|
|
615
|
+
if (!teamA) {
|
|
616
|
+
return json({
|
|
617
|
+
error: `Team '${args.team_a}' not found.`,
|
|
618
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (!teamB) {
|
|
622
|
+
return json({
|
|
623
|
+
error: `Team '${args.team_b}' not found.`,
|
|
624
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (teamA.id === teamB.id) {
|
|
628
|
+
return json({
|
|
629
|
+
error: "Both inputs resolve to the same team. Please provide two different teams.",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// TBD team check
|
|
633
|
+
if (teamA.code === "TBD") {
|
|
634
|
+
return json({
|
|
635
|
+
note: `${teamA.name} has not been determined yet. Historical matchup data will be available once the playoff is decided.`,
|
|
636
|
+
team_b: { id: teamB.id, name: teamB.name, flag_emoji: teamB.flag_emoji },
|
|
637
|
+
related_tools: [
|
|
638
|
+
"Use what_to_know_now to check the latest tournament status including pending playoffs",
|
|
639
|
+
"Use get_team_profile to learn more about the confirmed team",
|
|
640
|
+
],
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (teamB.code === "TBD") {
|
|
644
|
+
return json({
|
|
645
|
+
note: `${teamB.name} has not been determined yet. Historical matchup data will be available once the playoff is decided.`,
|
|
646
|
+
team_a: { id: teamA.id, name: teamA.name, flag_emoji: teamA.flag_emoji },
|
|
647
|
+
related_tools: [
|
|
648
|
+
"Use what_to_know_now to check the latest tournament status including pending playoffs",
|
|
649
|
+
"Use get_team_profile to learn more about the confirmed team",
|
|
650
|
+
],
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// Sort alphabetically for canonical lookup
|
|
654
|
+
const [first, second] = [teamA, teamB].sort((a, b) => a.id.localeCompare(b.id));
|
|
655
|
+
const record = historicalMatchups.find((h) => h.team_a === first.id && h.team_b === second.id);
|
|
656
|
+
// Check for a 2026 match between these teams
|
|
657
|
+
const upcoming2026 = matches.find((m) => (m.home_team_id === teamA.id && m.away_team_id === teamB.id) ||
|
|
658
|
+
(m.home_team_id === teamB.id && m.away_team_id === teamA.id));
|
|
659
|
+
const matchContext = upcoming2026
|
|
660
|
+
? {
|
|
661
|
+
upcoming_2026_match: {
|
|
662
|
+
date: upcoming2026.date,
|
|
663
|
+
time_utc: upcoming2026.time_utc,
|
|
664
|
+
round: upcoming2026.round,
|
|
665
|
+
venue: venueName(upcoming2026.venue_id),
|
|
666
|
+
group: upcoming2026.group ?? undefined,
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
: {};
|
|
670
|
+
// No history found
|
|
671
|
+
if (!record) {
|
|
672
|
+
const profileA = teamProfiles.find((p) => p.team_id === first.id);
|
|
673
|
+
const profileB = teamProfiles.find((p) => p.team_id === second.id);
|
|
674
|
+
const firstTimerNote = (id, profile) => {
|
|
675
|
+
if (profile && profile.world_cup_history.appearances <= 1) {
|
|
676
|
+
const t = teams.find((t) => t.id === id);
|
|
677
|
+
return `${t?.name ?? id} is making their World Cup debut in 2026.`;
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
};
|
|
681
|
+
return json({
|
|
682
|
+
team_a: { id: first.id, name: first.name, flag_emoji: first.flag_emoji },
|
|
683
|
+
team_b: { id: second.id, name: second.name, flag_emoji: second.flag_emoji },
|
|
684
|
+
total_matches: 0,
|
|
685
|
+
summary: `${first.name} and ${second.name} have never met at a FIFA World Cup.`,
|
|
686
|
+
first_timer_notes: [firstTimerNote(first.id, profileA), firstTimerNote(second.id, profileB)].filter(Boolean),
|
|
687
|
+
...matchContext,
|
|
688
|
+
related_tools: [
|
|
689
|
+
"Use get_team_profile to learn about each team's World Cup history",
|
|
690
|
+
"Use get_matches with team filter to see their 2026 schedules",
|
|
691
|
+
],
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
// Return full matchup data
|
|
695
|
+
return json({
|
|
696
|
+
team_a: { id: first.id, name: first.name, flag_emoji: first.flag_emoji },
|
|
697
|
+
team_b: { id: second.id, name: second.name, flag_emoji: second.flag_emoji },
|
|
698
|
+
total_matches: record.total_matches,
|
|
699
|
+
team_a_wins: record.team_a_wins,
|
|
700
|
+
draws: record.draws,
|
|
701
|
+
team_b_wins: record.team_b_wins,
|
|
702
|
+
total_goals_team_a: record.total_goals_team_a,
|
|
703
|
+
total_goals_team_b: record.total_goals_team_b,
|
|
704
|
+
summary: record.summary,
|
|
705
|
+
meetings: record.meetings,
|
|
706
|
+
...matchContext,
|
|
707
|
+
related_tools: [
|
|
708
|
+
"Use get_team_profile to learn more about either team",
|
|
709
|
+
"Use get_matches with team filter to see their 2026 schedules",
|
|
710
|
+
],
|
|
711
|
+
});
|
|
712
|
+
});
|
|
433
713
|
// ── Tool: what_to_know_now ──────────────────────────────────────────
|
|
434
714
|
const TOURNAMENT_DATES = {
|
|
435
715
|
playoff_end: "2026-03-31",
|
|
@@ -460,11 +740,13 @@ const PHASE_LABELS = {
|
|
|
460
740
|
tournament_over: "Tournament Complete",
|
|
461
741
|
};
|
|
462
742
|
server.registerTool("what_to_know_now", {
|
|
743
|
+
annotations: { readOnlyHint: true },
|
|
463
744
|
title: "What to Know Now",
|
|
464
745
|
description: "Zero-query temporal briefing for the FIFA World Cup 2026. No parameters needed — just brief me. Automatically detects the current tournament phase and returns the most relevant information for today. Optionally pass a date to simulate a different day, or a timezone for local times.",
|
|
465
746
|
inputSchema: z.object({
|
|
466
747
|
date: z
|
|
467
748
|
.string()
|
|
749
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
|
|
468
750
|
.optional()
|
|
469
751
|
.describe("Override date in YYYY-MM-DD format (for testing different phases). Defaults to today."),
|
|
470
752
|
timezone: z
|
|
@@ -509,6 +791,37 @@ server.registerTool("what_to_know_now", {
|
|
|
509
791
|
title: "Venue Summary",
|
|
510
792
|
content: `${venues.length} venues across 3 countries: ${venues.filter((v) => v.country === "USA").length} in USA, ${venues.filter((v) => v.country === "Mexico").length} in Mexico, ${venues.filter((v) => v.country === "Canada").length} in Canada.`,
|
|
511
793
|
});
|
|
794
|
+
sections.push({
|
|
795
|
+
title: "Fan Zones",
|
|
796
|
+
content: `${fanZones.length} official FIFA Fan Festival and fan zone locations across all host cities. Free entry at most sites with live screenings, concerts, food, and interactive experiences.`,
|
|
797
|
+
});
|
|
798
|
+
// Key injuries
|
|
799
|
+
const activeInjuries = injuries.filter((i) => i.status !== "fit");
|
|
800
|
+
if (activeInjuries.length > 0) {
|
|
801
|
+
sections.push({
|
|
802
|
+
title: "Key Injuries to Watch",
|
|
803
|
+
content: activeInjuries.map((i) => ({
|
|
804
|
+
player: i.player,
|
|
805
|
+
team: teamName(i.team_id),
|
|
806
|
+
status: i.status,
|
|
807
|
+
injury: i.injury,
|
|
808
|
+
expected_return: i.expected_return,
|
|
809
|
+
})),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
// Tournament favorites
|
|
813
|
+
sections.push({
|
|
814
|
+
title: "Tournament Favorites",
|
|
815
|
+
content: tournamentOdds.tournament_winner.slice(0, 8).map((o) => ({
|
|
816
|
+
team: teamName(o.team_id),
|
|
817
|
+
odds: o.odds,
|
|
818
|
+
implied_probability: o.implied_probability,
|
|
819
|
+
})),
|
|
820
|
+
dark_horses: tournamentOdds.dark_horses.map((d) => ({
|
|
821
|
+
team: teamName(d.team_id),
|
|
822
|
+
reason: d.reason,
|
|
823
|
+
})),
|
|
824
|
+
});
|
|
512
825
|
}
|
|
513
826
|
else if (phase === "post_playoff") {
|
|
514
827
|
headline = `${daysUntilKickoff} days until the FIFA World Cup 2026 kicks off. All 48 teams are confirmed.`;
|
|
@@ -549,6 +862,37 @@ server.registerTool("what_to_know_now", {
|
|
|
549
862
|
content: marquee,
|
|
550
863
|
});
|
|
551
864
|
}
|
|
865
|
+
sections.push({
|
|
866
|
+
title: "Fan Zones",
|
|
867
|
+
content: `${fanZones.length} official FIFA Fan Festival and fan zone locations across all host cities. Free entry at most sites with live screenings, concerts, food, and interactive experiences.`,
|
|
868
|
+
});
|
|
869
|
+
// Key injuries
|
|
870
|
+
const activeInjuriesPost = injuries.filter((i) => i.status !== "fit");
|
|
871
|
+
if (activeInjuriesPost.length > 0) {
|
|
872
|
+
sections.push({
|
|
873
|
+
title: "Key Injuries to Watch",
|
|
874
|
+
content: activeInjuriesPost.map((i) => ({
|
|
875
|
+
player: i.player,
|
|
876
|
+
team: teamName(i.team_id),
|
|
877
|
+
status: i.status,
|
|
878
|
+
injury: i.injury,
|
|
879
|
+
expected_return: i.expected_return,
|
|
880
|
+
})),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
// Tournament favorites
|
|
884
|
+
sections.push({
|
|
885
|
+
title: "Tournament Favorites",
|
|
886
|
+
content: tournamentOdds.tournament_winner.slice(0, 8).map((o) => ({
|
|
887
|
+
team: teamName(o.team_id),
|
|
888
|
+
odds: o.odds,
|
|
889
|
+
implied_probability: o.implied_probability,
|
|
890
|
+
})),
|
|
891
|
+
dark_horses: tournamentOdds.dark_horses.map((d) => ({
|
|
892
|
+
team: teamName(d.team_id),
|
|
893
|
+
reason: d.reason,
|
|
894
|
+
})),
|
|
895
|
+
});
|
|
552
896
|
}
|
|
553
897
|
else if (phase === "group_stage") {
|
|
554
898
|
const todayMatches = matches.filter((m) => m.date === today);
|
|
@@ -642,15 +986,30 @@ server.registerTool("what_to_know_now", {
|
|
|
642
986
|
});
|
|
643
987
|
}
|
|
644
988
|
}
|
|
989
|
+
// Latest news (up to 5 most recent)
|
|
990
|
+
const recentNews = news
|
|
991
|
+
.filter((n) => n.date >= new Date(new Date(today).getTime() - 7 * 86400000).toISOString().slice(0, 10))
|
|
992
|
+
.slice(0, 5);
|
|
993
|
+
if (recentNews.length > 0) {
|
|
994
|
+
sections.push({
|
|
995
|
+
title: "Latest News",
|
|
996
|
+
articles: recentNews.map((n) => ({
|
|
997
|
+
title: n.title,
|
|
998
|
+
date: n.date,
|
|
999
|
+
source: n.source,
|
|
1000
|
+
summary: n.summary,
|
|
1001
|
+
})),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
645
1004
|
const available_tools = [];
|
|
646
1005
|
if (phase === "pre_playoff" || phase === "post_playoff") {
|
|
647
|
-
available_tools.push("Use get_team_profile to research any team in depth", "Use get_groups to explore group compositions and match schedules", "Use get_venues to explore stadiums and host cities", "Use get_schedule to see the full tournament calendar");
|
|
1006
|
+
available_tools.push("Use get_team_profile to research any team in depth", "Use get_groups to explore group compositions and match schedules", "Use get_venues to explore stadiums and host cities", "Use get_schedule to see the full tournament calendar", "Use get_visa_info to check entry requirements for any team's fans", "Use get_fan_zones to find fan festival locations in any host city", "Use get_news for more World Cup headlines", "Use get_injuries to check key player availability", "Use get_odds for tournament predictions and group previews");
|
|
648
1007
|
}
|
|
649
1008
|
else if (phase === "group_stage") {
|
|
650
|
-
available_tools.push("Use get_team_profile to research teams playing today", "Use get_matches to filter matches by team, group, or date", "Use get_groups to see full group standings and remaining fixtures");
|
|
1009
|
+
available_tools.push("Use get_team_profile to research teams playing today", "Use get_matches to filter matches by team, group, or date", "Use get_groups to see full group standings and remaining fixtures", "Use get_fan_zones to find fan festival locations near today's matches", "Use get_news for more World Cup headlines", "Use get_injuries to check key player availability", "Use get_odds for tournament predictions and group previews");
|
|
651
1010
|
}
|
|
652
1011
|
else if (phase === "knockout") {
|
|
653
|
-
available_tools.push("Use get_team_profile to research teams still in the tournament", "Use get_matches with round filter to see knockout bracket", "Use get_schedule to see remaining match dates");
|
|
1012
|
+
available_tools.push("Use get_team_profile to research teams still in the tournament", "Use get_matches with round filter to see knockout bracket", "Use get_schedule to see remaining match dates", "Use get_fan_zones to find fan festival locations near today's matches", "Use get_news for more World Cup headlines", "Use get_injuries to check key player availability");
|
|
654
1013
|
}
|
|
655
1014
|
else {
|
|
656
1015
|
available_tools.push("Use get_matches to review any match from the tournament", "Use get_team_profile to look up team details");
|
|
@@ -665,6 +1024,629 @@ server.registerTool("what_to_know_now", {
|
|
|
665
1024
|
available_tools,
|
|
666
1025
|
});
|
|
667
1026
|
});
|
|
1027
|
+
// ── Tool: get_visa_info ──────────────────────────────────────────────
|
|
1028
|
+
server.registerTool("get_visa_info", {
|
|
1029
|
+
annotations: { readOnlyHint: true },
|
|
1030
|
+
title: "Get Visa Info",
|
|
1031
|
+
description: "Entry requirements for FIFA World Cup 2026 travelers. Returns visa/ESTA/eTA requirements for any team's nationals entering the three host countries (USA, Mexico, Canada). Accepts team ID, FIFA code, or country name. Optionally filter to a specific host country.",
|
|
1032
|
+
inputSchema: z.object({
|
|
1033
|
+
team: z
|
|
1034
|
+
.string()
|
|
1035
|
+
.describe("Team ID, FIFA code, or country name (e.g. 'bra', 'ARG', 'england'). Case-insensitive."),
|
|
1036
|
+
host_country: z
|
|
1037
|
+
.enum(["USA", "Mexico", "Canada"])
|
|
1038
|
+
.optional()
|
|
1039
|
+
.describe("Filter to a specific host country. Omit for all three."),
|
|
1040
|
+
}),
|
|
1041
|
+
}, async (args) => {
|
|
1042
|
+
const team = resolveTeam(args.team);
|
|
1043
|
+
if (!team) {
|
|
1044
|
+
return json({
|
|
1045
|
+
error: `Team '${args.team}' not found.`,
|
|
1046
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
const info = visaInfo.find((v) => v.team_id === team.id);
|
|
1050
|
+
if (!info) {
|
|
1051
|
+
return json({
|
|
1052
|
+
error: `No visa data available for ${team.name}.`,
|
|
1053
|
+
team: { id: team.id, name: team.name, flag_emoji: team.flag_emoji },
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
const requirements = args.host_country
|
|
1057
|
+
? info.entry_requirements.filter((r) => r.country === args.host_country)
|
|
1058
|
+
: info.entry_requirements;
|
|
1059
|
+
return json({
|
|
1060
|
+
team: {
|
|
1061
|
+
id: team.id,
|
|
1062
|
+
name: team.name,
|
|
1063
|
+
flag_emoji: team.flag_emoji,
|
|
1064
|
+
group: team.group,
|
|
1065
|
+
},
|
|
1066
|
+
nationality: info.nationality,
|
|
1067
|
+
passport_country: info.passport_country,
|
|
1068
|
+
entry_requirements: requirements,
|
|
1069
|
+
related_tools: [
|
|
1070
|
+
"Use get_city_guide for travel tips in each host city",
|
|
1071
|
+
"Use get_matches with team filter to see where this team plays",
|
|
1072
|
+
"Use get_venues to check which countries host their matches",
|
|
1073
|
+
],
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
// ── Tool: get_fan_zones ─────────────────────────────────────────────
|
|
1077
|
+
server.registerTool("get_fan_zones", {
|
|
1078
|
+
annotations: { readOnlyHint: true },
|
|
1079
|
+
title: "Get Fan Zones",
|
|
1080
|
+
description: "Official FIFA Fan Festival and fan zone locations for World Cup 2026 host cities. Returns venue details, capacity, hours, activities, transportation tips, and amenities. Filter by city, country, or match venue.",
|
|
1081
|
+
inputSchema: z.object({
|
|
1082
|
+
city: z
|
|
1083
|
+
.string()
|
|
1084
|
+
.optional()
|
|
1085
|
+
.describe("Filter by city name (e.g. 'Dallas', 'Mexico City', 'Toronto'). Case-insensitive."),
|
|
1086
|
+
country: z
|
|
1087
|
+
.enum(["USA", "Mexico", "Canada"])
|
|
1088
|
+
.optional()
|
|
1089
|
+
.describe("Filter by host country."),
|
|
1090
|
+
venue_id: z
|
|
1091
|
+
.string()
|
|
1092
|
+
.optional()
|
|
1093
|
+
.describe("Filter by match venue ID (e.g. 'metlife', 'azteca'). Returns fan zones associated with that stadium's city."),
|
|
1094
|
+
}),
|
|
1095
|
+
}, async (args) => {
|
|
1096
|
+
let results = fanZones;
|
|
1097
|
+
if (args.venue_id) {
|
|
1098
|
+
results = results.filter((fz) => fz.venue_id === args.venue_id);
|
|
1099
|
+
}
|
|
1100
|
+
if (args.country) {
|
|
1101
|
+
results = results.filter((fz) => fz.country === args.country);
|
|
1102
|
+
}
|
|
1103
|
+
if (args.city) {
|
|
1104
|
+
const city = args.city.toLowerCase();
|
|
1105
|
+
const cityMatches = results.filter((fz) => fz.city.toLowerCase().includes(city));
|
|
1106
|
+
if (cityMatches.length > 0) {
|
|
1107
|
+
// Also include sibling zones sharing the same venue_id (e.g. NYNJ metro has 3 zones)
|
|
1108
|
+
const venueIds = new Set(cityMatches.map((fz) => fz.venue_id));
|
|
1109
|
+
results = results.filter((fz) => venueIds.has(fz.venue_id));
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
// Try matching via venue name/city for alternate names
|
|
1113
|
+
const matchingVenue = venues.find((v) => v.city.toLowerCase().includes(city) || v.name.toLowerCase().includes(city));
|
|
1114
|
+
results = matchingVenue
|
|
1115
|
+
? results.filter((fz) => fz.venue_id === matchingVenue.id)
|
|
1116
|
+
: [];
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (results.length === 0) {
|
|
1120
|
+
return json({
|
|
1121
|
+
error: "No fan zones found matching your criteria.",
|
|
1122
|
+
suggestion: "Try get_fan_zones without filters to see all 18 fan zones, or use get_venues to find valid venue IDs.",
|
|
1123
|
+
available_countries: ["USA", "Mexico", "Canada"],
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return json({
|
|
1127
|
+
count: results.length,
|
|
1128
|
+
fan_zones: results.map((fz) => ({
|
|
1129
|
+
id: fz.id,
|
|
1130
|
+
name: fz.name,
|
|
1131
|
+
city: fz.city,
|
|
1132
|
+
country: fz.country,
|
|
1133
|
+
location: fz.location,
|
|
1134
|
+
address: fz.address,
|
|
1135
|
+
coordinates: fz.coordinates,
|
|
1136
|
+
capacity: fz.capacity,
|
|
1137
|
+
free_entry: fz.free_entry,
|
|
1138
|
+
hours: fz.hours,
|
|
1139
|
+
activities: fz.activities,
|
|
1140
|
+
highlights: fz.highlights,
|
|
1141
|
+
transportation: fz.transportation,
|
|
1142
|
+
amenities: fz.amenities,
|
|
1143
|
+
family_friendly: fz.family_friendly,
|
|
1144
|
+
status: fz.status,
|
|
1145
|
+
match_venue: venueName(fz.venue_id),
|
|
1146
|
+
})),
|
|
1147
|
+
related_tools: [
|
|
1148
|
+
"Use get_city_guide for full travel info on any host city",
|
|
1149
|
+
"Use get_venues to see stadium details",
|
|
1150
|
+
"Use get_visa_info for entry requirements",
|
|
1151
|
+
],
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
// ── Tool: get_news ──────────────────────────────────────────────────
|
|
1155
|
+
const newsCategories = [
|
|
1156
|
+
"roster", "venue", "schedule", "injury", "analysis",
|
|
1157
|
+
"transfer", "qualifying", "fan-content", "logistics", "general",
|
|
1158
|
+
];
|
|
1159
|
+
server.registerTool("get_news", {
|
|
1160
|
+
annotations: { readOnlyHint: true },
|
|
1161
|
+
title: "Get News",
|
|
1162
|
+
description: "Latest FIFA World Cup 2026 news from ESPN, BBC Sport, and Reddit. Auto-updated daily by the WC26 Scout Agent. Filter by team, category, or both. Returns AI-generated summaries and source links.",
|
|
1163
|
+
inputSchema: z.object({
|
|
1164
|
+
team: z
|
|
1165
|
+
.string()
|
|
1166
|
+
.optional()
|
|
1167
|
+
.describe("Team ID or FIFA code (e.g. 'usa', 'BRA'). Filters news mentioning this team."),
|
|
1168
|
+
category: z
|
|
1169
|
+
.enum(newsCategories)
|
|
1170
|
+
.optional()
|
|
1171
|
+
.describe("News category to filter by."),
|
|
1172
|
+
limit: z
|
|
1173
|
+
.number()
|
|
1174
|
+
.optional()
|
|
1175
|
+
.describe("Max articles to return (default 10, max 50)."),
|
|
1176
|
+
}),
|
|
1177
|
+
}, async (args) => {
|
|
1178
|
+
if (news.length === 0) {
|
|
1179
|
+
return json({
|
|
1180
|
+
count: 0,
|
|
1181
|
+
articles: [],
|
|
1182
|
+
note: "No news articles available yet. The Scout Agent runs daily to fetch new articles.",
|
|
1183
|
+
related_tools: [
|
|
1184
|
+
"Use what_to_know_now for a tournament briefing",
|
|
1185
|
+
"Use get_teams to browse participating teams",
|
|
1186
|
+
],
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
let filtered = news;
|
|
1190
|
+
if (args.team) {
|
|
1191
|
+
const team = resolveTeam(args.team);
|
|
1192
|
+
if (!team) {
|
|
1193
|
+
return json({
|
|
1194
|
+
error: `Team '${args.team}' not found.`,
|
|
1195
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
filtered = filtered.filter((n) => n.related_teams.includes(team.id));
|
|
1199
|
+
}
|
|
1200
|
+
if (args.category) {
|
|
1201
|
+
filtered = filtered.filter((n) => n.categories.includes(args.category));
|
|
1202
|
+
}
|
|
1203
|
+
const limit = Math.min(Math.max(args.limit ?? 10, 1), 50);
|
|
1204
|
+
const result = filtered.slice(0, limit);
|
|
1205
|
+
return json({
|
|
1206
|
+
count: result.length,
|
|
1207
|
+
total_available: filtered.length,
|
|
1208
|
+
articles: result.map((n) => ({
|
|
1209
|
+
id: n.id,
|
|
1210
|
+
title: n.title,
|
|
1211
|
+
date: n.date,
|
|
1212
|
+
source: n.source,
|
|
1213
|
+
url: n.url,
|
|
1214
|
+
summary: n.summary,
|
|
1215
|
+
categories: n.categories,
|
|
1216
|
+
related_teams: n.related_teams,
|
|
1217
|
+
})),
|
|
1218
|
+
related_tools: [
|
|
1219
|
+
"Use get_team_profile to learn more about a mentioned team",
|
|
1220
|
+
"Use get_matches to check a team's schedule",
|
|
1221
|
+
"Use what_to_know_now for a full tournament briefing",
|
|
1222
|
+
],
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
// ── Tool: get_injuries ────────────────────────────────────────────────
|
|
1226
|
+
const injuryStatuses = ["out", "doubtful", "recovering", "fit"];
|
|
1227
|
+
server.registerTool("get_injuries", {
|
|
1228
|
+
annotations: { readOnlyHint: true },
|
|
1229
|
+
title: "Get Injury Report",
|
|
1230
|
+
description: "Key player injury tracker for the FIFA World Cup 2026. Track availability of star players across all 48 teams. Filter by team or injury status (out, doubtful, recovering, fit).",
|
|
1231
|
+
inputSchema: z.object({
|
|
1232
|
+
team: z
|
|
1233
|
+
.string()
|
|
1234
|
+
.optional()
|
|
1235
|
+
.describe("Team ID or FIFA code (e.g. 'usa', 'BRA'). Filters to injuries for this team."),
|
|
1236
|
+
status: z
|
|
1237
|
+
.enum(injuryStatuses)
|
|
1238
|
+
.optional()
|
|
1239
|
+
.describe("Filter by injury status: out, doubtful, recovering, or fit."),
|
|
1240
|
+
}),
|
|
1241
|
+
}, async (args) => {
|
|
1242
|
+
let filtered = injuries;
|
|
1243
|
+
if (args.team) {
|
|
1244
|
+
const team = resolveTeam(args.team);
|
|
1245
|
+
if (!team) {
|
|
1246
|
+
return json({
|
|
1247
|
+
error: `Team '${args.team}' not found.`,
|
|
1248
|
+
suggestion: "Use the get_teams tool to see all available teams and their IDs.",
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
filtered = filtered.filter((i) => i.team_id === team.id);
|
|
1252
|
+
}
|
|
1253
|
+
if (args.status) {
|
|
1254
|
+
filtered = filtered.filter((i) => i.status === args.status);
|
|
1255
|
+
}
|
|
1256
|
+
return json({
|
|
1257
|
+
count: filtered.length,
|
|
1258
|
+
injuries: filtered.map((i) => {
|
|
1259
|
+
const team = teams.find((t) => t.id === i.team_id);
|
|
1260
|
+
return {
|
|
1261
|
+
player: i.player,
|
|
1262
|
+
team: team ? `${team.flag_emoji} ${team.name}` : i.team_id,
|
|
1263
|
+
team_id: i.team_id,
|
|
1264
|
+
position: i.position,
|
|
1265
|
+
injury: i.injury,
|
|
1266
|
+
status: i.status,
|
|
1267
|
+
expected_return: i.expected_return,
|
|
1268
|
+
last_updated: i.last_updated,
|
|
1269
|
+
source: i.source,
|
|
1270
|
+
};
|
|
1271
|
+
}),
|
|
1272
|
+
related_tools: [
|
|
1273
|
+
"Use get_team_profile to see a team's full squad and key players",
|
|
1274
|
+
"Use get_news for the latest injury headlines",
|
|
1275
|
+
"Use get_odds to see how injuries affect tournament predictions",
|
|
1276
|
+
],
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
// ── Tool: get_odds ───────────────────────────────────────────────────
|
|
1280
|
+
server.registerTool("get_odds", {
|
|
1281
|
+
annotations: { readOnlyHint: true },
|
|
1282
|
+
title: "Get Tournament Odds",
|
|
1283
|
+
description: "FIFA World Cup 2026 betting odds and predictions. Tournament winner odds, Golden Boot favorites, group-by-group predictions, and dark horse picks. Illustrative odds aggregated from major bookmakers.",
|
|
1284
|
+
inputSchema: z.object({
|
|
1285
|
+
category: z
|
|
1286
|
+
.enum(["winner", "golden_boot", "groups", "dark_horses", "all"])
|
|
1287
|
+
.optional()
|
|
1288
|
+
.describe("Which odds category to return. Defaults to 'all'."),
|
|
1289
|
+
group: z
|
|
1290
|
+
.string()
|
|
1291
|
+
.optional()
|
|
1292
|
+
.describe("Specific group (A-L) for group predictions."),
|
|
1293
|
+
}),
|
|
1294
|
+
}, async (args) => {
|
|
1295
|
+
const category = args.category ?? "all";
|
|
1296
|
+
const enrichTeam = (tid) => {
|
|
1297
|
+
const t = teams.find((t) => t.id === tid);
|
|
1298
|
+
return t ? `${t.flag_emoji} ${t.name}` : tid;
|
|
1299
|
+
};
|
|
1300
|
+
const result = {
|
|
1301
|
+
last_updated: tournamentOdds.last_updated,
|
|
1302
|
+
source: tournamentOdds.source,
|
|
1303
|
+
};
|
|
1304
|
+
if (category === "winner" || category === "all") {
|
|
1305
|
+
result.tournament_winner = tournamentOdds.tournament_winner.map((o) => ({
|
|
1306
|
+
team: enrichTeam(o.team_id),
|
|
1307
|
+
team_id: o.team_id,
|
|
1308
|
+
odds: o.odds,
|
|
1309
|
+
implied_probability: o.implied_probability,
|
|
1310
|
+
}));
|
|
1311
|
+
}
|
|
1312
|
+
if (category === "golden_boot" || category === "all") {
|
|
1313
|
+
result.golden_boot = tournamentOdds.golden_boot.map((o) => ({
|
|
1314
|
+
player: o.player,
|
|
1315
|
+
team: enrichTeam(o.team_id),
|
|
1316
|
+
odds: o.odds,
|
|
1317
|
+
}));
|
|
1318
|
+
}
|
|
1319
|
+
if (category === "groups" || category === "all") {
|
|
1320
|
+
let groupPreds = tournamentOdds.group_predictions;
|
|
1321
|
+
if (args.group) {
|
|
1322
|
+
groupPreds = groupPreds.filter((g) => g.group.toUpperCase() === args.group.toUpperCase());
|
|
1323
|
+
}
|
|
1324
|
+
result.group_predictions = groupPreds.map((g) => ({
|
|
1325
|
+
group: `Group ${g.group}`,
|
|
1326
|
+
favorites: g.favorites.map(enrichTeam),
|
|
1327
|
+
dark_horse: enrichTeam(g.dark_horse),
|
|
1328
|
+
narrative: g.narrative,
|
|
1329
|
+
}));
|
|
1330
|
+
}
|
|
1331
|
+
if (category === "dark_horses" || category === "all") {
|
|
1332
|
+
result.dark_horses = tournamentOdds.dark_horses.map((d) => ({
|
|
1333
|
+
team: enrichTeam(d.team_id),
|
|
1334
|
+
team_id: d.team_id,
|
|
1335
|
+
reason: d.reason,
|
|
1336
|
+
}));
|
|
1337
|
+
}
|
|
1338
|
+
result.related_tools = [
|
|
1339
|
+
"Use get_team_profile to research any team in depth",
|
|
1340
|
+
"Use get_injuries to check key player availability",
|
|
1341
|
+
"Use get_historical_matchups to see head-to-head records",
|
|
1342
|
+
];
|
|
1343
|
+
return json(result);
|
|
1344
|
+
});
|
|
1345
|
+
// ── Tool: compare_teams ─────────────────────────────────────────────
|
|
1346
|
+
server.registerTool("compare_teams", {
|
|
1347
|
+
annotations: { readOnlyHint: true },
|
|
1348
|
+
title: "Compare Teams",
|
|
1349
|
+
description: "Side-by-side comparison of any two FIFA World Cup 2026 teams. Rankings, odds, key players, group difficulty, injury concerns, and head-to-head history — all in one view. Accepts team ID, FIFA code, or country name.",
|
|
1350
|
+
inputSchema: z.object({
|
|
1351
|
+
team_a: z
|
|
1352
|
+
.string()
|
|
1353
|
+
.describe("First team — ID, FIFA code, or name (e.g. 'bra', 'ARG', 'england'). Case-insensitive."),
|
|
1354
|
+
team_b: z
|
|
1355
|
+
.string()
|
|
1356
|
+
.describe("Second team — ID, FIFA code, or name (e.g. 'eng', 'FRA', 'germany'). Case-insensitive."),
|
|
1357
|
+
}),
|
|
1358
|
+
}, async (args) => {
|
|
1359
|
+
const teamA = resolveTeam(args.team_a);
|
|
1360
|
+
const teamB = resolveTeam(args.team_b);
|
|
1361
|
+
if (!teamA)
|
|
1362
|
+
return json({ error: `Team '${args.team_a}' not found.`, suggestion: "Use get_teams to see all available teams." });
|
|
1363
|
+
if (!teamB)
|
|
1364
|
+
return json({ error: `Team '${args.team_b}' not found.`, suggestion: "Use get_teams to see all available teams." });
|
|
1365
|
+
if (teamA.id === teamB.id)
|
|
1366
|
+
return json({ error: "Both inputs resolve to the same team." });
|
|
1367
|
+
function buildProfile(team) {
|
|
1368
|
+
const profile = teamProfiles.find((p) => p.team_id === team.id);
|
|
1369
|
+
const teamInjuries = injuries.filter((i) => i.team_id === team.id);
|
|
1370
|
+
const winnerOdds = tournamentOdds.tournament_winner.find((o) => o.team_id === team.id);
|
|
1371
|
+
const darkHorse = tournamentOdds.dark_horses.find((d) => d.team_id === team.id);
|
|
1372
|
+
const groupPred = tournamentOdds.group_predictions.find((g) => g.group === team.group);
|
|
1373
|
+
const groupTeams = teams.filter((t) => t.group === team.group && t.code !== "TBD");
|
|
1374
|
+
const avgGroupRanking = groupTeams.length > 0
|
|
1375
|
+
? Math.round(groupTeams.reduce((s, t) => s + (t.fifa_ranking ?? 100), 0) / groupTeams.length)
|
|
1376
|
+
: null;
|
|
1377
|
+
return {
|
|
1378
|
+
team: `${team.flag_emoji} ${team.name}`,
|
|
1379
|
+
team_id: team.id,
|
|
1380
|
+
group: team.group,
|
|
1381
|
+
confederation: team.confederation,
|
|
1382
|
+
fifa_ranking: team.fifa_ranking ?? null,
|
|
1383
|
+
is_host: team.is_host,
|
|
1384
|
+
coach: profile?.coach ?? "Unknown",
|
|
1385
|
+
playing_style: profile?.playing_style ?? null,
|
|
1386
|
+
key_players: profile?.key_players.slice(0, 5).map((p) => `${p.name} (${p.position}, ${p.club})`) ?? [],
|
|
1387
|
+
world_cup_history: profile?.world_cup_history ?? null,
|
|
1388
|
+
winner_odds: winnerOdds ? { odds: winnerOdds.odds, implied_probability: winnerOdds.implied_probability } : null,
|
|
1389
|
+
dark_horse_pick: darkHorse?.reason ?? null,
|
|
1390
|
+
group_difficulty: {
|
|
1391
|
+
avg_ranking: avgGroupRanking,
|
|
1392
|
+
group_prediction: groupPred ? {
|
|
1393
|
+
favorites: groupPred.favorites.map((f) => teamName(f)),
|
|
1394
|
+
dark_horse: teamName(groupPred.dark_horse),
|
|
1395
|
+
} : null,
|
|
1396
|
+
},
|
|
1397
|
+
injuries: teamInjuries.map((i) => ({
|
|
1398
|
+
player: i.player,
|
|
1399
|
+
status: i.status,
|
|
1400
|
+
injury: i.injury,
|
|
1401
|
+
expected_return: i.expected_return,
|
|
1402
|
+
})),
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
// Head-to-head
|
|
1406
|
+
const [first, second] = [teamA, teamB].sort((a, b) => a.id.localeCompare(b.id));
|
|
1407
|
+
const h2h = historicalMatchups.find((h) => h.team_a === first.id && h.team_b === second.id);
|
|
1408
|
+
const upcoming = matches.find((m) => (m.home_team_id === teamA.id && m.away_team_id === teamB.id) ||
|
|
1409
|
+
(m.home_team_id === teamB.id && m.away_team_id === teamA.id));
|
|
1410
|
+
return json({
|
|
1411
|
+
comparison: {
|
|
1412
|
+
[teamA.id]: buildProfile(teamA),
|
|
1413
|
+
[teamB.id]: buildProfile(teamB),
|
|
1414
|
+
},
|
|
1415
|
+
head_to_head: h2h ? {
|
|
1416
|
+
total_matches: h2h.total_matches,
|
|
1417
|
+
[`${first.name}_wins`]: h2h.team_a_wins,
|
|
1418
|
+
draws: h2h.draws,
|
|
1419
|
+
[`${second.name}_wins`]: h2h.team_b_wins,
|
|
1420
|
+
summary: h2h.summary,
|
|
1421
|
+
} : { total_matches: 0, summary: `${first.name} and ${second.name} have never met at a World Cup.` },
|
|
1422
|
+
upcoming_2026_match: upcoming ? {
|
|
1423
|
+
date: upcoming.date,
|
|
1424
|
+
time_utc: upcoming.time_utc,
|
|
1425
|
+
venue: venueName(upcoming.venue_id),
|
|
1426
|
+
round: upcoming.round,
|
|
1427
|
+
} : null,
|
|
1428
|
+
related_tools: [
|
|
1429
|
+
"Use get_team_profile for a deeper dive on either team",
|
|
1430
|
+
"Use get_historical_matchups for full match-by-match history",
|
|
1431
|
+
"Use get_injuries for complete injury details",
|
|
1432
|
+
"Use get_odds for full tournament predictions",
|
|
1433
|
+
],
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
// ── Tool: get_standings ─────────────────────────────────────────────
|
|
1437
|
+
server.registerTool("get_standings", {
|
|
1438
|
+
annotations: { readOnlyHint: true },
|
|
1439
|
+
title: "Get Group Standings",
|
|
1440
|
+
description: "Group power rankings for the FIFA World Cup 2026. Before the tournament starts, returns pre-tournament strength ratings for each group based on FIFA rankings, betting odds, and predictions. Filter by group (A-L) or get all 12 groups at once.",
|
|
1441
|
+
inputSchema: z.object({
|
|
1442
|
+
group: z
|
|
1443
|
+
.string()
|
|
1444
|
+
.optional()
|
|
1445
|
+
.describe("Specific group letter (A-L). If omitted, returns all 12 groups."),
|
|
1446
|
+
}),
|
|
1447
|
+
}, async (args) => {
|
|
1448
|
+
let targetGroups = groups;
|
|
1449
|
+
if (args.group) {
|
|
1450
|
+
const gid = args.group.toUpperCase();
|
|
1451
|
+
targetGroups = groups.filter((g) => g.id === gid);
|
|
1452
|
+
if (targetGroups.length === 0) {
|
|
1453
|
+
return json({
|
|
1454
|
+
error: `Group '${args.group}' not found. Valid groups are A through L.`,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const result = targetGroups.map((g) => {
|
|
1459
|
+
const groupTeams = g.teams
|
|
1460
|
+
.map((tid) => teams.find((t) => t.id === tid))
|
|
1461
|
+
.filter(Boolean);
|
|
1462
|
+
const ranked = groupTeams.map((t) => {
|
|
1463
|
+
const winnerOdds = tournamentOdds.tournament_winner.find((o) => o.team_id === t.id);
|
|
1464
|
+
const darkHorse = tournamentOdds.dark_horses.find((d) => d.team_id === t.id);
|
|
1465
|
+
const profile = teamProfiles.find((p) => p.team_id === t.id);
|
|
1466
|
+
const teamInjuries = injuries.filter((i) => i.team_id === t.id && i.status !== "fit");
|
|
1467
|
+
// Power score: lower is better. Ranking contributes directly; odds boost reduces it.
|
|
1468
|
+
const ranking = t.fifa_ranking ?? 100;
|
|
1469
|
+
const oddsBoost = winnerOdds
|
|
1470
|
+
? Math.max(0, 50 - parseFloat(winnerOdds.implied_probability))
|
|
1471
|
+
: 50;
|
|
1472
|
+
const powerScore = ranking + oddsBoost;
|
|
1473
|
+
return {
|
|
1474
|
+
team: `${t.flag_emoji} ${t.name}`,
|
|
1475
|
+
team_id: t.id,
|
|
1476
|
+
fifa_ranking: t.fifa_ranking ?? null,
|
|
1477
|
+
confederation: t.confederation,
|
|
1478
|
+
is_host: t.is_host,
|
|
1479
|
+
winner_odds: winnerOdds ? { odds: winnerOdds.odds, implied_probability: winnerOdds.implied_probability } : null,
|
|
1480
|
+
dark_horse_pick: darkHorse?.reason ?? null,
|
|
1481
|
+
world_cup_pedigree: profile?.world_cup_history
|
|
1482
|
+
? `${profile.world_cup_history.appearances} appearances, ${profile.world_cup_history.titles} titles, best: ${profile.world_cup_history.best_result}`
|
|
1483
|
+
: null,
|
|
1484
|
+
key_injuries: teamInjuries.length > 0
|
|
1485
|
+
? teamInjuries.map((i) => `${i.player} (${i.status})`)
|
|
1486
|
+
: null,
|
|
1487
|
+
_power_score: powerScore,
|
|
1488
|
+
};
|
|
1489
|
+
}).sort((a, b) => a._power_score - b._power_score);
|
|
1490
|
+
const groupPred = tournamentOdds.group_predictions.find((gp) => gp.group === g.id);
|
|
1491
|
+
const confirmedTeams = ranked.filter((t) => !t.team_id.startsWith("tbd"));
|
|
1492
|
+
const avgRanking = confirmedTeams.length > 0
|
|
1493
|
+
? Math.round(confirmedTeams.reduce((s, t) => s + (t.fifa_ranking ?? 100), 0) / confirmedTeams.length)
|
|
1494
|
+
: null;
|
|
1495
|
+
// Collect head-to-head matchups within the group
|
|
1496
|
+
const groupH2H = [];
|
|
1497
|
+
for (let i = 0; i < groupTeams.length; i++) {
|
|
1498
|
+
for (let j = i + 1; j < groupTeams.length; j++) {
|
|
1499
|
+
const [a, b] = [groupTeams[i].id, groupTeams[j].id].sort();
|
|
1500
|
+
const h2h = historicalMatchups.find((h) => h.team_a === a && h.team_b === b);
|
|
1501
|
+
if (h2h) {
|
|
1502
|
+
groupH2H.push({
|
|
1503
|
+
matchup: `${teamName(a)} vs ${teamName(b)}`,
|
|
1504
|
+
summary: h2h.summary,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
group: `Group ${g.id}`,
|
|
1511
|
+
difficulty_rating: avgRanking,
|
|
1512
|
+
prediction: groupPred ? {
|
|
1513
|
+
favorites: groupPred.favorites.map((f) => teamName(f)),
|
|
1514
|
+
dark_horse: teamName(groupPred.dark_horse),
|
|
1515
|
+
narrative: groupPred.narrative,
|
|
1516
|
+
} : null,
|
|
1517
|
+
teams: ranked.map(({ _power_score, ...rest }) => rest),
|
|
1518
|
+
head_to_head_history: groupH2H.length > 0 ? groupH2H : null,
|
|
1519
|
+
};
|
|
1520
|
+
});
|
|
1521
|
+
return json({
|
|
1522
|
+
count: result.length,
|
|
1523
|
+
note: "Teams ranked by pre-tournament power rating (FIFA ranking + betting odds). Lower difficulty_rating = harder group.",
|
|
1524
|
+
groups: result,
|
|
1525
|
+
related_tools: [
|
|
1526
|
+
"Use get_team_profile for a deep dive on any team",
|
|
1527
|
+
"Use get_odds for full tournament predictions",
|
|
1528
|
+
"Use get_historical_matchups for complete head-to-head records",
|
|
1529
|
+
"Use get_bracket to see the knockout path from each group",
|
|
1530
|
+
],
|
|
1531
|
+
});
|
|
1532
|
+
});
|
|
1533
|
+
// ── Tool: get_bracket ──────────────────────────────────────────────
|
|
1534
|
+
server.registerTool("get_bracket", {
|
|
1535
|
+
annotations: { readOnlyHint: true },
|
|
1536
|
+
title: "Get Knockout Bracket",
|
|
1537
|
+
description: "Visualize the FIFA World Cup 2026 knockout bracket. Shows the full path from Round of 32 through the Final, including which group positions feed into each match, venues, and dates. Optionally filter by a specific round.",
|
|
1538
|
+
inputSchema: z.object({
|
|
1539
|
+
round: z
|
|
1540
|
+
.enum(["Round of 32", "Round of 16", "Quarter-final", "Semi-final", "Third-place play-off", "Final", "all"])
|
|
1541
|
+
.optional()
|
|
1542
|
+
.describe("Specific knockout round to show. Defaults to 'all'."),
|
|
1543
|
+
}),
|
|
1544
|
+
}, async (args) => {
|
|
1545
|
+
const knockoutRounds = [
|
|
1546
|
+
"Round of 32", "Round of 16", "Quarter-final", "Semi-final",
|
|
1547
|
+
"Third-place play-off", "Final",
|
|
1548
|
+
];
|
|
1549
|
+
const targetRound = args.round ?? "all";
|
|
1550
|
+
const knockoutMatches = matches.filter((m) => knockoutRounds.includes(m.round));
|
|
1551
|
+
function describePlaceholder(p) {
|
|
1552
|
+
if (!p)
|
|
1553
|
+
return "TBD";
|
|
1554
|
+
// Winner/Loser of match
|
|
1555
|
+
if (p.startsWith("W"))
|
|
1556
|
+
return `Winner of Match ${p.slice(1)}`;
|
|
1557
|
+
if (p.startsWith("L"))
|
|
1558
|
+
return `Loser of Match ${p.slice(1)}`;
|
|
1559
|
+
// Group position like "1A", "2B"
|
|
1560
|
+
if (/^[12][A-L]$/.test(p)) {
|
|
1561
|
+
const pos = p[0] === "1" ? "Winner" : "Runner-up";
|
|
1562
|
+
return `${pos} of Group ${p[1]}`;
|
|
1563
|
+
}
|
|
1564
|
+
// Best third-place like "3C/D/E"
|
|
1565
|
+
if (p.startsWith("3")) {
|
|
1566
|
+
const groups = p.slice(1);
|
|
1567
|
+
return `Best 3rd from Groups ${groups}`;
|
|
1568
|
+
}
|
|
1569
|
+
return p;
|
|
1570
|
+
}
|
|
1571
|
+
const enrichedKnockout = knockoutMatches.map((m) => {
|
|
1572
|
+
const venue = venues.find((v) => v.id === m.venue_id);
|
|
1573
|
+
return {
|
|
1574
|
+
match_number: m.match_number,
|
|
1575
|
+
match_id: m.id,
|
|
1576
|
+
round: m.round,
|
|
1577
|
+
date: m.date,
|
|
1578
|
+
time_utc: m.time_utc,
|
|
1579
|
+
venue: venue ? `${venue.name}, ${venue.city}` : m.venue_id,
|
|
1580
|
+
home: m.home_team_id ? teamName(m.home_team_id) : describePlaceholder(m.home_placeholder),
|
|
1581
|
+
away: m.away_team_id ? teamName(m.away_team_id) : describePlaceholder(m.away_placeholder),
|
|
1582
|
+
feeds_into: (() => {
|
|
1583
|
+
const nextAsHome = knockoutMatches.find((n) => n.home_placeholder === `W${m.match_number}`);
|
|
1584
|
+
const nextAsAway = knockoutMatches.find((n) => n.away_placeholder === `W${m.match_number}`);
|
|
1585
|
+
const next = nextAsHome || nextAsAway;
|
|
1586
|
+
return next ? `Winner → Match ${next.match_number} (${next.round})` : null;
|
|
1587
|
+
})(),
|
|
1588
|
+
};
|
|
1589
|
+
});
|
|
1590
|
+
// Group by round
|
|
1591
|
+
const byRound = {};
|
|
1592
|
+
for (const m of enrichedKnockout) {
|
|
1593
|
+
if (!byRound[m.round])
|
|
1594
|
+
byRound[m.round] = [];
|
|
1595
|
+
byRound[m.round].push(m);
|
|
1596
|
+
}
|
|
1597
|
+
// Filter if specific round requested
|
|
1598
|
+
let result;
|
|
1599
|
+
if (targetRound !== "all") {
|
|
1600
|
+
const roundKey = targetRound === "Final" ? "Final" : targetRound;
|
|
1601
|
+
const roundMatches = byRound[roundKey];
|
|
1602
|
+
if (!roundMatches) {
|
|
1603
|
+
return json({ error: `No matches found for '${targetRound}'.` });
|
|
1604
|
+
}
|
|
1605
|
+
result = {
|
|
1606
|
+
round: roundKey,
|
|
1607
|
+
matches: roundMatches,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
// Build bracket paths: show which group feeds where
|
|
1612
|
+
const bracketPaths = groups.map((g) => {
|
|
1613
|
+
const winnerId = `1${g.id}`;
|
|
1614
|
+
const runnerUpId = `2${g.id}`;
|
|
1615
|
+
const winnerMatch = knockoutMatches.find((m) => m.home_placeholder === winnerId || m.away_placeholder === winnerId);
|
|
1616
|
+
const runnerUpMatch = knockoutMatches.find((m) => m.home_placeholder === runnerUpId || m.away_placeholder === runnerUpId);
|
|
1617
|
+
return {
|
|
1618
|
+
group: `Group ${g.id}`,
|
|
1619
|
+
teams: g.teams.map((tid) => teamName(tid)),
|
|
1620
|
+
winner_enters: winnerMatch
|
|
1621
|
+
? `Match ${winnerMatch.match_number} (${winnerMatch.round})`
|
|
1622
|
+
: "TBD",
|
|
1623
|
+
runner_up_enters: runnerUpMatch
|
|
1624
|
+
? `Match ${runnerUpMatch.match_number} (${runnerUpMatch.round})`
|
|
1625
|
+
: "TBD",
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1628
|
+
result = {
|
|
1629
|
+
bracket_overview: bracketPaths,
|
|
1630
|
+
rounds: Object.fromEntries(knockoutRounds.map((r) => [r, byRound[r] ?? []])),
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
return json({
|
|
1634
|
+
...result,
|
|
1635
|
+
key_dates: {
|
|
1636
|
+
round_of_32: "June 29 - July 3",
|
|
1637
|
+
round_of_16: "July 4 - 7",
|
|
1638
|
+
quarter_finals: "July 9 - 11",
|
|
1639
|
+
semi_finals: "July 14 - 15",
|
|
1640
|
+
third_place: "July 18",
|
|
1641
|
+
final: "July 19 (MetLife Stadium, East Rutherford, NJ)",
|
|
1642
|
+
},
|
|
1643
|
+
related_tools: [
|
|
1644
|
+
"Use get_standings to see group power rankings",
|
|
1645
|
+
"Use get_matches to see the full schedule with filters",
|
|
1646
|
+
"Use get_venues for stadium details",
|
|
1647
|
+
],
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
668
1650
|
// ── Start server ────────────────────────────────────────────────────
|
|
669
1651
|
async function main() {
|
|
670
1652
|
const transport = new StdioServerTransport();
|
|
@@ -674,4 +1656,3 @@ main().catch((err) => {
|
|
|
674
1656
|
console.error("Fatal error:", err);
|
|
675
1657
|
process.exit(1);
|
|
676
1658
|
});
|
|
677
|
-
//# sourceMappingURL=index.js.map
|