@gotza02/sequential-thinking 10000.1.3 → 10000.1.4

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.
@@ -2,7 +2,7 @@
2
2
  * SPORTS MODULE BASE CLASSES
3
3
  * Abstract base classes and interfaces for data providers
4
4
  */
5
- import { Match, Team, Player, TableEntry, APIResponse, ProviderStatus, ProviderType } from './types.js';
5
+ import { Match, Team, Player, TableEntry, APIResponse, ProviderStatus, ProviderType, HeadToHead, MatchOdds } from './types.js';
6
6
  import { CacheService } from './cache.js';
7
7
  /**
8
8
  * Base interface for all data providers
@@ -48,6 +48,18 @@ export interface IDataProvider {
48
48
  * Search for players
49
49
  */
50
50
  searchPlayers(query: string): Promise<APIResponse<Player[]>>;
51
+ /**
52
+ * Fetch head-to-head statistics between two teams
53
+ */
54
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
55
+ /**
56
+ * Fetch recent form (last N matches) for a team
57
+ */
58
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
59
+ /**
60
+ * Fetch odds for a match
61
+ */
62
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
51
63
  }
52
64
  /**
53
65
  * Abstract base class for API-based providers
@@ -84,6 +96,18 @@ export declare abstract class APIProviderBase implements IDataProvider {
84
96
  abstract getPlayer(playerId: string): Promise<APIResponse<Player>>;
85
97
  abstract searchTeams(query: string): Promise<APIResponse<Team[]>>;
86
98
  abstract searchPlayers(query: string): Promise<APIResponse<Player[]>>;
99
+ /**
100
+ * Head-to-head statistics (optional, may not be supported by all providers)
101
+ */
102
+ abstract getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
103
+ /**
104
+ * Team recent form (optional, may not be supported by all providers)
105
+ */
106
+ abstract getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
107
+ /**
108
+ * Match odds (optional, may not be supported by all providers)
109
+ */
110
+ abstract getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
87
111
  /**
88
112
  * Make a rate-limited API call
89
113
  */
@@ -117,6 +141,9 @@ export declare abstract class ScraperProviderBase implements IDataProvider {
117
141
  abstract getPlayer(playerId: string): Promise<APIResponse<Player>>;
118
142
  abstract searchTeams(query: string): Promise<APIResponse<Team[]>>;
119
143
  abstract searchPlayers(query: string): Promise<APIResponse<Player[]>>;
144
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
145
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
146
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
120
147
  /**
121
148
  * Scrape a webpage and extract structured data
122
149
  */
@@ -143,6 +170,9 @@ export declare class FallbackProvider implements IDataProvider {
143
170
  getPlayer(playerId: string): Promise<APIResponse<Player>>;
144
171
  searchTeams(query: string): Promise<APIResponse<Team[]>>;
145
172
  searchPlayers(query: string): Promise<APIResponse<Player[]>>;
173
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
174
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
175
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
146
176
  }
147
177
  /**
148
178
  * Base class for sports tools
@@ -141,6 +141,16 @@ export class ScraperProviderBase {
141
141
  quotaLimit: Infinity,
142
142
  };
143
143
  }
144
+ // Optional methods - may not be implemented by all scrapers
145
+ async getH2H(matchId, team1Id, team2Id) {
146
+ return { success: false, error: 'H2H not available via scraper' };
147
+ }
148
+ async getTeamForm(teamId, last) {
149
+ return { success: false, error: 'Team form not available via scraper' };
150
+ }
151
+ async getMatchOdds(matchId) {
152
+ return { success: false, error: 'Match odds not available via scraper' };
153
+ }
144
154
  /**
145
155
  * Scrape a webpage and extract structured data
146
156
  */
@@ -243,6 +253,21 @@ export class FallbackProvider {
243
253
  return this.cache.getOrSet(cacheKey, () => this.tryProviders(p => p.searchPlayers(query)), 60 * 60 * 1000 // 1 hour
244
254
  );
245
255
  }
256
+ async getH2H(matchId, team1Id, team2Id) {
257
+ const cacheKey = CacheService.generateKey('h2h', matchId);
258
+ return this.cache.getOrSet(cacheKey, () => this.tryProviders(p => p.getH2H(matchId, team1Id, team2Id)), 30 * 60 * 1000 // 30 minutes
259
+ );
260
+ }
261
+ async getTeamForm(teamId, last) {
262
+ const cacheKey = CacheService.generateKey('form', teamId, last?.toString() || '5');
263
+ return this.cache.getOrSet(cacheKey, () => this.tryProviders(p => p.getTeamForm(teamId, last)), 30 * 60 * 1000 // 30 minutes
264
+ );
265
+ }
266
+ async getMatchOdds(matchId) {
267
+ const cacheKey = CacheService.generateKey('odds', matchId);
268
+ return this.cache.getOrSet(cacheKey, () => this.tryProviders(p => p.getMatchOdds(matchId)), 5 * 60 * 1000 // 5 minutes (odds change frequently)
269
+ );
270
+ }
246
271
  }
