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.
@@ -31,18 +31,18 @@ function numberOrNull(value) {
31
31
  }
32
32
  export const teamProfile = {
33
33
  name: 'team_profile',
34
- description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
35
-
36
- When to use:
37
- - Team page / "who is on this roster?"
38
- - Builder team screen sample
39
-
40
- Prefer over: separate roster + matches + detail via call_api.
41
-
42
- Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
43
- Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
44
-
45
- Parallel-safe: yes. API calls: 2–4.
34
+ description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
35
+
36
+ When to use:
37
+ - Team page / "who is on this roster?"
38
+ - Builder team screen sample
39
+
40
+ Prefer over: separate roster + matches + detail via call_api.
41
+
42
+ Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
43
+ Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
44
+
45
+ Parallel-safe: yes. API calls: 2–4.
46
46
  Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
47
47
  inputSchema: {
48
48
  type: 'object',
@@ -128,12 +128,12 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
128
128
  const idOrSlug = teamId || slug;
129
129
  const recentLimit = clampInt(args.recentLimit, 10, 1, 25);
130
130
  const partial = [];
131
- let upstreamCalls = 0;
131
+ let apiCalls = 0;
132
132
  let rateLimit = {};
133
133
  let teamRaw = null;
134
134
  if (game === 'lol') {
135
135
  const res = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(slug || idOrSlug)}`);
136
- upstreamCalls += 1;
136
+ apiCalls += 1;
137
137
  rateLimit = res.headers;
138
138
  if (!res.ok) {
139
139
  return errorEnvelope({
@@ -143,7 +143,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
143
143
  source: 'team_profile',
144
144
  requestId,
145
145
  tookMs: Date.now() - started,
146
- upstreamCalls,
146
+ apiCalls,
147
147
  httpStatus: res.status,
148
148
  rateLimit: res.headers,
149
149
  });
@@ -155,7 +155,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
155
155
  const res = directId
156
156
  ? await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(idOrSlug)}`)
157
157
  : await fetchJson(ctx, '/cs2/teams', { query: { search: idOrSlug, limit: 5 } });
158
- upstreamCalls += 1;
158
+ apiCalls += 1;
159
159
  rateLimit = res.headers;
160
160
  if (!res.ok) {
161
161
  return errorEnvelope({
@@ -165,7 +165,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
165
165
  source: 'team_profile',
166
166
  requestId,
167
167
  tookMs: Date.now() - started,
168
- upstreamCalls,
168
+ apiCalls,
169
169
  httpStatus: res.status,
170
170
  rateLimit: res.headers,
171
171
  recover: [
@@ -192,7 +192,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
192
192
  source: 'team_profile',
193
193
  requestId,
194
194
  tookMs: Date.now() - started,
195
- upstreamCalls,
195
+ apiCalls,
196
196
  rateLimit: res.headers,
197
197
  recover: [
198
198
  'Call resolve_entity with the team name and type=team',
@@ -204,7 +204,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
204
204
  }
205
205
  else if (game === 'dota2') {
206
206
  const res = await fetchJson(ctx, '/dota2/teams', { query: { search: idOrSlug, limit: 5 } });
207
- upstreamCalls += 1;
207
+ apiCalls += 1;
208
208
  rateLimit = res.headers;
209
209
  if (!res.ok) {
210
210
  return errorEnvelope({
@@ -214,7 +214,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
214
214
  source: 'team_profile',
215
215
  requestId,
216
216
  tookMs: Date.now() - started,
217
- upstreamCalls,
217
+ apiCalls,
218
218
  httpStatus: res.status,
219
219
  rateLimit: res.headers,
220
220
  recover: [
@@ -238,7 +238,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
238
238
  source: 'team_profile',
239
239
  requestId,
240
240
  tookMs: Date.now() - started,
241
- upstreamCalls,
241
+ apiCalls,
242
242
  rateLimit: res.headers,
243
243
  recover: [
244
244
  'Call resolve_entity with the team name and type=team',
@@ -250,7 +250,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
250
250
  }
251
251
  else if (game === 'cod') {
252
252
  const res = await fetchJson(ctx, `/cod/orgs/${encodeURIComponent(slug || idOrSlug)}`);
253
- upstreamCalls += 1;
253
+ apiCalls += 1;
254
254
  rateLimit = res.headers;
255
255
  if (!res.ok) {
256
256
  return errorEnvelope({
@@ -260,7 +260,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
260
260
  source: 'team_profile',
261
261
  requestId,
262
262
  tookMs: Date.now() - started,
263
- upstreamCalls,
263
+ apiCalls,
264
264
  httpStatus: res.status,
265
265
  rateLimit: res.headers,
266
266
  });
@@ -279,7 +279,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
279
279
  if (game === 'lol') {
280
280
  tasks.push((async () => {
281
281
  const res = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(team.slug || idOrSlug)}/roster`);
282
- upstreamCalls += 1;
282
+ apiCalls += 1;
283
283
  rateLimit = { ...rateLimit, ...res.headers };
284
284
  if (res.ok) {
285
285
  roster = {
@@ -301,7 +301,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
301
301
  const res = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(slug)}/matches`, {
302
302
  query: { limit: String(recentLimit) },
303
303
  });
304
- upstreamCalls += 1;
304
+ apiCalls += 1;
305
305
  rateLimit = { ...rateLimit, ...res.headers };
306
306
  if (res.ok) {
307
307
  const startMs = (row) => {
@@ -340,7 +340,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
340
340
  const tid = team.id !== 'unknown' ? team.id : idOrSlug;
341
341
  tasks.push((async () => {
342
342
  const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(tid)}/roster`);
343
- upstreamCalls += 1;
343
+ apiCalls += 1;
344
344
  rateLimit = { ...rateLimit, ...res.headers };
345
345
  if (res.ok) {
346
346
  roster = {
@@ -359,7 +359,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
359
359
  })());
360
360
  tasks.push((async () => {
361
361
  const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(tid)}/trends`);
362
- upstreamCalls += 1;
362
+ apiCalls += 1;
363
363
  rateLimit = { ...rateLimit, ...res.headers };
364
364
  if (res.ok)
365
365
  form = unwrapPayload(res.data) ?? res.data;
@@ -375,7 +375,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
375
375
  const res = await fetchJson(ctx, '/cs2/matches', {
376
376
  query: { team: tid, limit: recentLimit, status: 'completed' },
377
377
  });
378
- upstreamCalls += 1;
378
+ apiCalls += 1;
379
379
  rateLimit = { ...rateLimit, ...res.headers };
380
380
  if (res.ok) {
381
381
  recentMatches = extractRows(res.data)
@@ -400,7 +400,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
400
400
  }));
401
401
  tasks.push((async () => {
402
402
  const res = await fetchJson(ctx, `/dota2/teams/${encodeURIComponent(idOrSlug)}/radar`);
403
- upstreamCalls += 1;
403
+ apiCalls += 1;
404
404
  rateLimit = { ...rateLimit, ...res.headers };
405
405
  if (res.ok)
406
406
  form = { radar: res.data };
@@ -414,7 +414,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
414
414
  })());
415
415
  tasks.push((async () => {
416
416
  const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: recentLimit } });
417
- upstreamCalls += 1;
417
+ apiCalls += 1;
418
418
  rateLimit = { ...rateLimit, ...res.headers };
419
419
  if (res.ok) {
420
420
  const t = idOrSlug.toLowerCase();
@@ -443,7 +443,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
443
443
  const res = await fetchJson(ctx, `/cod/orgs/${encodeURIComponent(team.slug || idOrSlug)}/roster`, {
444
444
  query: { includeFormer: args.includeFormer === true },
445
445
  });
446
- upstreamCalls += 1;
446
+ apiCalls += 1;
447
447
  rateLimit = { ...rateLimit, ...res.headers };
448
448
  if (res.ok) {
449
449
  roster = {
@@ -464,7 +464,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
464
464
  const res = await fetchJson(ctx, '/cod/matches', {
465
465
  query: { team: team.slug || idOrSlug, limit: recentLimit },
466
466
  });
467
- upstreamCalls += 1;
467
+ apiCalls += 1;
468
468
  rateLimit = { ...rateLimit, ...res.headers };
469
469
  if (res.ok) {
470
470
  recentMatches = extractRows(res.data)
@@ -486,7 +486,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
486
486
  game,
487
487
  requestId,
488
488
  tookMs: Date.now() - started,
489
- upstreamCalls,
489
+ apiCalls,
490
490
  rateLimit,
491
491
  partial: partial.length ? partial : undefined,
492
492
  entities: {
@@ -532,21 +532,21 @@ function winnerSide(match, a) {
532
532
  }
533
533
  export const headToHead = {
534
534
  name: 'head_to_head',
535
- description: `Head-to-head record between two teams, two UFC fighters, or two tennis players. Tennis reads the tennis head-to-head route; other games filter match history server-side.
536
-
537
- When to use:
538
- - Rivalry / series record questions
539
- - Supporting context for previews
540
-
541
- Prefer over: agent-side double match-list filtering.
542
-
543
- Do not use when: single-side form only → team_profile or player_profile.
544
-
545
- Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
546
-
547
- Tennis: pass entityType "player" with sideA and sideB (ids like atp_207989, or names). This tool has no players param; for three or more players use h2h_matrix, which does. limit pages the meetings list only (max 50); every tally covers all meetings. In the payload current_rank is null when a player is not on the tour's latest list: read last_rank. The tiebreak and level_breakdown keys are in data.h2h.
548
-
549
- Parallel-safe: yes. API calls: 2–4.
535
+ description: `Head-to-head record between two teams, two UFC fighters, or two tennis players. Tennis reads the tennis head-to-head route; other games filter match history server-side.
536
+
537
+ When to use:
538
+ - Rivalry / series record questions
539
+ - Supporting context for previews
540
+
541
+ Prefer over: agent-side double match-list filtering.
542
+
543
+ Do not use when: single-side form only → team_profile or player_profile.
544
+
545
+ Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
546
+
547
+ Tennis: pass entityType "player" with sideA and sideB (ids like atp_207989, or names). This tool has no players param; for three or more players use h2h_matrix, which does. limit pages the meetings list only (max 50); every tally covers all meetings. In the payload current_rank is null when a player is not on the tour's latest list: read last_rank. The tiebreak and level_breakdown keys are in data.h2h.
548
+
549
+ Parallel-safe: yes. API calls: 2–4.
550
550
  Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
551
551
  inputSchema: {
552
552
  type: 'object',
@@ -600,7 +600,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
600
600
  const fromMs = fromIso ? Date.parse(fromIso) : NaN;
601
601
  const toMs = toIso ? Date.parse(toIso) : NaN;
602
602
  const warnings = [];
603
- let upstreamCalls = 0;
603
+ let apiCalls = 0;
604
604
  let rateLimit = {};
605
605
  let rows = [];
606
606
  let matchA = sideA;
@@ -608,7 +608,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
608
608
  if (game === 'cs2') {
609
609
  const resolveCs2Side = async (side) => {
610
610
  const res = await fetchJson(ctx, '/cs2/teams', { query: { search: side, limit: 5 } });
611
- upstreamCalls += 1;
611
+ apiCalls += 1;
612
612
  rateLimit = { ...rateLimit, ...res.headers };
613
613
  if (!res.ok)
614
614
  return null;
@@ -627,7 +627,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
627
627
  const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(cs2SideA.id)}/vs/${encodeURIComponent(cs2SideB.id)}`, {
628
628
  query: { limit: 50 },
629
629
  });
630
- upstreamCalls += 1;
630
+ apiCalls += 1;
631
631
  rateLimit = { ...rateLimit, ...res.headers };
632
632
  if (res.ok) {
633
633
  rows = extractRows(res.data).map((row) => {
@@ -654,7 +654,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
654
654
  }
655
655
  if (!restH2hOk) {
656
656
  const res = await fetchJson(ctx, '/cs2/matches', { query: { team: sideA, limit: 50 } });
657
- upstreamCalls += 1;
657
+ apiCalls += 1;
658
658
  rateLimit = { ...rateLimit, ...res.headers };
659
659
  if (!res.ok) {
660
660
  return errorEnvelope({
@@ -664,7 +664,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
664
664
  source: 'head_to_head',
665
665
  requestId,
666
666
  tookMs: Date.now() - started,
667
- upstreamCalls,
667
+ apiCalls,
668
668
  httpStatus: res.status,
669
669
  rateLimit: res.headers,
670
670
  });
@@ -674,7 +674,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
674
674
  }
675
675
  else if (game === 'cod') {
676
676
  const res = await fetchJson(ctx, '/cod/matches', { query: { team: sideA, limit: 50 } });
677
- upstreamCalls += 1;
677
+ apiCalls += 1;
678
678
  rateLimit = res.headers;
679
679
  if (!res.ok) {
680
680
  return errorEnvelope({
@@ -684,7 +684,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
684
684
  source: 'head_to_head',
685
685
  requestId,
686
686
  tookMs: Date.now() - started,
687
- upstreamCalls,
687
+ apiCalls,
688
688
  httpStatus: res.status,
689
689
  rateLimit: res.headers,
690
690
  });
@@ -693,7 +693,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
693
693
  }
694
694
  else if (game === 'lol') {
695
695
  const h2hRes = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(sideA)}/h2h/${encodeURIComponent(sideB)}`, { query: { limit: String(limit) } });
696
- upstreamCalls += 1;
696
+ apiCalls += 1;
697
697
  rateLimit = { ...rateLimit, ...h2hRes.headers };
698
698
  if (h2hRes.ok) {
699
699
  const env = asRecord(h2hRes.data) ?? {};
@@ -727,7 +727,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
727
727
  game,
728
728
  requestId,
729
729
  tookMs: Date.now() - started,
730
- upstreamCalls,
730
+ apiCalls,
731
731
  rateLimit,
732
732
  data: {
733
733
  sideA: { idOrSlug: sideA },
@@ -749,7 +749,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
749
749
  const res = await fetchJson(ctx, '/lol/schedule', {
750
750
  query: { teamSlug: sideA, completed: 'true', limit: '50' },
751
751
  });
752
- upstreamCalls += 1;
752
+ apiCalls += 1;
753
753
  rateLimit = res.headers;
754
754
  if (!res.ok) {
755
755
  return errorEnvelope({
@@ -759,7 +759,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
759
759
  source: 'head_to_head',
760
760
  requestId,
761
761
  tookMs: Date.now() - started,
762
- upstreamCalls,
762
+ apiCalls,
763
763
  httpStatus: res.status,
764
764
  rateLimit: res.headers,
765
765
  });
@@ -769,7 +769,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
769
769
  else if (game === 'dota2') {
770
770
  warnings.push('Dota H2H uses recent matches with weak filters; record may be incomplete');
771
771
  const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: 50 } });
772
- upstreamCalls += 1;
772
+ apiCalls += 1;
773
773
  rateLimit = res.headers;
774
774
  if (!res.ok) {
775
775
  return errorEnvelope({
@@ -779,7 +779,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
779
779
  source: 'head_to_head',
780
780
  requestId,
781
781
  tookMs: Date.now() - started,
782
- upstreamCalls,
782
+ apiCalls,
783
783
  httpStatus: res.status,
784
784
  rateLimit: res.headers,
785
785
  });
@@ -793,7 +793,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
793
793
  fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideA)}/fights`, { query: { limit: 50 } }),
794
794
  fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideB)}/fights`, { query: { limit: 50 } }),
795
795
  ]);
796
- upstreamCalls += 4;
796
+ apiCalls += 4;
797
797
  rateLimit = { ...aRes.headers, ...bRes.headers, ...histA.headers, ...histB.headers };
798
798
  if (!aRes.ok || !bRes.ok) {
799
799
  return errorEnvelope({
@@ -803,7 +803,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
803
803
  source: 'head_to_head',
804
804
  requestId,
805
805
  tookMs: Date.now() - started,
806
- upstreamCalls,
806
+ apiCalls,
807
807
  recover: [
808
808
  'resolve_entity { game: "ufc", type: "fighter", q } for each name',
809
809
  'Retry head_to_head with returned slugs as sideA/sideB',
@@ -833,7 +833,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
833
833
  if (/^(atp|wta)_\d+$/i.test(side))
834
834
  return { id: side, name: side };
835
835
  const res = await fetchJson(ctx, '/tennis/players/search', { query: { q: side, limit: 3 } });
836
- upstreamCalls += 1;
836
+ apiCalls += 1;
837
837
  rateLimit = { ...rateLimit, ...res.headers };
838
838
  if (!res.ok)
839
839
  return null;
@@ -852,7 +852,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
852
852
  source: 'head_to_head',
853
853
  requestId,
854
854
  tookMs: Date.now() - started,
855
- upstreamCalls,
855
+ apiCalls,
856
856
  rateLimit,
857
857
  recover: [
858
858
  'resolve_entity { game: "tennis", type: "player", q } for each name',
@@ -864,7 +864,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
864
864
  if (args.limit !== undefined && args.limit !== null)
865
865
  tennisQuery.limit = limit;
866
866
  const res = await fetchJson(ctx, '/tennis/h2h', { query: tennisQuery });
867
- upstreamCalls += 1;
867
+ apiCalls += 1;
868
868
  rateLimit = { ...rateLimit, ...res.headers };
869
869
  if (!res.ok) {
870
870
  return errorEnvelope({
@@ -874,7 +874,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
874
874
  source: 'head_to_head',
875
875
  requestId,
876
876
  tookMs: Date.now() - started,
877
- upstreamCalls,
877
+ apiCalls,
878
878
  httpStatus: res.status,
879
879
  rateLimit,
880
880
  });
@@ -890,7 +890,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
890
890
  game,
891
891
  requestId,
892
892
  tookMs: Date.now() - started,
893
- upstreamCalls,
893
+ apiCalls,
894
894
  rateLimit,
895
895
  data: {
896
896
  sideA: { idOrSlug: tA.id, name: sideAName },
@@ -952,7 +952,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
952
952
  game,
953
953
  requestId,
954
954
  tookMs: Date.now() - started,
955
- upstreamCalls,
955
+ apiCalls,
956
956
  rateLimit,
957
957
  warnings: warnings.length ? warnings : undefined,
958
958
  data: {
@@ -972,18 +972,18 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
972
972
  };
973
973
  export const h2hMatrix = {
974
974
  name: 'h2h_matrix',
975
- description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
976
-
977
- When to use:
978
- - Draw/field analysis: how each contender fares against every other (e.g. Alcaraz vs Zverev vs Sinner round-robin records)
979
- - Group-stage or semifinal-field comparisons
980
-
981
- Prefer over: N head_to_head calls for an N-player field; raw matrix via call_api.
982
-
983
- Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits → head_to_head; season stat leaders → leaderboard_*; rankings → standings.
984
-
985
- Tennis-only. players takes 2-16 ids or names (names resolve via player search). Each matrix cell is "W-L" from the row player's perspective, "-" on the diagonal.
986
-
975
+ description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
976
+
977
+ When to use:
978
+ - Draw/field analysis: how each contender fares against every other (e.g. Alcaraz vs Zverev vs Sinner round-robin records)
979
+ - Group-stage or semifinal-field comparisons
980
+
981
+ Prefer over: N head_to_head calls for an N-player field; raw matrix via call_api.
982
+
983
+ Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits → head_to_head; season stat leaders → leaderboard_*; rankings → standings.
984
+
985
+ Tennis-only. players takes 2-16 ids or names (names resolve via player search). Each matrix cell is "W-L" from the row player's perspective, "-" on the diagonal.
986
+
987
987
  Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
988
988
  inputSchema: {
989
989
  type: 'object',
@@ -1044,13 +1044,13 @@ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
1044
1044
  recover: ['Pass 2-16 player ids (atp_207989) or names ("Carlos Alcaraz")'],
1045
1045
  });
1046
1046
  }
1047
- let upstreamCalls = 0;
1047
+ let apiCalls = 0;
1048
1048
  let rateLimit = {};
1049
1049
  const resolveTennisSide = async (side) => {
1050
1050
  if (/^(atp|wta)_\d+$/i.test(side))
1051
1051
  return { id: side, name: side };
1052
1052
  const res = await fetchJson(ctx, '/tennis/players/search', { query: { q: side, limit: 3 } });
1053
- upstreamCalls += 1;
1053
+ apiCalls += 1;
1054
1054
  rateLimit = { ...rateLimit, ...res.headers };
1055
1055
  if (!res.ok)
1056
1056
  return null;
@@ -1070,7 +1070,7 @@ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
1070
1070
  source: 'h2h_matrix',
1071
1071
  requestId,
1072
1072
  tookMs: Date.now() - started,
1073
- upstreamCalls,
1073
+ apiCalls,
1074
1074
  rateLimit,
1075
1075
  recover: [
1076
1076
  'resolve_entity { game: "tennis", type: "player", q } for each missing name',
@@ -1087,14 +1087,14 @@ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
1087
1087
  source: 'h2h_matrix',
1088
1088
  requestId,
1089
1089
  tookMs: Date.now() - started,
1090
- upstreamCalls,
1090
+ apiCalls,
1091
1091
  rateLimit,
1092
1092
  });
1093
1093
  }
1094
1094
  const res = await fetchJson(ctx, '/tennis/h2h/matrix', {
1095
1095
  query: { player_ids: ids.join(',') },
1096
1096
  });
1097
- upstreamCalls += 1;
1097
+ apiCalls += 1;
1098
1098
  rateLimit = { ...rateLimit, ...res.headers };
1099
1099
  if (!res.ok) {
1100
1100
  return errorEnvelope({
@@ -1104,7 +1104,7 @@ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
1104
1104
  source: 'h2h_matrix',
1105
1105
  requestId,
1106
1106
  tookMs: Date.now() - started,
1107
- upstreamCalls,
1107
+ apiCalls,
1108
1108
  httpStatus: res.status,
1109
1109
  rateLimit,
1110
1110
  });
@@ -1125,7 +1125,7 @@ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
1125
1125
  game,
1126
1126
  requestId,
1127
1127
  tookMs: Date.now() - started,
1128
- upstreamCalls,
1128
+ apiCalls,
1129
1129
  rateLimit,
1130
1130
  data: {
1131
1131
  players,
@@ -202,7 +202,7 @@ Parallel-safe: yes. API calls: 1.`,
202
202
  source: 'tournaments',
203
203
  requestId,
204
204
  tookMs: Date.now() - started,
205
- upstreamCalls: 1,
205
+ apiCalls: 1,
206
206
  httpStatus: res.status,
207
207
  rateLimit: res.headers,
208
208
  });
@@ -248,7 +248,7 @@ Parallel-safe: yes. API calls: 1.`,
248
248
  game,
249
249
  requestId,
250
250
  tookMs: Date.now() - started,
251
- upstreamCalls: 1,
251
+ apiCalls: 1,
252
252
  rateLimit: res.headers,
253
253
  warnings: warnings.length ? warnings : undefined,
254
254
  data: {
@@ -25,7 +25,7 @@ async function callApi(ctx, tool, method, path, opts = {}, shape = (b) => b) {
25
25
  ...(body.code || body.upgradeUrl ? { details: { ...(body.code ? { apiCode: body.code } : {}), ...(body.upgradeUrl ? { upgradeUrl: body.upgradeUrl, currentPlan: body.currentPlan, requiredPlan: body.requiredPlan } : {}) } } : {}),
26
26
  });
27
27
  }
28
- return successEnvelope({ data: shape(body), game: null, source: tool, tookMs: Date.now() - started, upstreamCalls: 1 });
28
+ return successEnvelope({ data: shape(body), game: null, source: tool, tookMs: Date.now() - started, apiCalls: 1 });
29
29
  }
30
30
  function bad(tool, message, recover) {
31
31
  return errorEnvelope({ code: 'VALIDATION', message, game: null, source: tool, retryable: false, recover });
@@ -119,7 +119,7 @@ export const webhookTools = [
119
119
  },
120
120
  game: null,
121
121
  source: 'explain_webhook_setup',
122
- upstreamCalls: 3,
122
+ apiCalls: 3,
123
123
  });
124
124
  },
125
125
  },
package/package.json CHANGED
@@ -1,52 +1,51 @@
1
- {
2
- "name": "cito-mcp",
3
- "version": "0.6.0",
4
- "description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
5
- "type": "module",
6
- "homepage": "https://cito.gg",
7
- "//REPOSITORY": "Intentionally NOT set. cito-api-scraper and cito.gg are PRIVATE repos and must stay that way, so a repository URL pointing at them would render as a broken 404 link on the public npm page and would not satisfy MCP Registry npm ownership validation. Set this only if a PUBLIC repo for this package is ever created (e.g. achillesscriptsvip/cito-mcp with this directory as root) — then use `git+https://github.com/OWNER/REPO.git` and mirror it in the `mcpName`/server.json `name` pair. Do not point it at the private monorepo.",
8
- "keywords": [
9
- "mcp",
10
- "model-context-protocol",
11
- "esports",
12
- "cs2",
13
- "counter-strike",
14
- "counter-strike-2",
15
- "lol",
16
- "league-of-legends",
17
- "dota2",
18
- "cod",
19
- "ufc",
20
- "mma",
21
- "bellator",
22
- "tennis",
23
- "live-scores",
24
- "sports-data",
25
- "esports-api"
26
- ],
27
- "bin": {
28
- "cito-mcp": "dist/index.js"
29
- },
30
- "files": [
31
- "dist",
32
- "README.md"
33
- ],
34
- "engines": {
35
- "node": ">=20"
36
- },
37
- "scripts": {
38
- "build": "tsc -p tsconfig.json",
39
- "test": "tsx --test src/**/*.test.ts src/*.test.ts",
40
- "start": "node dist/index.js",
41
- "prepublishOnly": "npm run build && npm test && npm run smoke",
42
- "smoke": "node scripts/smoke.mjs"
43
- },
44
- "dependencies": {
45
- "@modelcontextprotocol/sdk": "1.29.0"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^22.10.0",
49
- "tsx": "^4.23.1",
50
- "typescript": "^5.9.2"
51
- }
52
- }
1
+ {
2
+ "name": "cito-mcp",
3
+ "version": "0.6.1",
4
+ "description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
5
+ "type": "module",
6
+ "homepage": "https://cito.gg",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "esports",
11
+ "cs2",
12
+ "counter-strike",
13
+ "counter-strike-2",
14
+ "lol",
15
+ "league-of-legends",
16
+ "dota2",
17
+ "cod",
18
+ "ufc",
19
+ "mma",
20
+ "bellator",
21
+ "tennis",
22
+ "live-scores",
23
+ "sports-data",
24
+ "esports-api"
25
+ ],
26
+ "bin": {
27
+ "cito-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "test": "tsx --test src/**/*.test.ts src/*.test.ts",
39
+ "start": "node dist/index.js",
40
+ "prepublishOnly": "npm run build && npm test && npm run smoke",
41
+ "smoke": "node scripts/smoke.mjs"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "1.29.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.10.0",
48
+ "tsx": "^4.23.1",
49
+ "typescript": "^5.9.2"
50
+ }
51
+ }