cito-mcp 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MAX_RATE_LIMIT_RETRIES, MAX_UPSTREAM_RETRIES, MAX_WAIT_MS, UPSTREAM_BACKOFF_MS, classify429, getClock, jitterMs, newCallBudget, pacerFor, } from './pacing.js';
1
+ import { MAX_RATE_LIMIT_RETRIES, MAX_SERVER_RETRIES, MAX_WAIT_MS, SERVER_BACKOFF_MS, classify429, getClock, jitterMs, newCallBudget, pacerFor, } from './pacing.js';
2
2
  import { PACKAGE_VERSION } from './version.js';
3
3
  export const DEFAULT_API_BASE = 'https://api.citoapi.com/api/v1';
4
4
  export const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024;
@@ -87,7 +87,7 @@ export async function fetchJson(ctx, path, opts) {
87
87
  const pacer = pacerFor(ctx.apiKey);
88
88
  const clock = getClock();
89
89
  let rateLimitRetries = 0;
90
- let upstreamRetries = 0;
90
+ let serverRetries = 0;
91
91
  for (;;) {
92
92
  const gate = await pacer.acquire(budget);
93
93
  if (!gate.ok) {
@@ -117,7 +117,7 @@ export async function fetchJson(ctx, path, opts) {
117
117
  text,
118
118
  url,
119
119
  rateLimit: event,
120
- retries: rateLimitRetries + upstreamRetries,
120
+ retries: rateLimitRetries + serverRetries,
121
121
  };
122
122
  }
123
123
  let response;
@@ -162,9 +162,9 @@ export async function fetchJson(ctx, path, opts) {
162
162
  }
163
163
  }
164
164
  else if (response.status === 502 || response.status === 503) {
165
- const wait = UPSTREAM_BACKOFF_MS + jitterMs();
166
- if (upstreamRetries < MAX_UPSTREAM_RETRIES && clock.now() + wait < budget.deadline) {
167
- upstreamRetries += 1;
165
+ const wait = SERVER_BACKOFF_MS + jitterMs();
166
+ if (serverRetries < MAX_SERVER_RETRIES && clock.now() + wait < budget.deadline) {
167
+ serverRetries += 1;
168
168
  budget.retries += 1;
169
169
  budget.waitedMs += wait;
170
170
  log(`${response.status} on ${path}; retry after ${wait} ms`);
@@ -184,8 +184,8 @@ export async function fetchJson(ctx, path, opts) {
184
184
  };
185
185
  if (limited)
186
186
  result.rateLimit = limited;
187
- if (rateLimitRetries + upstreamRetries > 0)
188
- result.retries = rateLimitRetries + upstreamRetries;
187
+ if (rateLimitRetries + serverRetries > 0)
188
+ result.retries = rateLimitRetries + serverRetries;
189
189
  return result;
190
190
  }
191
191
  }
package/dist/envelope.js CHANGED
@@ -80,8 +80,8 @@ export function successEnvelope(args) {
80
80
  meta.requestId = args.requestId;
81
81
  if (args.tookMs !== undefined)
82
82
  meta.tookMs = args.tookMs;
83
- if (args.upstreamCalls !== undefined)
84
- meta.apiCalls = args.upstreamCalls;
83
+ if (args.apiCalls !== undefined)
84
+ meta.apiCalls = args.apiCalls;
85
85
  if (args.rateLimit)
86
86
  meta.rateLimit = args.rateLimit;
87
87
  if (args.warnings?.length)
@@ -113,8 +113,8 @@ export function errorEnvelope(args) {
113
113
  meta.requestId = args.requestId;
114
114
  if (args.tookMs !== undefined)
115
115
  meta.tookMs = args.tookMs;
116
- if (args.upstreamCalls !== undefined)
117
- meta.apiCalls = args.upstreamCalls;
116
+ if (args.apiCalls !== undefined)
117
+ meta.apiCalls = args.apiCalls;
118
118
  if (args.rateLimit)
119
119
  meta.rateLimit = args.rateLimit;
120
120
  const error = {
package/dist/install.js CHANGED
@@ -222,21 +222,21 @@ function parseArgs(argv) {
222
222
  }
223
223
  return out;
224
224
  }
225
- const HELP = `cito-mcp install — configure the Cito MCP server for your editors
226
-
227
- Usage:
228
- npx cito-mcp install --key cito_xxx
229
- npx cito-mcp install --key cito_xxx --client cursor,claude-code
230
- npx cito-mcp install --dry-run
231
-
232
- Options:
233
- --key <key> Cito API key. Falls back to $CITO_API_KEY.
234
- --client <ids> Comma-separated: claude-code, claude-desktop, cursor, windsurf, codex, grok.
235
- Default: every client detected on this machine.
236
- --base <url> Override API base (staging / self-hosted).
237
- --dry-run, -n Show what would change; write nothing.
238
- --help, -h This message.
239
-
225
+ const HELP = `cito-mcp install — configure the Cito MCP server for your editors
226
+
227
+ Usage:
228
+ npx cito-mcp install --key cito_xxx
229
+ npx cito-mcp install --key cito_xxx --client cursor,claude-code
230
+ npx cito-mcp install --dry-run
231
+
232
+ Options:
233
+ --key <key> Cito API key. Falls back to $CITO_API_KEY.
234
+ --client <ids> Comma-separated: claude-code, claude-desktop, cursor, windsurf, codex, grok.
235
+ Default: every client detected on this machine.
236
+ --base <url> Override API base (staging / self-hosted).
237
+ --dry-run, -n Show what would change; write nothing.
238
+ --help, -h This message.
239
+
240
240
  Get a key at https://citoapi.com/dashboard`;
241
241
  export async function runInstall(argv) {
242
242
  const args = parseArgs(argv);
package/dist/pacing.js CHANGED
@@ -3,8 +3,8 @@ export const CONSTRAINED_CONCURRENT_PER_KEY = 2;
3
3
  export const CONSTRAINED_WINDOW_MS = 60_000;
4
4
  export const MAX_WAIT_MS = 20_000;
5
5
  export const MAX_RATE_LIMIT_RETRIES = 2;
6
- export const MAX_UPSTREAM_RETRIES = 1;
7
- export const UPSTREAM_BACKOFF_MS = 750;
6
+ export const MAX_SERVER_RETRIES = 1;
7
+ export const SERVER_BACKOFF_MS = 750;
8
8
  export const CALL_BUDGET_MS = 45_000;
9
9
  const DEFAULT_RETRY_AFTER_MS = 2_000;
10
10
  const WINDOW_MS = 60_000;
package/dist/scrub.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { createHash } from 'node:crypto';
1
2
  const INTERNAL_KEYS = new Set([
2
3
  'emptyCardHeal',
3
4
  'llmValidation',
4
5
  'workerHostHint',
5
- 'scrapeMeta',
6
6
  'fetchMeta',
7
7
  'cacheKey',
8
8
  'failureSteps',
@@ -12,8 +12,6 @@ const INTERNAL_KEYS = new Set([
12
12
  'sourceUrl',
13
13
  'sourceUrls',
14
14
  'sourceIds',
15
- 'wikiUrl',
16
- 'wiki_url',
17
15
  'jsonLd',
18
16
  '_mergeSource',
19
17
  '_provenance',
@@ -32,21 +30,17 @@ const INTERNAL_KEYS = new Set([
32
30
  'profileImageAttemptedAt',
33
31
  'profileImageAttempts',
34
32
  'profileImageError',
35
- 'profileImageSteamAttemptedAt',
36
33
  'profileImageWorker',
37
34
  'ratingSource',
38
35
  'requiredSourceData',
39
36
  'rawText',
40
37
  'raw_text',
41
- 'scrapedAt',
42
- 'scraped_at',
43
38
  'sourceSummary',
44
39
  'sourceNote',
45
40
  'durableStatsTables',
46
41
  'lineupSource',
47
42
  'mapMediaSource',
48
43
  'rosterSource',
49
- 'upstream',
50
44
  'liveCandidateWindowMinutes',
51
45
  'rowHash',
52
46
  'row_hash',
@@ -73,20 +67,64 @@ const INTERNAL_KEYS = new Set([
73
67
  'data_source',
74
68
  'date_source',
75
69
  'meta_source',
76
- 'esportsMatchId',
77
- 'esportsGameId',
78
- 'esports_match_id',
79
- 'riotId',
80
- 'riot_id',
81
- 'steamId',
82
- 'steam_id',
83
- 'oddsKey',
84
- 'odds_key',
85
- 'provider',
86
- 'providers',
87
70
  ]);
88
71
  const ORIGIN_WORD_KEY = /(^|_)sources?($|_)|[a-z]Sources?($|[A-Z])|^_?sources?[A-Z]/;
89
- const PROVIDER_PREFIX_KEY = /^_?(hltv|open_?dota|polymarket|fight_?odds|ufc_?stats)/i;
72
+ const DENIED_WORDS = new Set([
73
+ '007d859ffc608f3a',
74
+ '017055b00735cdaa',
75
+ '0991545f91164c02',
76
+ '0a86e4e8778e5bc4',
77
+ '211022d4dba1354e',
78
+ '2273bc83a7916501',
79
+ '343406fa95fefe9a',
80
+ '3511b94bd7795e3b',
81
+ '3fbfbeeeaddc4fe5',
82
+ '417ccb9b693bc11d',
83
+ '438338424376242e',
84
+ '53a828d1313e2f24',
85
+ '56279b500aeff149',
86
+ '63919bdfdccc4e03',
87
+ '68ec9e984bd969e2',
88
+ '735bda6297e559c5',
89
+ '7a9040b69cc466a0',
90
+ '7e25e5e9baf788fe',
91
+ '827ce7e74edb79b2',
92
+ '82d2e789811fae32',
93
+ '93b0c3823ffe6d0b',
94
+ 'a85f3c568720d7da',
95
+ 'aab85fb91587ac06',
96
+ 'aaf4b5714130cfbd',
97
+ 'ac4399e79599c5f9',
98
+ 'b71515611a06c04e',
99
+ 'b769513d718b1bdd',
100
+ 'b8e91b77223e5c17',
101
+ 'c833542aacad261c',
102
+ 'cca8e34d07dda457',
103
+ 'd656bca8c6e48174',
104
+ 'dc2bcd7bbfe8773e',
105
+ 'e08ad232764c86ab',
106
+ 'e3bd135d984a0b44',
107
+ 'e924b9eb04be6af2',
108
+ 'f2b71a5a46b415e3',
109
+ 'f578f53f4c52c9c9',
110
+ 'fb25211c8b74e564',
111
+ ]);
112
+ const DENIED_MEMO = new Map();
113
+ function wordHash(word) {
114
+ return createHash('sha256').update(`cito-mcp-key-v1:${word}`).digest('hex').slice(0, 16);
115
+ }
116
+ function hasDeniedWord(key) {
117
+ let hit = DENIED_MEMO.get(key);
118
+ if (hit === undefined) {
119
+ const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[\s_-]+/).filter(Boolean).map((w) => w.toLowerCase());
120
+ const tries = [...words, ...words.slice(1).map((w, i) => `${words[i]}${w}`)];
121
+ hit = tries.some((w) => DENIED_WORDS.has(wordHash(w)));
122
+ if (DENIED_MEMO.size > 20_000)
123
+ DENIED_MEMO.clear();
124
+ DENIED_MEMO.set(key, hit);
125
+ }
126
+ return hit;
127
+ }
90
128
  const PORTRAIT_LICENCE_KEY = /^(portrait|image|photo|picture)_?source_?url$/i;
91
129
  const STATUS_PARENTS = new Set(['dataAvailability', 'health']);
92
130
  const STATUS_KEYS = new Set(['strategy', 'samples']);
@@ -104,7 +142,7 @@ function isInternal(key, parentKey) {
104
142
  return false;
105
143
  return (INTERNAL_KEYS.has(key) ||
106
144
  ORIGIN_WORD_KEY.test(key) ||
107
- PROVIDER_PREFIX_KEY.test(key) ||
145
+ hasDeniedWord(key) ||
108
146
  (STATUS_KEYS.has(key) && STATUS_PARENTS.has(parentKey)));
109
147
  }
110
148
  function scrub(node, parentKey, inError) {
@@ -82,7 +82,7 @@ function payload(res) {
82
82
  const body = asRecord(res.data);
83
83
  return body && 'data' in body ? body.data : res.data;
84
84
  }
85
- function upstreamNotes(res) {
85
+ function responseNotes(res) {
86
86
  const meta = asRecord(asRecord(res.data)?.meta);
87
87
  const out = {};
88
88
  if (typeof meta?.note === 'string')
@@ -114,7 +114,7 @@ function archiveGap(d, league, message, recover, calls = 0) {
114
114
  source: d.source,
115
115
  requestId: d.requestId,
116
116
  tookMs: Date.now() - d.started,
117
- upstreamCalls: calls,
117
+ apiCalls: calls,
118
118
  retryable: false,
119
119
  recover,
120
120
  });
@@ -178,21 +178,21 @@ function begin(source) {
178
178
  }
179
179
  export const combatEvents = {
180
180
  name: 'combat_events',
181
- description: `RAF wrestling, BKFC bare-knuckle or MMA archive (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction) events.
182
-
183
- When to use:
184
- - "When is the next RAF / BKFC card?", "recent BKFC results", "PRIDE results", "Bellator cards from 2015", schedule pages
185
- - Getting an event slug for combat_card
186
-
187
- Each event carries slug, name, date, local start time + timezone, venue/location, status and bout counts.
188
- The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, and mma = all of them, each
189
- event tagged with its real promotion) are closed: "when" defaults to "recent" (newest card first), and "upcoming"
190
- returns an empty list with data.note instead of failing.
191
-
192
- Prefer over: call_api /{league}/events.
193
- Do not use when: you already have the event slug and want its bouts (combat_card).
194
-
195
- Parallel-safe: yes. API calls: 1.
181
+ description: `RAF wrestling, BKFC bare-knuckle or MMA archive (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction) events.
182
+
183
+ When to use:
184
+ - "When is the next RAF / BKFC card?", "recent BKFC results", "PRIDE results", "Bellator cards from 2015", schedule pages
185
+ - Getting an event slug for combat_card
186
+
187
+ Each event carries slug, name, date, local start time + timezone, venue/location, status and bout counts.
188
+ The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, and mma = all of them, each
189
+ event tagged with its real promotion) are closed: "when" defaults to "recent" (newest card first), and "upcoming"
190
+ returns an empty list with data.note instead of failing.
191
+
192
+ Prefer over: call_api /{league}/events.
193
+ Do not use when: you already have the event slug and want its bouts (combat_card).
194
+
195
+ Parallel-safe: yes. API calls: 1.
196
196
  Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
197
197
  inputSchema: {
198
198
  type: 'object',
@@ -228,7 +228,7 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
228
228
  source: d.source,
229
229
  requestId: d.requestId,
230
230
  tookMs: Date.now() - d.started,
231
- upstreamCalls: 0,
231
+ apiCalls: 0,
232
232
  });
233
233
  }
234
234
  const res = await fetchJson(ctx, `/${league}/events/${when}`, { query: { limit } });
@@ -236,35 +236,35 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
236
236
  return httpError(d, league, res, `${league} ${when} events`);
237
237
  const events = extractRows(res.data).slice(0, limit);
238
238
  return successEnvelope({
239
- data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events, ...upstreamNotes(res) },
239
+ data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events, ...responseNotes(res) },
240
240
  game: league,
241
241
  source: d.source,
242
242
  requestId: d.requestId,
243
243
  tookMs: Date.now() - d.started,
244
- upstreamCalls: 1,
244
+ apiCalls: 1,
245
245
  rateLimit: res.headers,
246
246
  });
247
247
  },
248
248
  };
249
249
  export const combatCard = {
250
250
  name: 'combat_card',
251
- description: `COMPOSITE fight card for one RAF, BKFC or MMA archive event: event identity + bouts in running order with results.
252
-
253
- When to use:
254
- - "RAF 14 card", "BKFC 95 results", "Bellator 142 card", "PRIDE 34", "who won the main event at the last BKFC?"
255
- - Event pages and results pages
256
-
257
- event accepts a slug (e.g. "raf13", "bkfc-95-newark", "bellator-142", "pride-34", "wec-48") or "next" / "last".
258
- Bouts come main event first; each carries weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
259
- punch totals, red/blue fighters for the MMA archives with UFC-format totals and ufcFighterSlug), and once finished
260
- the winner and result (RAF: score, takedowns, method; BKFC/MMA archives: method, round, time).
261
- MMA archives are closed: event defaults to "last" (the most recent card; for mma, across every promotion), and
262
- "next" is a non-retryable NOT_IMPLEMENTED (there are no upcoming cards).
263
-
264
- Prefer over: combat_events + call_api per bout.
265
- Do not use when: you need one bout's period/round breakdown (combat_bout).
266
-
267
- Parallel-safe: yes. API calls: 2–3.
251
+ description: `COMPOSITE fight card for one RAF, BKFC or MMA archive event: event identity + bouts in running order with results.
252
+
253
+ When to use:
254
+ - "RAF 14 card", "BKFC 95 results", "Bellator 142 card", "PRIDE 34", "who won the main event at the last BKFC?"
255
+ - Event pages and results pages
256
+
257
+ event accepts a slug (e.g. "raf13", "bkfc-95-newark", "bellator-142", "pride-34", "wec-48") or "next" / "last".
258
+ Bouts come main event first; each carries weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
259
+ punch totals, red/blue fighters for the MMA archives with UFC-format totals and ufcFighterSlug), and once finished
260
+ the winner and result (RAF: score, takedowns, method; BKFC/MMA archives: method, round, time).
261
+ MMA archives are closed: event defaults to "last" (the most recent card; for mma, across every promotion), and
262
+ "next" is a non-retryable NOT_IMPLEMENTED (there are no upcoming cards).
263
+
264
+ Prefer over: combat_events + call_api per bout.
265
+ Do not use when: you need one bout's period/round breakdown (combat_bout).
266
+
267
+ Parallel-safe: yes. API calls: 2–3.
268
268
  Example: { "league": "raf", "event": "last" }`,
269
269
  inputSchema: {
270
270
  type: 'object',
@@ -298,7 +298,7 @@ Example: { "league": "raf", "event": "last" }`,
298
298
  return httpError(d, league, list, `${league} ${which} events`);
299
299
  const first = asRecord(extractRows(list.data)[0]);
300
300
  if (!first?.slug) {
301
- const windowed = !!upstreamNotes(list).historyWindow;
301
+ const windowed = !!responseNotes(list).historyWindow;
302
302
  return errorEnvelope({
303
303
  code: 'NOT_FOUND',
304
304
  message: windowed
@@ -308,8 +308,8 @@ Example: { "league": "raf", "event": "last" }`,
308
308
  source: d.source,
309
309
  requestId: d.requestId,
310
310
  tookMs: Date.now() - d.started,
311
- upstreamCalls: calls,
312
- ...(windowed ? { retryable: false, details: { historyWindow: upstreamNotes(list).historyWindow } } : {}),
311
+ apiCalls: calls,
312
+ ...(windowed ? { retryable: false, details: { historyWindow: responseNotes(list).historyWindow } } : {}),
313
313
  });
314
314
  }
315
315
  slug = String(first.slug);
@@ -338,7 +338,7 @@ Example: { "league": "raf", "event": "last" }`,
338
338
  source: d.source,
339
339
  requestId: d.requestId,
340
340
  tookMs: Date.now() - d.started,
341
- upstreamCalls: calls,
341
+ apiCalls: calls,
342
342
  rateLimit: bouts.headers ?? ev.headers,
343
343
  partial,
344
344
  truncated: rows.length > limit
@@ -349,22 +349,22 @@ Example: { "league": "raf", "event": "last" }`,
349
349
  };
350
350
  export const combatBout = {
351
351
  name: 'combat_bout',
352
- description: `One RAF, BKFC or MMA archive bout in full, with its scoring breakdown.
353
-
354
- RAF: winner, method (decision / tech fall / pin / injury), final score, takedowns, and per-PERIOD scores.
355
- BKFC: winner, method, round, time, and per-ROUND punch stats for red and blue (landed/thrown, head/body,
356
- distance/clinch, knockdowns). BKFC round stats need the Starter plan; on Free they come back as a partial note.
357
- MMA archives (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, mma): winner, method, round, time,
358
- finish (position / submission), referee, judges' scores, each corner's whole-fight totals, and per-ROUND official
359
- stats in UFC's format (significantStrikes "13 of 18", totalStrikes, takedowns, head/body/leg, distance/clinch/ground,
360
- knockdowns, submissionAttempts, reversals, controlTime "1:42"). PRIDE's first round is 10 minutes. The archives are
361
- older than the Free/Starter history window: on those plans rounds come back as a HISTORY_WINDOW_EXCEEDED partial
362
- note while the result still returns.
363
-
364
- Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington", "bellator-5600").
365
- MMA archive bout ids are "<promotion>-<fightId>"; a bare fight id resolves in any MMA archive league.
366
-
367
- Parallel-safe: yes. API calls: 1–2.
352
+ description: `One RAF, BKFC or MMA archive bout in full, with its scoring breakdown.
353
+
354
+ RAF: winner, method (decision / tech fall / pin / injury), final score, takedowns, and per-PERIOD scores.
355
+ BKFC: winner, method, round, time, and per-ROUND punch stats for red and blue (landed/thrown, head/body,
356
+ distance/clinch, knockdowns). BKFC round stats need the Starter plan; on Free they come back as a partial note.
357
+ MMA archives (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, mma): winner, method, round, time,
358
+ finish (position / submission), referee, judges' scores, each corner's whole-fight totals, and per-ROUND official
359
+ stats in UFC's format (significantStrikes "13 of 18", totalStrikes, takedowns, head/body/leg, distance/clinch/ground,
360
+ knockdowns, submissionAttempts, reversals, controlTime "1:42"). PRIDE's first round is 10 minutes. The archives are
361
+ older than the Free/Starter history window: on those plans rounds come back as a HISTORY_WINDOW_EXCEEDED partial
362
+ note while the result still returns.
363
+
364
+ Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington", "bellator-5600").
365
+ MMA archive bout ids are "<promotion>-<fightId>"; a bare fight id resolves in any MMA archive league.
366
+
367
+ Parallel-safe: yes. API calls: 1–2.
368
368
  Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cisneros--ryan-petersen" }`,
369
369
  inputSchema: {
370
370
  type: 'object',
@@ -414,7 +414,7 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
414
414
  source: d.source,
415
415
  requestId: d.requestId,
416
416
  tookMs: Date.now() - d.started,
417
- upstreamCalls: parts ? 2 : 1,
417
+ apiCalls: parts ? 2 : 1,
418
418
  rateLimit: bout.headers,
419
419
  partial,
420
420
  });
@@ -422,24 +422,24 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
422
422
  };
423
423
  export const combatAthlete = {
424
424
  name: 'combat_athlete',
425
- description: `COMPOSITE RAF athlete / BKFC or MMA archive fighter page: profile + recent bouts + career stats.
426
-
427
- When to use:
428
- - "Kyle Snyder's RAF record", "Arlovski BKFC stats", "Tito Ortiz in Bellator", "Fedor's PRIDE record", athlete/fighter profile pages
429
-
430
- athlete accepts a slug ("kyle-snyder") or a name ("Andrei Arlovski"); names are resolved through the league's
431
- search, and several matches return AMBIGUOUS_ENTITY with candidates. Profile carries record, division, nickname,
432
- country, official headshot (+ its credit — show it with the image). Career stats need the Starter plan; on Free
433
- they come back as a partial note while profile and history still return.
434
- MMA archives: profile is bio (DOB, height, reach, stance, hometown) + the record in that promotion's archive (not the
435
- whole career); stats are career totals in UFC's format. league "mma" covers every archived promotion at once, and
436
- data.recordByPromotion splits the record ({ pride: {...}, strikeforce: {...} }). A fighter we also hold a UFC record
437
- for keeps the SAME slug as /ufc/fighters/{slug}: data.ufcFighterSlug is set then (a UFC slug is accepted here too).
438
- Keep the records apart: archive fights never belong in a UFC answer.
439
-
440
- Prefer over: call_api profile + history + stats separately.
441
-
442
- Parallel-safe: yes. API calls: 2–4.
425
+ description: `COMPOSITE RAF athlete / BKFC or MMA archive fighter page: profile + recent bouts + career stats.
426
+
427
+ When to use:
428
+ - "Kyle Snyder's RAF record", "Arlovski BKFC stats", "Tito Ortiz in Bellator", "Fedor's PRIDE record", athlete/fighter profile pages
429
+
430
+ athlete accepts a slug ("kyle-snyder") or a name ("Andrei Arlovski"); names are resolved through the league's
431
+ search, and several matches return AMBIGUOUS_ENTITY with candidates. Profile carries record, division, nickname,
432
+ country, official headshot (+ its credit — show it with the image). Career stats need the Starter plan; on Free
433
+ they come back as a partial note while profile and history still return.
434
+ MMA archives: profile is bio (DOB, height, reach, stance, hometown) + the record in that promotion's archive (not the
435
+ whole career); stats are career totals in UFC's format. league "mma" covers every archived promotion at once, and
436
+ data.recordByPromotion splits the record ({ pride: {...}, strikeforce: {...} }). A fighter we also hold a UFC record
437
+ for keeps the SAME slug as /ufc/fighters/{slug}: data.ufcFighterSlug is set then (a UFC slug is accepted here too).
438
+ Keep the records apart: archive fights never belong in a UFC answer.
439
+
440
+ Prefer over: call_api profile + history + stats separately.
441
+
442
+ Parallel-safe: yes. API calls: 2–4.
443
443
  Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
444
444
  inputSchema: {
445
445
  type: 'object',
@@ -502,7 +502,7 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
502
502
  source: d.source,
503
503
  requestId: d.requestId,
504
504
  tookMs: Date.now() - d.started,
505
- upstreamCalls: 1 + (history ? 1 : 0) + (stats ? 1 : 0),
505
+ apiCalls: 1 + (history ? 1 : 0) + (stats ? 1 : 0),
506
506
  rateLimit: profile.headers,
507
507
  partial,
508
508
  });
@@ -510,13 +510,13 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
510
510
  };
511
511
  export const combatH2h = {
512
512
  name: 'combat_h2h',
513
- description: `Head-to-head between two RAF athletes, two BKFC fighters or two MMA archive fighters: record and every meeting.
514
-
515
- Accepts slugs or names for both sides (names resolve through the league's search). MMA archive meetings are only
516
- the ones on that promotion's cards (league "mma": every archived promotion); UFC meetings are a UFC question
517
- (head_to_head).
518
-
519
- Parallel-safe: yes. API calls: 1–3.
513
+ description: `Head-to-head between two RAF athletes, two BKFC fighters or two MMA archive fighters: record and every meeting.
514
+
515
+ Accepts slugs or names for both sides (names resolve through the league's search). MMA archive meetings are only
516
+ the ones on that promotion's cards (league "mma": every archived promotion); UFC meetings are a UFC question
517
+ (head_to_head).
518
+
519
+ Parallel-safe: yes. API calls: 1–3.
520
520
  Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-cisneros" }`,
521
521
  inputSchema: {
522
522
  type: 'object',
@@ -557,23 +557,23 @@ Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-
557
557
  source: d.source,
558
558
  requestId: d.requestId,
559
559
  tookMs: Date.now() - d.started,
560
- upstreamCalls: 1,
560
+ apiCalls: 1,
561
561
  rateLimit: res.headers,
562
562
  });
563
563
  },
564
564
  };
565
565
  export const combatRankings = {
566
566
  name: 'combat_rankings',
567
- description: `Champions and rankings: RAF title holders per division, BKFC official rankings + champions.
568
-
569
- BKFC: omit division for every division's ranked list (champion, interim champion, ranked fighters), or pass one
570
- (e.g. "heavyweight", "womens-flyweight"). RAF has no ranked lists; it returns every title and its holder, or one
571
- division's detail when division is given (e.g. "men-heavyweight").
572
- The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, mma) have no rankings: they return a
573
- non-retryable NOT_IMPLEMENTED.
574
- Never answer with UFC rankings instead.
575
-
576
- Parallel-safe: yes. API calls: 1.
567
+ description: `Champions and rankings: RAF title holders per division, BKFC official rankings + champions.
568
+
569
+ BKFC: omit division for every division's ranked list (champion, interim champion, ranked fighters), or pass one
570
+ (e.g. "heavyweight", "womens-flyweight"). RAF has no ranked lists; it returns every title and its holder, or one
571
+ division's detail when division is given (e.g. "men-heavyweight").
572
+ The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, mma) have no rankings: they return a
573
+ non-retryable NOT_IMPLEMENTED.
574
+ Never answer with UFC rankings instead.
575
+
576
+ Parallel-safe: yes. API calls: 1.
577
577
  Example: { "league": "bkfc", "division": "heavyweight" }`,
578
578
  inputSchema: {
579
579
  type: 'object',
@@ -610,7 +610,7 @@ Example: { "league": "bkfc", "division": "heavyweight" }`,
610
610
  source: d.source,
611
611
  requestId: d.requestId,
612
612
  tookMs: Date.now() - d.started,
613
- upstreamCalls: 1,
613
+ apiCalls: 1,
614
614
  rateLimit: res.headers,
615
615
  });
616
616
  },