247
272
  /**
248
273
  * Base class for sports tools
@@ -377,3 +377,154 @@ export interface LiveScoreUpdate {
377
377
  events: MatchEvent[];
378
378
  timestamp: Date;
379
379
  }
380
+ /**
381
+ * Probability range with uncertainty quantification
382
+ */
383
+ export interface ProbabilityRange {
384
+ low: number;
385
+ mid: number;
386
+ high: number;
387
+ }
388
+ /**
389
+ * Data quality assessment
390
+ */
391
+ export interface DataQuality {
392
+ score: number;
393
+ factors: {
394
+ hasAPIData: boolean;
395
+ hasRecentData: boolean;
396
+ sampleSize: 'small' | 'medium' | 'large';
397
+ dataAge: number;
398
+ missingDataPoints: string[];
399
+ };
400
+ }
401
+ /**
402
+ * Value bet recommendation with full analysis
403
+ */
404
+ export interface ValueBetRecommendation {
405
+ selection: string;
406
+ market: '1x2' | 'over_under' | 'btts' | 'asian_handicap';
407
+ odds: number;
408
+ fairOdds: number;
409
+ value: number;
410
+ confidence: number;
411
+ kellyFraction: number;
412
+ recommendedStake: number;
413
+ reasoning: string;
414
+ }
415
+ /**
416
+ * Game scenario for skeptic analysis
417
+ */
418
+ export interface GameScenario {
419
+ trigger: string;
420
+ probability: number;
421
+ impact: 'high' | 'medium' | 'low';
422
+ description: string;
423
+ }
424
+ /**
425
+ * Fatigue assessment
426
+ */
427
+ export interface FatigueAssessment {
428
+ score: number;
429
+ daysRest: number;
430
+ travelDistance?: number;
431
+ squadDepth: number;
432
+ keyInjuries: string[];
433
+ }
434
+ /**
435
+ * Six-persona analysis output structure
436
+ */
437
+ export interface SixPersonaAnalysis {
438
+ dataScientist: {
439
+ homeWinProbability: ProbabilityRange;
440
+ drawProbability: ProbabilityRange;
441
+ awayWinProbability: ProbabilityRange;
442
+ expectedGoals: {
443
+ home: number;
444
+ away: number;
445
+ };
446
+ keyStats: ComparisonMetric[];
447
+ };
448
+ tacticalScout: {
449
+ homeFormation?: string;
450
+ awayFormation?: string;
451
+ styleMatchup: string;
452
+ keyBattles: string[];
453
+ tacticalAdvantage: 'home' | 'away' | 'neutral';
454
+ };
455
+ physio: {
456
+ homeFatigue: FatigueAssessment;
457
+ awayFatigue: FatigueAssessment;
458
+ injuryImpact: string;
459
+ };
460
+ setPieceAnalyst: {
461
+ homeStrength: number;
462
+ awayStrength: number;
463
+ predictedCorners: number;
464
+ aerialAdvantage: 'home' | 'away' | 'neutral';
465
+ };
466
+ insider: {
467
+ marketMovement: string;
468
+ weatherImpact?: string;
469
+ refereeImpact?: string;
470
+ managerialContext?: string;
471
+ };
472
+ skeptic: {
473
+ upsetRisk: number;
474
+ trapIndicators: string[];
475
+ gameScenarios: GameScenario[];
476
+ };
477
+ }
478
+ /**
479
+ * Complete match analysis result (structured output)
480
+ */
481
+ export interface MatchAnalysisResult {
482
+ match: {
483
+ homeTeam: string;
484
+ awayTeam: string;
485
+ league?: string;
486
+ date?: Date;
487
+ matchId?: string;
488
+ };
489
+ dataQuality: DataQuality;
490
+ analysis: SixPersonaAnalysis;
491
+ predictions: {
492
+ matchResult: {
493
+ homeWin: ProbabilityRange;
494
+ draw: ProbabilityRange;
495
+ awayWin: ProbabilityRange;
496
+ };
497
+ overUnder25: {
498
+ over: ProbabilityRange;
499
+ under: ProbabilityRange;
500
+ };
501
+ btts: {
502
+ yes: ProbabilityRange;
503
+ no: ProbabilityRange;
504
+ };
505
+ };
506
+ valueBets: ValueBetRecommendation[];
507
+ verdict: {
508
+ summary: string;
509
+ bestBet: string;
510
+ confidence: number;
511
+ riskLevel: 'low' | 'medium' | 'high';
512
+ };
513
+ metadata: {
514
+ generatedAt: Date;
515
+ dataSources: string[];
516
+ };
517
+ }
518
+ /**
519
+ * API Match data bundle
520
+ */
521
+ export interface APIMatchData {
522
+ match?: Match;
523
+ h2h?: HeadToHead;
524
+ homeForm?: Match[];
525
+ awayForm?: Match[];
526
+ homeStats?: TeamStats;
527
+ awayStats?: TeamStats;
528
+ odds?: MatchOdds;
529
+ lastUpdated?: Date;
530
+ }
@@ -2,7 +2,7 @@
2
2
  * SPORTS MODULE API PROVIDERS
3
3
  * Concrete implementations of football API providers
4
4
  */
5
- import { Match, Team, Player, TableEntry, APIResponse } from '../core/types.js';
5
+ import { Match, Team, Player, TableEntry, APIResponse, HeadToHead, MatchOdds } from '../core/types.js';
6
6
  import { APIProviderBase, FallbackProvider } from '../core/base.js';
