@gotza02/sequential-thinking 10000.1.2 → 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.
- package/README.md +39 -249
- package/dist/dashboard/server.d.ts +2 -45
- package/dist/dashboard/server.js +64 -250
- package/dist/index.js +4 -1
- package/dist/tools/sports/core/base.d.ts +31 -1
- package/dist/tools/sports/core/base.js +25 -0
- package/dist/tools/sports/core/cache.d.ts +8 -19
- package/dist/tools/sports/core/cache.js +38 -95
- package/dist/tools/sports/core/constants.d.ts +4 -63
- package/dist/tools/sports/core/constants.js +11 -86
- package/dist/tools/sports/core/types.d.ts +146 -34
- package/dist/tools/sports/providers/api.d.ts +12 -1
- package/dist/tools/sports/providers/api.js +170 -0
- package/dist/tools/sports/tools/match.d.ts +5 -0
- package/dist/tools/sports/tools/match.js +530 -5
- package/package.json +1 -1
- package/dist/tools/sports/core/alert-manager.d.ts +0 -96
- package/dist/tools/sports/core/alert-manager.js +0 -319
- package/dist/tools/sports/core/circuit-breaker.d.ts +0 -40
- package/dist/tools/sports/core/circuit-breaker.js +0 -99
- package/dist/tools/sports/core/data-quality.d.ts +0 -36
- package/dist/tools/sports/core/data-quality.js +0 -243
- package/dist/tools/sports/core/historical-analyzer.d.ts +0 -54
- package/dist/tools/sports/core/historical-analyzer.js +0 -261
- package/dist/tools/sports/core/index.d.ts +0 -13
- package/dist/tools/sports/core/index.js +0 -16
- package/dist/tools/sports/core/ml-prediction.d.ts +0 -76
- package/dist/tools/sports/core/ml-prediction.js +0 -260
- package/dist/tools/sports/core/realtime-manager.d.ts +0 -51
- package/dist/tools/sports/core/realtime-manager.js +0 -222
- package/dist/tools/sports/core/retry.d.ts +0 -29
- package/dist/tools/sports/core/retry.js +0 -77
|
@@ -14,49 +14,31 @@ export const API_CONFIG = {
|
|
|
14
14
|
// TheSportsDB
|
|
15
15
|
SPORTS_DB_KEY: process.env.SPORTS_DB_KEY || '',
|
|
16
16
|
SPORTS_DB_BASE: 'https://www.thesportsdb.com/api/v1/json',
|
|
17
|
-
// Odds API
|
|
18
|
-
ODDS_API_KEY: process.env.ODDS_API_KEY || '',
|
|
19
|
-
ODDS_API_BASE: 'https://api.the-odds-api.com/v4',
|
|
20
|
-
// SportRadar (requires enterprise license)
|
|
21
|
-
SPORTRADAR_KEY: process.env.SPORTRADAR_KEY || '',
|
|
22
|
-
SPORTRADAR_BASE: 'https://api.sportradar.us/soccer/trial/v4',
|
|
23
17
|
// Rate limits (requests per minute)
|
|
24
18
|
RATE_LIMITS: {
|
|
25
19
|
API_FOOTBALL: 10,
|
|
26
20
|
FOOTBALL_DATA: 10,
|
|
27
21
|
SPORTS_DB: 30,
|
|
28
|
-
ODDS_API: 20,
|
|
29
|
-
SPORTRADAR: 100,
|
|
30
22
|
}
|
|
31
23
|
};
|
|
32
24
|
// ============= Cache Configuration =============
|
|
33
25
|
export const CACHE_CONFIG = {
|
|
34
26
|
// Default TTL in milliseconds
|
|
35
27
|
DEFAULT_TTL: parseInt(process.env.SPORTS_CACHE_TTL || '300000'), // 5 minutes
|
|
36
|
-
// Specific TTLs for different data types
|
|
28
|
+
// Specific TTLs for different data types
|
|
37
29
|
TTL: {
|
|
38
|
-
LIVE_SCORES:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
TRANSFER_NEWS: 10 * 60 * 1000, // 10 minutes (was 15 minutes)
|
|
46
|
-
PREDICTIONS: 60 * 60 * 1000, // 1 hour
|
|
47
|
-
},
|
|
48
|
-
// Stale-while-revalidate multipliers
|
|
49
|
-
STALE_MULTIPLIERS: {
|
|
50
|
-
LIVE_SCORES: 2, // Stale for 30 seconds (15s * 2)
|
|
51
|
-
ODDS: 2, // Stale for 1 minute (30s * 2)
|
|
52
|
-
MATCH_DETAILS: 2, // Stale for 4 minutes
|
|
53
|
-
STANDINGS: 1.5, // Stale for 22.5 minutes
|
|
54
|
-
TEAM_STATS: 1.5, // Stale for 45 minutes
|
|
30
|
+
LIVE_SCORES: 60 * 1000, // 1 minute
|
|
31
|
+
MATCH_DETAILS: 5 * 60 * 1000, // 5 minutes
|
|
32
|
+
STANDINGS: 30 * 60 * 1000, // 30 minutes
|
|
33
|
+
TEAM_STATS: 60 * 60 * 1000, // 1 hour
|
|
34
|
+
PLAYER_STATS: 60 * 60 * 1000, // 1 hour
|
|
35
|
+
ODDS: 2 * 60 * 1000, // 2 minutes
|
|
36
|
+
TRANSFER_NEWS: 15 * 60 * 1000, // 15 minutes
|
|
55
37
|
},
|
|
56
38
|
// Cache file path
|
|
57
39
|
CACHE_PATH: process.env.SPORTS_CACHE_PATH || '.sports_cache.json',
|
|
58
40
|
// Max cache size (number of entries)
|
|
59
|
-
MAX_SIZE:
|
|
41
|
+
MAX_SIZE: 1000,
|
|
60
42
|
};
|
|
61
43
|
// ============= Scraping Configuration =============
|
|
62
44
|
export const SCRAPER_CONFIG = {
|
|
@@ -170,67 +152,10 @@ export const ERRORS = {
|
|
|
170
152
|
TIMEOUT: 'Request timed out.',
|
|
171
153
|
INVALID_RESPONSE: 'Invalid response from API.',
|
|
172
154
|
ALL_PROVIDERS_FAILED: 'All data providers failed.',
|
|
173
|
-
CIRCUIT_BREAKER_OPEN: 'Service temporarily unavailable due to repeated failures.',
|
|
174
|
-
};
|
|
175
|
-
// ============= Alert Configuration =============
|
|
176
|
-
export const ALERT_CONFIG = {
|
|
177
|
-
// Check interval for alerts (milliseconds)
|
|
178
|
-
CHECK_INTERVAL: 10000, // 10 seconds
|
|
179
|
-
// Default cooldown between same alert (milliseconds)
|
|
180
|
-
DEFAULT_COOLDOWN: 300000, // 5 minutes
|
|
181
|
-
// Max alerts per hour per rule
|
|
182
|
-
MAX_ALERTS_PER_HOUR: 10,
|
|
183
|
-
// Alert types
|
|
184
|
-
TYPES: {
|
|
185
|
-
ODDS_DROP: 'odds_drop',
|
|
186
|
-
ODDS_VALUE: 'odds_value',
|
|
187
|
-
GOAL: 'goal',
|
|
188
|
-
RED_CARD: 'red_card',
|
|
189
|
-
LINEUP_CHANGE: 'lineup_change',
|
|
190
|
-
MATCH_START: 'match_start',
|
|
191
|
-
MATCH_END: 'match_end',
|
|
192
|
-
CUSTOM: 'custom',
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
// ============= Realtime Configuration =============
|
|
196
|
-
export const REALTIME_CONFIG = {
|
|
197
|
-
// WebSocket reconnection
|
|
198
|
-
WS_RECONNECT_INTERVAL: 5000,
|
|
199
|
-
WS_MAX_RECONNECT_ATTEMPTS: 10,
|
|
200
|
-
// Polling intervals (fallback when WebSocket unavailable)
|
|
201
|
-
POLLING_INTERVALS: {
|
|
202
|
-
LIVE_SCORES: 15000, // 15 seconds
|
|
203
|
-
ODDS: 30000, // 30 seconds
|
|
204
|
-
MATCH_EVENTS: 5000, // 5 seconds
|
|
205
|
-
},
|
|
206
|
-
// Event batching
|
|
207
|
-
EVENT_BATCH_SIZE: 10,
|
|
208
|
-
EVENT_BATCH_TIMEOUT: 1000,
|
|
209
|
-
};
|
|
210
|
-
// ============= ML Prediction Configuration =============
|
|
211
|
-
export const ML_CONFIG = {
|
|
212
|
-
// Minimum matches for training
|
|
213
|
-
MIN_TRAINING_MATCHES: 50,
|
|
214
|
-
// Feature weights
|
|
215
|
-
FEATURE_WEIGHTS: {
|
|
216
|
-
form: 0.25,
|
|
217
|
-
h2h: 0.20,
|
|
218
|
-
home_advantage: 0.15,
|
|
219
|
-
xg: 0.15,
|
|
220
|
-
injuries: 0.10,
|
|
221
|
-
fatigue: 0.10,
|
|
222
|
-
weather: 0.05,
|
|
223
|
-
},
|
|
224
|
-
// Confidence thresholds
|
|
225
|
-
CONFIDENCE_THRESHOLDS: {
|
|
226
|
-
high: 0.70,
|
|
227
|
-
medium: 0.55,
|
|
228
|
-
low: 0.40,
|
|
229
|
-
},
|
|
230
155
|
};
|
|
231
156
|
// ============= Version Info =============
|
|
232
157
|
export const MODULE_INFO = {
|
|
233
158
|
NAME: 'sports-module',
|
|
234
|
-
VERSION: '
|
|
235
|
-
DESCRIPTION: 'Football Intelligence System for MCP Server
|
|
159
|
+
VERSION: '2.0.0',
|
|
160
|
+
DESCRIPTION: 'Football Intelligence System for MCP Server',
|
|
236
161
|
};
|
|
@@ -7,7 +7,7 @@ export type MatchStatus = 'scheduled' | 'live' | 'finished' | 'postponed' | 'can
|
|
|
7
7
|
export type BetType = 'home' | 'draw' | 'away' | 'over' | 'under' | 'btts' | 'handicap';
|
|
8
8
|
export type KellyVariant = 'full' | 'half' | 'quarter';
|
|
9
9
|
export type QueryType = 'general' | 'h2h' | 'form' | 'news' | 'stats' | 'odds' | 'referee' | 'weather' | 'fatigue' | 'setpieces';
|
|
10
|
-
export type ProviderType = 'api-football' | 'football-data' | 'sports-db' | '
|
|
10
|
+
export type ProviderType = 'api-football' | 'football-data' | 'sports-db' | 'scraper';
|
|
11
11
|
export interface SearchQuery {
|
|
12
12
|
type: QueryType;
|
|
13
13
|
query: string;
|
|
@@ -175,8 +175,6 @@ export interface Match {
|
|
|
175
175
|
homeSubstitutes?: Player[];
|
|
176
176
|
awaySubstitutes?: Player[];
|
|
177
177
|
events?: MatchEvent[];
|
|
178
|
-
dataQuality?: number;
|
|
179
|
-
lastUpdated?: Date;
|
|
180
178
|
}
|
|
181
179
|
export interface MatchOdds {
|
|
182
180
|
homeWin: number;
|
|
@@ -346,7 +344,6 @@ export interface APIResponse<T = any> {
|
|
|
346
344
|
provider?: ProviderType;
|
|
347
345
|
cached?: boolean;
|
|
348
346
|
rateLimited?: boolean;
|
|
349
|
-
quality?: number;
|
|
350
347
|
}
|
|
351
348
|
export interface ProviderStatus {
|
|
352
349
|
name: ProviderType;
|
|
@@ -355,7 +352,6 @@ export interface ProviderStatus {
|
|
|
355
352
|
quotaLimit: number;
|
|
356
353
|
rateLimitUntil?: Date;
|
|
357
354
|
lastCall?: Date;
|
|
358
|
-
circuitBreakerState?: 'closed' | 'open' | 'half-open';
|
|
359
355
|
}
|
|
360
356
|
export interface ComparisonResult {
|
|
361
357
|
teamA: Team;
|
|
@@ -381,38 +377,154 @@ export interface LiveScoreUpdate {
|
|
|
381
377
|
events: MatchEvent[];
|
|
382
378
|
timestamp: Date;
|
|
383
379
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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;
|
|
391
410
|
confidence: number;
|
|
392
|
-
|
|
393
|
-
|
|
411
|
+
kellyFraction: number;
|
|
412
|
+
recommendedStake: number;
|
|
413
|
+
reasoning: string;
|
|
394
414
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Game scenario for skeptic analysis
|
|
417
|
+
*/
|
|
418
|
+
export interface GameScenario {
|
|
419
|
+
trigger: string;
|
|
420
|
+
probability: number;
|
|
421
|
+
impact: 'high' | 'medium' | 'low';
|
|
399
422
|
description: string;
|
|
400
423
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Fatigue assessment
|
|
426
|
+
*/
|
|
427
|
+
export interface FatigueAssessment {
|
|
428
|
+
score: number;
|
|
429
|
+
daysRest: number;
|
|
430
|
+
travelDistance?: number;
|
|
431
|
+
squadDepth: number;
|
|
432
|
+
keyInjuries: string[];
|
|
407
433
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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;
|
|
418
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;
|