@gotza02/sequential-thinking 10000.1.3 → 10000.1.5
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 +128 -6
- package/dist/tools/sports/core/base.d.ts +31 -1
- package/dist/tools/sports/core/base.js +25 -0
- package/dist/tools/sports/core/types.d.ts +151 -0
- 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/README.md
CHANGED
|
@@ -17,11 +17,16 @@ Forces the AI to think before acting using a strict block-based flow:
|
|
|
17
17
|
- **Auto-Backup:** Automatically creates `.bak` backups before modifying any file via `deep_code_edit`.
|
|
18
18
|
- **Destructive Action Protection:** Prevents accidental data loss during code refactoring.
|
|
19
19
|
|
|
20
|
-
### ⚽ Sports Intelligence (
|
|
21
|
-
- **
|
|
22
|
-
- **Smart
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
20
|
+
### ⚽ Sports Intelligence (v10000.1.4+)
|
|
21
|
+
- **API Data Integration:** Direct API access for H2H, form, and odds (API-Football, Football-Data, SportsDB)
|
|
22
|
+
- **Smart Query Optimization:** Skips web search when API has data, assesses data quality (0-100 score)
|
|
23
|
+
- **Structured Output:** JSON analysis with probability ranges, value bets, and confidence scores
|
|
24
|
+
- **Uncertainty Quantification:** Probability ranges (e.g., 45% ± 10%) based on data quality
|
|
25
|
+
- **Value Detection:** Auto-detect value bets with Kelly Criterion stake recommendations
|
|
26
|
+
- **Adaptive Caching:** Dynamic TTL based on match timing (odds: 1-15 min, stats: 24 hours)
|
|
27
|
+
- **Dual-Source Deep Dive:** Simultaneously scrapes news (BBC, Sky) and stats (Understat, FBref)
|
|
28
|
+
- **Smart xG Extraction:** "Smart Extractor" for accurate Expected Goals data
|
|
29
|
+
- **Fallback Protection:** Intelligent warning system when API keys are missing
|
|
25
30
|
|
|
26
31
|
### 🌐 Web Search Integration
|
|
27
32
|
- Built-in support for **Exa**, **Brave**, **Google Search**, and **DuckDuckGo** (New!).
|
|
@@ -49,4 +54,121 @@ Forces the AI to think before acting using a strict block-based flow:
|
|
|
49
54
|
|
|
50
55
|
```bash
|
|
51
56
|
npx -y @gotza02/sequential-thinking
|
|
52
|
-
```
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 🔧 Claude Code Configuration
|
|
60
|
+
|
|
61
|
+
Add to your `~/.claude/CLAUDE.md` or project-specific `.claude/CLAUDE.md`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"sequential-thinking": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@gotza02/sequential-thinking"],
|
|
69
|
+
"env": {
|
|
70
|
+
"BRAVE_API_KEY": "your_brave_key",
|
|
71
|
+
"EXA_API_KEY": "your_exa_key",
|
|
72
|
+
"API_FOOTBALL_KEY": "your_api_football_key"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🛠️ Available MCP Tools
|
|
80
|
+
|
|
81
|
+
### 🧠 Thinking & Analysis Tools
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
|------|-------------|
|
|
84
|
+
| `sequentialthinking` | Structured problem-solving with Analysis → Planning → Execution → Observation → Reflection |
|
|
85
|
+
| `analyze_football_match_v2` | Comprehensive football match analysis with API + web search |
|
|
86
|
+
| `analyze_football_match_structured` | JSON output match analysis with probabilities & value bets |
|
|
87
|
+
| `get_live_scores` | Live football scores with caching |
|
|
88
|
+
| `get_match_details` | Detailed match information by ID |
|
|
89
|
+
|
|
90
|
+
### ⚽ Sports Analysis Tools
|
|
91
|
+
| Tool | Description |
|
|
92
|
+
|------|-------------|
|
|
93
|
+
| `get_team_stats` | Team statistics and performance metrics |
|
|
94
|
+
| `compare_teams` | Head-to-head team comparison |
|
|
95
|
+
| `get_player_stats` | Player statistics and form |
|
|
96
|
+
| `odds_comparison` | Compare betting odds across markets |
|
|
97
|
+
| `value_bet_calculator` | Calculate Kelly Criterion & value bets |
|
|
98
|
+
| `track_prediction` | Log sports predictions for ROI tracking |
|
|
99
|
+
| `resolve_prediction` | Update prediction results |
|
|
100
|
+
| `analyze_roi` | Analyze betting performance statistics |
|
|
101
|
+
|
|
102
|
+
### 🔍 Search & Research Tools
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `brave_web_search` | Web search via Brave Search API |
|
|
106
|
+
| `exa_web_search` | Neural web search via Exa API |
|
|
107
|
+
| `duckduckgo_search` | Anonymous web search (no API key) |
|
|
108
|
+
| `web_read` | Read and extract content from URLs |
|
|
109
|
+
|
|
110
|
+
### 💻 Code Management Tools
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `deep_code_edit` | Structural code editing with auto-backup |
|
|
114
|
+
| `add_code_snippet` | Store reusable code patterns |
|
|
115
|
+
| `search_code` | Semantic code search |
|
|
116
|
+
| `get_project_graph` | Analyze codebase structure |
|
|
117
|
+
|
|
118
|
+
### 📝 Knowledge & Notes Tools
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
|------|-------------|
|
|
121
|
+
| `add_note` | Create and store notes |
|
|
122
|
+
| `search_notes` | Search through stored notes |
|
|
123
|
+
| `create_knowledge_node` | Add node to knowledge graph |
|
|
124
|
+
| `query_knowledge_graph` | Query relationships in knowledge graph |
|
|
125
|
+
|
|
126
|
+
### 👤 Human Interface Tools
|
|
127
|
+
| Tool | Description |
|
|
128
|
+
|------|-------------|
|
|
129
|
+
| `ask_human` | Request human input/clarification |
|
|
130
|
+
| `request_workflow_approval` | Get approval for multi-step workflows |
|
|
131
|
+
|
|
132
|
+
## 🤖 System Instructions for Claude Code
|
|
133
|
+
|
|
134
|
+
Add this to your Claude Code settings for optimal use:
|
|
135
|
+
|
|
136
|
+
```markdown
|
|
137
|
+
SYSTEM INSTRUCTION: SmartAgent Elite Controller
|
|
138
|
+
|
|
139
|
+
You are an AI assistant with access to the Sequential Thinking MCP server.
|
|
140
|
+
|
|
141
|
+
CRITICAL DIRECTIVES:
|
|
142
|
+
1. ALWAYS use sequentialthinking tool for complex tasks
|
|
143
|
+
2. Use web search for up-to-date information (never hallucinate)
|
|
144
|
+
3. For sports analysis:
|
|
145
|
+
- Use analyze_football_match_v2 for detailed text analysis
|
|
146
|
+
- Use analyze_football_match_structured for JSON/probability data
|
|
147
|
+
- Check value_bet_calculator for betting recommendations
|
|
148
|
+
4. Use deep_code_edit for structural code changes (creates backups)
|
|
149
|
+
5. Track predictions with track_prediction for ROI analysis
|
|
150
|
+
|
|
151
|
+
TOOL PRIORITY:
|
|
152
|
+
- Information needed → web_search (Brave/Exa/DuckDuckGo)
|
|
153
|
+
- Problem solving → sequentialthinking
|
|
154
|
+
- Sports data → analyze_football_match_* tools
|
|
155
|
+
- Code editing → deep_code_edit
|
|
156
|
+
- Human input → ask_human
|
|
157
|
+
|
|
158
|
+
SAFETY PROTOCOLS:
|
|
159
|
+
- Auto-backup enabled for all file edits
|
|
160
|
+
- Destructive actions require human approval
|
|
161
|
+
- Rate limiting active on all external APIs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🔐 Required Environment Variables
|
|
165
|
+
|
|
166
|
+
| Variable | Purpose | Required For |
|
|
167
|
+
|----------|---------|--------------|
|
|
168
|
+
| `BRAVE_API_KEY` | Brave Search API | Web search |
|
|
169
|
+
| `EXA_API_KEY` | Exa Neural Search | Advanced web search |
|
|
170
|
+
| `API_FOOTBALL_KEY` | API-Football | Sports data (RapidAPI) |
|
|
171
|
+
| `FOOTBALL_DATA_KEY` | Football-Data.org | Alternative sports API |
|
|
172
|
+
| `SPORTS_DB_KEY` | TheSportsDB | Backup sports API |
|
|
173
|
+
|
|
174
|
+
**Note:** DuckDuckGo search works without API keys as a fallback.
|
|
@@ -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
|
-
//
|
|
281
|
+
// Try to get API data first
|
|
282
|
+
const api = createAPIProvider();
|
|
87
283
|
const matchId = await findMatchId(homeTeam, awayTeam, league);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
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.
|