7
7
  /**
8
8
  * API-Football Provider (via RapidAPI)
@@ -19,12 +19,17 @@ export declare class APIFootballProvider extends APIProviderBase {
19
19
  getPlayer(playerId: string): Promise<APIResponse<Player>>;
20
20
  searchTeams(query: string): Promise<APIResponse<Team[]>>;
21
21
  searchPlayers(query: string): Promise<APIResponse<Player[]>>;
22
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
23
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
24
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
22
25
  private transformMatch;
23
26
  private transformTeam;
24
27
  private transformPlayer;
25
28
  private transformTableEntry;
26
29
  private mapStatus;
27
30
  private mapPosition;
31
+ private transformH2H;
32
+ private transformOdds;
28
33
  }
29
34
  /**
30
35
  * Football-Data.org Provider
@@ -41,6 +46,9 @@ export declare class FootballDataProvider extends APIProviderBase {
41
46
  getPlayer(playerId: string): Promise<APIResponse<Player>>;
42
47
  searchTeams(query: string): Promise<APIResponse<Team[]>>;
43
48
  searchPlayers(query: string): Promise<APIResponse<Player[]>>;
49
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
50
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
51
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
44
52
  private transformMatchFD;
45
53
  private transformTeamFD;
46
54
  private transformTableEntryFD;
@@ -61,6 +69,9 @@ export declare class SportsDBProvider extends APIProviderBase {
61
69
  getPlayer(playerId: string): Promise<APIResponse<Player>>;
62
70
  searchTeams(query: string): Promise<APIResponse<Team[]>>;
63
71
  searchPlayers(query: string): Promise<APIResponse<Player[]>>;
72
+ getH2H(matchId: string, team1Id?: string, team2Id?: string): Promise<APIResponse<HeadToHead>>;
73
+ getTeamForm(teamId: string, last?: number): Promise<APIResponse<Match[]>>;
74
+ getMatchOdds(matchId: string): Promise<APIResponse<MatchOdds>>;
64
75
  private transformMatchDB;
65
76
  private transformTeamDB;
66
77
  private transformPlayerDB;
@@ -88,6 +88,58 @@ export class APIFootballProvider extends APIProviderBase {
88
88
  }
89
89
  return response;
90
90
  }
91
+ // ============= New Methods for Enhanced Analysis =============
92
+ async getH2H(matchId, team1Id, team2Id) {
93
+ const cacheKey = `h2h:api-football:${matchId}`;
94
+ const cached = this.cache.get(cacheKey);
95
+ if (cached)
96
+ return { success: true, data: cached, cached: true };
97
+ // Use team IDs if provided, otherwise fetch match first to get teams
98
+ let homeId = team1Id;
99
+ let awayId = team2Id;
100
+ if (!homeId || !awayId) {
101
+ const matchResult = await this.getMatch(matchId);
102
+ if (!matchResult.success || !matchResult.data) {
103
+ return { success: false, error: 'Could not resolve team IDs for H2H' };
104
+ }
105
+ homeId = matchResult.data.homeTeam.id;
106
+ awayId = matchResult.data.awayTeam.id;
107
+ }
108
+ const response = await this.callAPI(`/fixtures/headtohead?h2h=${homeId}-${awayId}`);
109
+ if (response.success && response.data?.response) {
110
+ const h2h = this.transformH2H(response.data.response, homeId, awayId);
111
+ this.cache.set(cacheKey, h2h);
112
+ return { success: true, data: h2h, provider: 'api-football' };
113
+ }
114
+ return response;
115
+ }
116
+ async getTeamForm(teamId, last = 5) {
117
+ const cacheKey = `form:api-football:${teamId}:${last}`;
118
+ const cached = this.cache.get(cacheKey);
119
+ if (cached)
120
+ return { success: true, data: cached, cached: true };
121
+ const currentYear = new Date().getFullYear();
122
+ const response = await this.callAPI(`/fixtures?team=${teamId}&last=${last}&season=${currentYear}`);
123
+ if (response.success && response.data?.response) {
124
+ const matches = response.data.response.map((m) => this.transformMatch(m));
125
+ this.cache.set(cacheKey, matches);
126
+ return { success: true, data: matches, provider: 'api-football' };
127
+ }
128
+ return response;
129
+ }
130
+ async getMatchOdds(matchId) {
131
+ const cacheKey = `odds:api-football:${matchId}`;
132
+ const cached = this.cache.get(cacheKey);
133
+ if (cached)
134
+ return { success: true, data: cached, cached: true };
135
+ const response = await this.callAPI(`/odds?fixture=${matchId}`);
136
+ if (response.success && response.data?.response?.[0]) {
137
+ const odds = this.transformOdds(response.data.response[0]);
138
+ this.cache.set(cacheKey, odds);
139
+ return { success: true, data: odds, provider: 'api-football' };
140
+ }
141
+ return response;
142
+ }
91
143
  // ============= Transformation Helpers =============
92
144
  transformMatch(data) {
93
145
  return {
@@ -194,6 +246,87 @@ export class APIFootballProvider extends APIProviderBase {
194
246
  return 'FW';
195
247
  return 'SUB';
196
248
  }
249
+ transformH2H(data, homeId, awayId) {
250
+ const matches = data.map(m => this.transformMatch(m));
251
+ let homeWins = 0;
252
+ let draws = 0;
253
+ let awayWins = 0;
254
+ let homeGoals = 0;
255
+ let awayGoals = 0;
256
+ for (const match of matches) {
257
+ if (!match.score)
258
+ continue;
259
+ // Determine which team was home/away in the H2H match
260
+ const isHomeTeam = match.homeTeam.id === homeId;
261
+ if (match.score.home === match.score.away) {
262
+ draws++;
263
+ }
264
+ else if (isHomeTeam) {
265
+ if (match.score.home > match.score.away)
266
+ homeWins++;
267
+ else
268
+ awayWins++;
269
+ }
270
+ else {
271
+ if (match.score.away > match.score.home)
272
+ homeWins++;
273
+ else
274
+ awayWins++;
275
+ }
276
+ homeGoals += isHomeTeam ? match.score.home : match.score.away;
277
+ awayGoals += isHomeTeam ? match.score.away : match.score.home;
278
+ }
279
+ return {
280
+ homeTeam: matches[0]?.homeTeam.id === homeId ? matches[0].homeTeam : matches[0]?.awayTeam,
281
+ awayTeam: matches[0]?.homeTeam.id === awayId ? matches[0].homeTeam : matches[0]?.awayTeam,
282
+ totalMatches: matches.length,
283
+ homeWins,
284
+ draws,
285
+ awayWins,
286
+ homeGoals,
287
+ awayGoals,
288
+ recentMatches: matches.slice(0, 5),
289
+ };
290
+ }
291
+ transformOdds(data) {
292
+ // Find 1x2 odds from the first bookmaker
293
+ const bookmaker = data.bookmakers?.[0];
294
+ const matchWinnerBet = bookmaker?.bets?.find((b) => b.name === 'Match Winner');
295
+ const homeWinOdds = matchWinnerBet?.values?.find((v) => v.value === 'Home')?.odd;
296
+ const drawOdds = matchWinnerBet?.values?.find((v) => v.value === 'Draw')?.odd;
297
+ const awayWinOdds = matchWinnerBet?.values?.find((v) => v.value === 'Away')?.odd;
298
+ // Default odds if not found
299
+ const result = {
300
+ homeWin: parseFloat(homeWinOdds) || 2.0,
301
+ draw: parseFloat(drawOdds) || 3.0,
302
+ awayWin: parseFloat(awayWinOdds) || 2.0,
303
+ provider: bookmaker?.name || 'Unknown',
304
+ timestamp: new Date(data.update || Date.now()),
305
+ };
306
+ // Try to find Asian Handicap odds
307
+ const ahBet = bookmaker?.bets?.find((b) => b.name?.toLowerCase().includes('asian') ||
308
+ b.name?.toLowerCase().includes('handicap'));
309
+ if (ahBet?.values?.[0]) {
310
+ const ahValue = ahBet.values[0];
311
+ result.asianHandicap = {
312
+ home: parseFloat(ahValue.odd) || 2.0,
313
+ away: parseFloat(ahValue.odd) || 2.0,
314
+ line: parseFloat(ahValue.value) || 0,
315
+ };
316
+ }
317
+ // Try to find Over/Under odds
318
+ const ouBet = bookmaker?.bets?.find((b) => b.name?.toLowerCase().includes('over/under'));
319
+ if (ouBet?.values?.[0]) {
320
+ const ouValue = ouBet.values[0];
321
+ const line = parseFloat(ouValue.value?.replace('Over ', '').replace('Under ', '')) || 2.5;
322
+ result.overUnder = {
323
+ line,
324
+ over: parseFloat(ouValue.odd) || 2.0,
325
+ under: parseFloat(ouValue.odd) || 2.0,
326
+ };
327
+ }
328
+ return result;
329
+ }
197
330
  }
198
331
  /**
199
332
  * Football-Data.org Provider
@@ -263,6 +396,25 @@ export class FootballDataProvider extends APIProviderBase {
263
396
  error: 'Search not available via Football-Data.org',
264
397
  };
265
398
  }
399
+ async getH2H(matchId, team1Id, team2Id) {
400
+ return {
401
+ success: false,
402
+ error: 'H2H data not available via Football-Data.org',
403
+ };
404
+ }
405
+ async getTeamForm(teamId, last) {
406
+ // Football-Data.org has limited form data in match endpoints
407
+ return {
408
+ success: false,
409
+ error: 'Team form not directly available via Football-Data.org',
410
+ };
411
+ }
412
+ async getMatchOdds(matchId) {
413
+ return {
414
+ success: false,
415
+ error: 'Odds not available via Football-Data.org',
416
+ };
417
+ }
266
418
  // ============= Transformation Helpers (Football-Data.org format) =============
267
419
  transformMatchFD(data) {
268
420
  return {
@@ -416,6 +568,24 @@ export class SportsDBProvider extends APIProviderBase {
416
568
  }
417
569
  return response;
418
570
  }
571
+ async getH2H(matchId, team1Id, team2Id) {
572
+ return {
573
+ success: false,
574
+ error: 'H2H data not available via TheSportsDB',
575
+ };
576
+ }
577
+ async getTeamForm(teamId, last) {
578
+ return {
579
+ success: false,
580
+ error: 'Team form not directly available via TheSportsDB',
581
+ };
582
+ }
583
+ async getMatchOdds(matchId) {
584
+ return {
585
+ success: false,
586
+ error: 'Odds not available via TheSportsDB',
587
+ };
588
+ }
419
589
  // ============= Transformation Helpers (TheSportsDB format) =============
420
590
  transformMatchDB(data) {
421
591
  return {
@@ -3,8 +3,13 @@
3
3
  * Tools for match analysis and live scores
4
4
  */
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { MatchAnalysisResult } from '../core/types.js';
6
7
  /**
7
8
  * Perform comprehensive match analysis using web search
8
9
  */
9
10
  export declare function performMatchAnalysis(homeTeam: string, awayTeam: string, league?: string, context?: string): Promise<string>;
11
+ /**
12
+ * Perform structured match analysis with JSON output
13
+ */
14
+ export declare function performStructuredMatchAnalysis(homeTeam: string, awayTeam: string, league?: string, context?: string): Promise<MatchAnalysisResult>;
10
15
  export declare function registerMatchTools(server: McpServer): void;
@@ -9,6 +9,7 @@ import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../provid
9
9
  import { getGlobalCache, CacheService } from '../core/cache.js';
10
10
  import { formatMatchesTable, formatScore, formatMatchStatus } from '../utils/formatter.js';
11
11
  import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
12
+ import { logger } from '../../../utils.js';
12
13
  // ============= Helper Functions =============
13
14
  /**
14
15
  * Get date context for search queries
@@ -62,6 +63,200 @@ async function findMatchId(homeTeam, awayTeam, league) {
62
63
  }
63
64
  return null;
64
65
  }
66
+ /**
67
+ * Fetch comprehensive API data for a match
68
+ */
69
+ async function fetchAPIMatchData(api, matchId, homeTeam, awayTeam) {
70
+ try {
71
+ // Fetch all data in parallel
72
+ const [matchResult, h2hResult, oddsResult] = await Promise.all([
73
+ api.getMatch(matchId),
74
+ api.getH2H(matchId),
75
+ api.getMatchOdds(matchId),
76
+ ]);
77
+ let homeForm;
78
+ let awayForm;
79
+ // Get team form if we have match data
80
+ if (matchResult.success && matchResult.data) {
81
+ const [homeFormResult, awayFormResult] = await Promise.all([
82
+ api.getTeamForm(matchResult.data.homeTeam.id, 5),
83
+ api.getTeamForm(matchResult.data.awayTeam.id, 5),
84
+ ]);
85
+ if (homeFormResult.success && homeFormResult.data) {
86
+ homeForm = homeFormResult.data;
87
+ }
88
+ if (awayFormResult.success && awayFormResult.data) {
89
+ awayForm = awayFormResult.data;
90
+ }
91
+ }
92
+ return {
93
+ match: matchResult.success ? matchResult.data : undefined,
94
+ h2h: h2hResult.success ? h2hResult.data : undefined,
95
+ homeForm,
96
+ awayForm,
97
+ odds: oddsResult.success ? oddsResult.data : undefined,
98
+ lastUpdated: new Date(),
99
+ };
100
+ }
101
+ catch (error) {
102
+ logger.error(`Error fetching API match data: ${error}`);
103
+ return null;
104
+ }
105
+ }
106
+ /**
107
+ * Assess what data coverage we have from API
108
+ */
109
+ function assessDataCoverage(apiData) {
110
+ if (!apiData) {
111
+ return {
112
+ score: 0,
113
+ factors: {
114
+ hasAPIData: false,
115
+ hasRecentData: false,
116
+ sampleSize: 'small',
117
+ dataAge: 999,
118
+ missingDataPoints: ['all'],
119
+ },
120
+ };
121
+ }
122
+ let score = 0;
123
+ const missingDataPoints = [];
124
+ // Base match data
125
+ if (apiData.match) {
126
+ score += 40;
127
+ }
128
+ else {
129
+ missingDataPoints.push('match');
130
+ }
131
+ // H2H data
132
+ if (apiData.h2h && apiData.h2h.totalMatches > 0) {
133
+ score += 20;
134
+ }
135
+ else {
136
+ missingDataPoints.push('h2h');
137
+ }
138
+ // Form data
139
+ if (apiData.homeForm && apiData.homeForm.length >= 3) {
140
+ score += 15;
141
+ }
142
+ else {
143
+ missingDataPoints.push('homeForm');
144
+ }
145
+ if (apiData.awayForm && apiData.awayForm.length >= 3) {
146
+ score += 15;
147
+ }
148
+ else {
149
+ missingDataPoints.push('awayForm');
150
+ }
151
+ // Odds data
152
+ if (apiData.odds) {
153
+ score += 10;
154
+ }
155
+ else {
156
+ missingDataPoints.push('odds');
157
+ }
158
+ // Calculate data age
159
+ const dataAge = apiData.lastUpdated
160
+ ? (Date.now() - apiData.lastUpdated.getTime()) / (1000 * 60 * 60)
161
+ : 999;
162
+ // Determine sample size
163
+ let sampleSize = 'small';
164
+ const totalMatches = (apiData.homeForm?.length || 0) + (apiData.awayForm?.length || 0);
165
+ if (totalMatches >= 10)
166
+ sampleSize = 'large';
167
+ else if (totalMatches >= 5)
168
+ sampleSize = 'medium';
169
+ return {
170
+ score: Math.min(100, score),
171
+ factors: {
172
+ hasAPIData: score > 0,
173
+ hasRecentData: dataAge < 24,
174
+ sampleSize,
175
+ dataAge,
176
+ missingDataPoints,
177
+ },
178
+ };
179
+ }
180
+ /**
181
+ * Optimize search queries based on API data coverage
182
+ */
183
+ function optimizeQueries(coverage, baseQueries) {
184
+ // Always search for these (API doesn't cover well)
185
+ const alwaysSearch = ['fatigue', 'setpieces', 'referee', 'weather'];
186
+ return baseQueries.filter(q => {
187
+ // Always include critical queries
188
+ if (alwaysSearch.includes(q.type))
189
+ return true;
190
+ // Skip queries that API already covers well
191
+ switch (q.type) {
192
+ case 'h2h':
193
+ // Skip H2H search if we have good H2H data
194
+ return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('h2h');
195
+ case 'form':
196
+ // Skip form search if we have form data
197
+ return !coverage.factors.hasAPIData ||
198
+ (coverage.factors.missingDataPoints.includes('homeForm') ||
199
+ coverage.factors.missingDataPoints.includes('awayForm'));
200
+ case 'stats':
201
+ // Stats need web search for xG
202
+ return true;
203
+ case 'news':
204
+ // Always search for news/lineups
205
+ return true;
206
+ case 'odds':
207
+ // Skip odds search if we have API odds
208
+ return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('odds');
209
+ default:
210
+ return true;
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Calculate adaptive TTL based on data type and match timing
216
+ */
217
+ function calculateAdaptiveTTL(dataType, matchDate) {
218
+ const now = Date.now();
219
+ const hoursUntilMatch = matchDate
220
+ ? (matchDate.getTime() - now) / (1000 * 60 * 60)
221
+ : Infinity;
222
+ switch (dataType) {
223
+ case 'odds':
224
+ // Odds change frequently - short TTL
225
+ if (hoursUntilMatch < 1)
226
+ return 60 * 1000; // 1 minute
227
+ if (hoursUntilMatch < 24)
228
+ return 5 * 60 * 1000; // 5 minutes
229
+ return 15 * 60 * 1000; // 15 minutes
230
+ case 'lineups':
231
+ // Lineups announced ~1 hour before match
232
+ if (hoursUntilMatch < 2)
233
+ return 5 * 60 * 1000; // 5 minutes
234
+ return 60 * 60 * 1000; // 1 hour
235
+ case 'news':
236
+ // News changes throughout the day
237
+ if (hoursUntilMatch < 24)
238
+ return 15 * 60 * 1000; // 15 minutes
239
+ return 60 * 60 * 1000; // 1 hour
240
+ case 'stats':
241
+ case 'h2h':
242
+ // Historical stats don't change
243
+ return 24 * 60 * 60 * 1000; // 24 hours
244
+ case 'form':
245
+ // Form updates after each match
246
+ if (hoursUntilMatch < 0)
247
+ return 60 * 1000; // Live match - 1 minute
248
+ return 6 * 60 * 60 * 1000; // 6 hours
249
+ case 'match':
250
+ // Match data changes based on timing
251
+ if (hoursUntilMatch < 0)
252
+ return 60 * 1000; // Live - 1 minute
253
+ if (hoursUntilMatch < 1)
254
+ return 5 * 60 * 1000; // 5 minutes
255
+ return 30 * 60 * 1000; // 30 minutes
256
+ default:
257
+ return 30 * 60 * 1000; // 30 minutes default
258
+ }
259
+ }
65
260
  /**
66
261
  * Perform comprehensive match analysis using web search
67
262
  */
@@ -83,15 +278,73 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
83
278
  if (context) {
84
279
  queries.push({ type: 'general', query: `${baseQuery} ${context}${dateQuery}`, title: 'Specific Context' });
85
280
  }
86
- // Optimization: Skip some web searches if we have a matchId (API should cover these)
281
+ // Try to get API data first
282
+ const api = createAPIProvider();
87
283
  const matchId = await findMatchId(homeTeam, awayTeam, league);
88
- const queriesToExecute = matchId
89
- ? queries.filter(q => !['news', 'stats'].includes(q.type)) // API covers lineups and basic stats
90
- : queries;
284
+ let apiData = null;
285
+ if (matchId) {
286
+ apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
287
+ }
288
+ // Assess data coverage and optimize queries
289
+ const coverage = assessDataCoverage(apiData);
290
+ const queriesToExecute = optimizeQueries(coverage, queries);
91
291
  let combinedResults = `--- FOOTBALL MATCH DATA: ${homeTeam} vs ${awayTeam} ---\n`;
92
292
  if (matchId)
93
293
  combinedResults += `Resolved API Match ID: ${matchId}\n`;
294
+ combinedResults += `Data Quality Score: ${coverage.score}/100\n`;
94
295
  combinedResults += `Match Context Date: ${new Date().toLocaleDateString()}\n\n`;
296
+ // Add API Data section if available
297
+ if (apiData && coverage.score > 0) {
298
+ combinedResults += `--- API DATA (Reliable Source) ---\n\n`;
299
+ if (apiData.h2h) {
300
+ combinedResults += `### Head-to-Head Record\n`;
301
+ combinedResults += `- Total Matches: ${apiData.h2h.totalMatches}\n`;
302
+ combinedResults += `- ${homeTeam} Wins: ${apiData.h2h.homeWins}\n`;
303
+ combinedResults += `- Draws: ${apiData.h2h.draws}\n`;
304
+ combinedResults += `- ${awayTeam} Wins: ${apiData.h2h.awayWins}\n`;
305
+ combinedResults += `- Goals: ${apiData.h2h.homeGoals}-${apiData.h2h.awayGoals}\n\n`;
306
+ }
307
+ if (apiData.homeForm?.length) {
308
+ combinedResults += `### ${homeTeam} Recent Form (Last ${apiData.homeForm.length})\n`;
309
+ combinedResults += apiData.homeForm.map(m => {
310
+ const isHome = m.homeTeam.name.includes(homeTeam);
311
+ const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
312
+ const opponent = isHome ? m.awayTeam.name : m.homeTeam.name;
313
+ const result = isHome
314
+ ? (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
315
+ : (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
316
+ return `- ${result} vs ${opponent} (${score})`;
317
+ }).join('\n');
318
+ combinedResults += '\n\n';
319
+ }
320
+ if (apiData.awayForm?.length) {
321
+ combinedResults += `### ${awayTeam} Recent Form (Last ${apiData.awayForm.length})\n`;
322
+ combinedResults += apiData.awayForm.map(m => {
323
+ const isAway = m.awayTeam.name.includes(awayTeam);
324
+ const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
325
+ const opponent = isAway ? m.homeTeam.name : m.awayTeam.name;
326
+ const result = isAway
327
+ ? (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
328
+ : (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
329
+ return `- ${result} vs ${opponent} (${score})`;
330
+ }).join('\n');
331
+ combinedResults += '\n\n';
332
+ }
333
+ if (apiData.odds) {
334
+ combinedResults += `### Current Odds (from API)\n`;
335
+ combinedResults += `- Home Win: ${apiData.odds.homeWin.toFixed(2)}\n`;
336
+ combinedResults += `- Draw: ${apiData.odds.draw.toFixed(2)}\n`;
337
+ combinedResults += `- Away Win: ${apiData.odds.awayWin.toFixed(2)}\n`;
338
+ if (apiData.odds.asianHandicap) {
339
+ combinedResults += `- Asian Handicap: ${apiData.odds.asianHandicap.line}\n`;
340
+ }
341
+ combinedResults += '\n';
342
+ }
343
+ combinedResults += `--- END API DATA ---\n\n`;
344
+ }
345
+ if (coverage.factors.missingDataPoints.length > 0) {
346
+ combinedResults += `*Note: Using web search to supplement: ${coverage.factors.missingDataPoints.join(', ')}*\n\n`;
347
+ }
95
348
  const candidateUrls = [];
96
349
  // Execute searches in parallel (in batches)
97
350
  const BATCH_SIZE = 4;
@@ -164,6 +417,225 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
164
417
  combinedResults += `🏆 FINAL VERDICT:\n - Asian Handicap Leans\n - Goal Line (Over/Under)\n - The "Value Pick"`;
165
418
  return combinedResults;
166
419
  }
420
+ /**
421
+ * Calculate probability range with uncertainty quantification
422
+ */
423
+ function calculateProbabilityRange(baseProbability, dataQuality) {
424
+ // Higher data quality = narrower range
425
+ const qualityFactor = dataQuality.score / 100;
426
+ const baseUncertainty = 0.15; // 15% base uncertainty
427
+ // Adjust uncertainty based on data quality
428
+ const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
429
+ // Calculate range
430
+ const mid = baseProbability;
431
+ const margin = adjustedUncertainty * Math.sqrt(baseProbability * (1 - baseProbability));
432
+ return {
433
+ low: Math.max(0, mid - margin * 1.645), // 5th percentile
434
+ mid,
435
+ high: Math.min(1, mid + margin * 1.645), // 95th percentile
436
+ };
437
+ }
438
+ /**
439
+ * Detect value bets from predictions and odds
440
+ */
441
+ async function detectValueBets(apiData, homeTeam, awayTeam) {
442
+ const valueBets = [];
443
+ if (!apiData?.odds)
444
+ return valueBets;
445
+ // Estimate probabilities from API data
446
+ // Simple model: use form and H2H to estimate
447
+ let homeWinProb = 0.33;
448
+ let drawProb = 0.33;
449
+ let awayWinProb = 0.33;
450
+ if (apiData.homeForm && apiData.awayForm) {
451
+ const homeWins = apiData.homeForm.filter(m => {
452
+ const isHome = m.homeTeam.name.includes(homeTeam);
453
+ return isHome ? (m.score?.home ?? 0) > (m.score?.away ?? 0) : (m.score?.away ?? 0) > (m.score?.home ?? 0);
454
+ }).length;
455
+ const awayWins = apiData.awayForm.filter(m => {
456
+ const isAway = m.awayTeam.name.includes(awayTeam);
457
+ return isAway ? (m.score?.away ?? 0) > (m.score?.home ?? 0) : (m.score?.home ?? 0) > (m.score?.away ?? 0);
458
+ }).length;
459
+ // Normalize to probabilities
460
+ const total = homeWins + awayWins + 2; // +2 for draws
461
+ homeWinProb = (homeWins + 1) / total;
462
+ awayWinProb = (awayWins + 1) / total;
463
+ drawProb = 1 - homeWinProb - awayWinProb;
464
+ }
465
+ // Calculate value for each market
466
+ const markets = [
467
+ { selection: `${homeTeam} Win`, prob: homeWinProb, odds: apiData.odds.homeWin },
468
+ { selection: 'Draw', prob: drawProb, odds: apiData.odds.draw },
469
+ { selection: `${awayTeam} Win`, prob: awayWinProb, odds: apiData.odds.awayWin },
470
+ ];
471
+ for (const market of markets) {
472
+ if (!market.odds || market.prob <= 0)
473
+ continue;
474
+ const fairOdds = 1 / market.prob;
475
+ const value = (market.odds / fairOdds) - 1;
476
+ // Only include if value > 5%
477
+ if (value >= 0.05) {
478
+ const kelly = (market.prob * market.odds - 1) / (market.odds - 1);
479
+ valueBets.push({
480
+ selection: market.selection,
481
+ market: '1x2',
482
+ odds: market.odds,
483
+ fairOdds,
484
+ value,
485
+ confidence: Math.min(90, 50 + value * 200), // Scale value to confidence
486
+ kellyFraction: Math.max(0, kelly / 2), // Half Kelly
487
+ recommendedStake: Math.max(0, kelly / 2) * 100, // Based on 100 unit bankroll
488
+ reasoning: `Estimated probability ${(market.prob * 100).toFixed(1)}% vs implied ${((1 / market.odds) * 100).toFixed(1)}%`,
489
+ });
490
+ }
491
+ }
492
+ return valueBets.sort((a, b) => b.value - a.value);
493
+ }
494
+ /**
495
+ * Build structured analysis result from API and web data
496
+ */
497
+ async function buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage) {
498
+ // Calculate base probabilities
499
+ let homeWinProb = 0.35;
500
+ let drawProb = 0.30;
501
+ let awayWinProb = 0.35;
502
+ // Adjust based on form if available
503
+ if (apiData?.homeForm && apiData?.awayForm) {
504
+ const homePoints = apiData.homeForm.reduce((sum, m) => {
505
+ const isHome = m.homeTeam.name.includes(homeTeam);
506
+ const score = m.score;
507
+ if (!score)
508
+ return sum;
509
+ if (isHome) {
510
+ return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
511
+ }
512
+ else {
513
+ return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
514
+ }
515
+ }, 0);
516
+ const awayPoints = apiData.awayForm.reduce((sum, m) => {
517
+ const isAway = m.awayTeam.name.includes(awayTeam);
518
+ const score = m.score;
519
+ if (!score)
520
+ return sum;
521
+ if (isAway) {
522
+ return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
523
+ }
524
+ else {
525
+ return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
526
+ }
527
+ }, 0);
528
+ const totalPoints = homePoints + awayPoints + 5; // +5 for draw possibility
529
+ homeWinProb = (homePoints + 1.5) / totalPoints;
530
+ awayWinProb = (awayPoints + 1.5) / totalPoints;
531
+ drawProb = 1 - homeWinProb - awayWinProb;
532
+ }
533
+ // Calculate probability ranges
534
+ const predictions = {
535
+ matchResult: {
536
+ homeWin: calculateProbabilityRange(homeWinProb, coverage),
537
+ draw: calculateProbabilityRange(drawProb, coverage),
538
+ awayWin: calculateProbabilityRange(awayWinProb, coverage),
539
+ },
540
+ overUnder25: {
541
+ over: calculateProbabilityRange(0.50, coverage), // Placeholder
542
+ under: calculateProbabilityRange(0.50, coverage),
543
+ },
544
+ btts: {
545
+ yes: calculateProbabilityRange(0.50, coverage), // Placeholder
546
+ no: calculateProbabilityRange(0.50, coverage),
547
+ },
548
+ };
549
+ // Detect value bets
550
+ const valueBets = await detectValueBets(apiData, homeTeam, awayTeam);
551
+ // Build six-persona analysis
552
+ const analysis = {
553
+ dataScientist: {
554
+ homeWinProbability: predictions.matchResult.homeWin,
555
+ drawProbability: predictions.matchResult.draw,
556
+ awayWinProbability: predictions.matchResult.awayWin,
557
+ expectedGoals: { home: 1.3, away: 1.2 }, // Placeholder - would need xG data
558
+ keyStats: [],
559
+ },
560
+ tacticalScout: {
561
+ styleMatchup: 'Analysis requires detailed tactical data',
562
+ keyBattles: [],
563
+ tacticalAdvantage: 'neutral',
564
+ },
565
+ physio: {
566
+ homeFatigue: {
567
+ score: 50,
568
+ daysRest: 3,
569
+ squadDepth: 5,
570
+ keyInjuries: [],
571
+ },
572
+ awayFatigue: {
573
+ score: 50,
574
+ daysRest: 3,
575
+ squadDepth: 5,
576
+ keyInjuries: [],
577
+ },
578
+ injuryImpact: 'Analysis requires injury data',
579
+ },
580
+ setPieceAnalyst: {
581
+ homeStrength: 5,
582
+ awayStrength: 5,
583
+ predictedCorners: 10,
584
+ aerialAdvantage: 'neutral',
585
+ },
586
+ insider: {
587
+ marketMovement: apiData?.odds ? `Home: ${apiData.odds.homeWin.toFixed(2)}, Draw: ${apiData.odds.draw.toFixed(2)}, Away: ${apiData.odds.awayWin.toFixed(2)}` : 'No odds data',
588
+ },
589
+ skeptic: {
590
+ upsetRisk: 0.25,
591
+ trapIndicators: [],
592
+ gameScenarios: [],
593
+ },
594
+ };
595
+ // Determine best bet
596
+ const bestBet = valueBets.length > 0
597
+ ? valueBets[0].selection
598
+ : `${homeTeam} Win or Draw (Double Chance)`;
599
+ return {
600
+ match: {
601
+ homeTeam,
602
+ awayTeam,
603
+ league,
604
+ matchId: apiData?.match?.id,
605
+ },
606
+ dataQuality: coverage,
607
+ analysis,
608
+ predictions,
609
+ valueBets,
610
+ verdict: {
611
+ summary: `Analysis based on ${coverage.score}/100 data quality. ${apiData?.h2h ? `H2H: ${apiData.h2h.homeWins}-${apiData.h2h.draws}-${apiData.h2h.awayWins}` : 'No H2H data'}`,
612
+ bestBet,
613
+ confidence: coverage.score,
614
+ riskLevel: coverage.score >= 70 ? 'low' : coverage.score >= 40 ? 'medium' : 'high',
615
+ },
616
+ metadata: {
617
+ generatedAt: new Date(),
618
+ dataSources: apiData ? ['api-football'] : ['web-search'],
619
+ },
620
+ };
621
+ }
622
+ /**
623
+ * Perform structured match analysis with JSON output
624
+ */
625
+ export async function performStructuredMatchAnalysis(homeTeam, awayTeam, league, context) {
626
+ // Fetch API data
627
+ const api = createAPIProvider();
628
+ const matchId = await findMatchId(homeTeam, awayTeam, league);
629
+ let apiData = null;
630
+ if (matchId) {
631
+ apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
632
+ }
633
+ // Assess data coverage
634
+ const coverage = assessDataCoverage(apiData);
635
+ // Build structured analysis
636
+ const result = await buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage);
637
+ return result;
638
+ }
167
639
  // ============= Tool Registration =============
168
640
  export function registerMatchTools(server) {
169
641
  /**
@@ -211,7 +683,60 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
211
683
  };
212
684
  });
213
685
  /**
214
- * Tool 2: get_live_scores
686
+ * Tool 2: analyze_football_match_structured
687
+ * Structured JSON output for programmatic use
688
+ */
689
+ server.tool('analyze_football_match_structured', `Comprehensive football match analysis with structured JSON output.
690
+ Returns: Probabilities with confidence ranges, 6-persona analysis, value bets, data quality score.
691
+ Use this for programmatic access to match analysis.`, {
692
+ homeTeam: z.string().describe('Name of the home team'),
693
+ awayTeam: z.string().describe('Name of the away team'),
694
+ league: z.string().optional().describe('League name (optional, helps with accuracy)'),
695
+ includeOdds: z.boolean().optional().default(true).describe('Include current odds in analysis'),
696
+ }, async ({ homeTeam, awayTeam, league, includeOdds }) => {
697
+ try {
698
+ const result = await performStructuredMatchAnalysis(homeTeam, awayTeam, league);
699
+ // Format as readable JSON with summary
700
+ const output = {
701
+ summary: `${homeTeam} vs ${awayTeam}`,
702
+ dataQuality: `${result.dataQuality.score}/100`,
703
+ predictions: {
704
+ homeWin: `${(result.predictions.matchResult.homeWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.homeWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.homeWin.high * 100).toFixed(1)}%)`,
705
+ draw: `${(result.predictions.matchResult.draw.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.draw.low * 100).toFixed(1)}%-${(result.predictions.matchResult.draw.high * 100).toFixed(1)}%)`,
706
+ awayWin: `${(result.predictions.matchResult.awayWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.awayWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.awayWin.high * 100).toFixed(1)}%)`,
707
+ },
708
+ valueBets: result.valueBets.length > 0
709
+ ? result.valueBets.map(vb => ({
710
+ selection: vb.selection,
711
+ odds: vb.odds.toFixed(2),
712
+ value: `${(vb.value * 100).toFixed(1)}%`,
713
+ stake: `${vb.recommendedStake.toFixed(1)}u`
714
+ }))
715
+ : 'No value bets detected',
716
+ bestBet: result.verdict.bestBet,
717
+ confidence: `${result.verdict.confidence}/100`,
718
+ riskLevel: result.verdict.riskLevel,
719
+ fullAnalysis: result,
720
+ };
721
+ return {
722
+ content: [{
723
+ type: 'text',
724
+ text: JSON.stringify(output, null, 2)
725
+ }],
726
+ };
727
+ }
728
+ catch (error) {
729
+ return {
730
+ content: [{
731
+ type: 'text',
732
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
733
+ }],
734
+ isError: true,
735
+ };
736
+ }
737
+ });
738
+ /**
739
+ * Tool 3: get_live_scores
215
740
  * Get live football scores for a league or team
216
741
  */
217
742
  server.tool('get_live_scores', `Get live football scores.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "10000.1.3",
3
+ "version": "10000.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },