cito-mcp 0.5.8 → 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. Upstream cost: 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,19 +532,21 @@ function winnerSide(match, a) {
532
532
  }
533
533
  export const headToHead = {
534
534
  name: 'head_to_head',
535
- description: `Composed head-to-head record between two teams, two UFC fighters, or two tennis players. No first-class REST H2H exists — this tool filters 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
- Parallel-safe: yes. Upstream cost: 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.
548
550
  Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
549
551
  inputSchema: {
550
552
  type: 'object',
@@ -558,8 +560,8 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
558
560
  default: 'team',
559
561
  description: 'team (default), fighter (UFC), or player (tennis).',
560
562
  },
561
- sideA: stringSchema('Id or slug for side A.', 'faze'),
562
- sideB: stringSchema('Id or slug for side B.', 'navi'),
563
+ sideA: stringSchema('Id or slug for side A (tennis: atp_207989 or a player name).', 'faze'),
564
+ sideB: stringSchema('Id or slug for side B (tennis: atp_206173 or a player name).', 'navi'),
563
565
  limit: limitSchema({ default: 20, max: 50, description: 'Max meetings returned.' }),
564
566
  from: stringSchema('ISO-8601 lower bound on meeting startTime (client filter).'),
565
567
  to: stringSchema('ISO-8601 upper bound on meeting startTime (client filter).'),
@@ -598,7 +600,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
598
600
  const fromMs = fromIso ? Date.parse(fromIso) : NaN;
599
601
  const toMs = toIso ? Date.parse(toIso) : NaN;
600
602
  const warnings = [];
601
- let upstreamCalls = 0;
603
+ let apiCalls = 0;
602
604
  let rateLimit = {};
603
605
  let rows = [];
604
606
  let matchA = sideA;
@@ -606,7 +608,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
606
608
  if (game === 'cs2') {
607
609
  const resolveCs2Side = async (side) => {
608
610
  const res = await fetchJson(ctx, '/cs2/teams', { query: { search: side, limit: 5 } });
609
- upstreamCalls += 1;
611
+ apiCalls += 1;
610
612
  rateLimit = { ...rateLimit, ...res.headers };
611
613
  if (!res.ok)
612
614
  return null;
@@ -625,7 +627,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
625
627
  const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(cs2SideA.id)}/vs/${encodeURIComponent(cs2SideB.id)}`, {
626
628
  query: { limit: 50 },
627
629
  });
628
- upstreamCalls += 1;
630
+ apiCalls += 1;
629
631
  rateLimit = { ...rateLimit, ...res.headers };
630
632
  if (res.ok) {
631
633
  rows = extractRows(res.data).map((row) => {
@@ -652,7 +654,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
652
654
  }
653
655
  if (!restH2hOk) {
654
656
  const res = await fetchJson(ctx, '/cs2/matches', { query: { team: sideA, limit: 50 } });
655
- upstreamCalls += 1;
657
+ apiCalls += 1;
656
658
  rateLimit = { ...rateLimit, ...res.headers };
657
659
  if (!res.ok) {
658
660
  return errorEnvelope({
@@ -662,7 +664,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
662
664
  source: 'head_to_head',
663
665
  requestId,
664
666
  tookMs: Date.now() - started,
665
- upstreamCalls,
667
+ apiCalls,
666
668
  httpStatus: res.status,
667
669
  rateLimit: res.headers,
668
670
  });
@@ -672,7 +674,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
672
674
  }
673
675
  else if (game === 'cod') {
674
676
  const res = await fetchJson(ctx, '/cod/matches', { query: { team: sideA, limit: 50 } });
675
- upstreamCalls += 1;
677
+ apiCalls += 1;
676
678
  rateLimit = res.headers;
677
679
  if (!res.ok) {
678
680
  return errorEnvelope({
@@ -682,7 +684,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
682
684
  source: 'head_to_head',
683
685
  requestId,
684
686
  tookMs: Date.now() - started,
685
- upstreamCalls,
687
+ apiCalls,
686
688
  httpStatus: res.status,
687
689
  rateLimit: res.headers,
688
690
  });
@@ -691,7 +693,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
691
693
  }
692
694
  else if (game === 'lol') {
693
695
  const h2hRes = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(sideA)}/h2h/${encodeURIComponent(sideB)}`, { query: { limit: String(limit) } });
694
- upstreamCalls += 1;
696
+ apiCalls += 1;
695
697
  rateLimit = { ...rateLimit, ...h2hRes.headers };
696
698
  if (h2hRes.ok) {
697
699
  const env = asRecord(h2hRes.data) ?? {};
@@ -725,7 +727,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
725
727
  game,
726
728
  requestId,
727
729
  tookMs: Date.now() - started,
728
- upstreamCalls,
730
+ apiCalls,
729
731
  rateLimit,
730
732
  data: {
731
733
  sideA: { idOrSlug: sideA },
@@ -747,7 +749,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
747
749
  const res = await fetchJson(ctx, '/lol/schedule', {
748
750
  query: { teamSlug: sideA, completed: 'true', limit: '50' },
749
751
  });
750
- upstreamCalls += 1;
752
+ apiCalls += 1;
751
753
  rateLimit = res.headers;
752
754
  if (!res.ok) {
753
755
  return errorEnvelope({
@@ -757,7 +759,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
757
759
  source: 'head_to_head',
758
760
  requestId,
759
761
  tookMs: Date.now() - started,
760
- upstreamCalls,
762
+ apiCalls,
761
763
  httpStatus: res.status,
762
764
  rateLimit: res.headers,
763
765
  });
@@ -767,7 +769,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
767
769
  else if (game === 'dota2') {
768
770
  warnings.push('Dota H2H uses recent matches with weak filters; record may be incomplete');
769
771
  const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: 50 } });
770
- upstreamCalls += 1;
772
+ apiCalls += 1;
771
773
  rateLimit = res.headers;
772
774
  if (!res.ok) {
773
775
  return errorEnvelope({
@@ -777,7 +779,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
777
779
  source: 'head_to_head',
778
780
  requestId,
779
781
  tookMs: Date.now() - started,
780
- upstreamCalls,
782
+ apiCalls,
781
783
  httpStatus: res.status,
782
784
  rateLimit: res.headers,
783
785
  });
@@ -791,7 +793,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
791
793
  fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideA)}/fights`, { query: { limit: 50 } }),
792
794
  fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideB)}/fights`, { query: { limit: 50 } }),
793
795
  ]);
794
- upstreamCalls += 4;
796
+ apiCalls += 4;
795
797
  rateLimit = { ...aRes.headers, ...bRes.headers, ...histA.headers, ...histB.headers };
796
798
  if (!aRes.ok || !bRes.ok) {
797
799
  return errorEnvelope({
@@ -801,7 +803,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
801
803
  source: 'head_to_head',
802
804
  requestId,
803
805
  tookMs: Date.now() - started,
804
- upstreamCalls,
806
+ apiCalls,
805
807
  recover: [
806
808
  'resolve_entity { game: "ufc", type: "fighter", q } for each name',
807
809
  'Retry head_to_head with returned slugs as sideA/sideB',
@@ -831,7 +833,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
831
833
  if (/^(atp|wta)_\d+$/i.test(side))
832
834
  return { id: side, name: side };
833
835
  const res = await fetchJson(ctx, '/tennis/players/search', { query: { q: side, limit: 3 } });
834
- upstreamCalls += 1;
836
+ apiCalls += 1;
835
837
  rateLimit = { ...rateLimit, ...res.headers };
836
838
  if (!res.ok)
837
839
  return null;
@@ -850,7 +852,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
850
852
  source: 'head_to_head',
851
853
  requestId,
852
854
  tookMs: Date.now() - started,
853
- upstreamCalls,
855
+ apiCalls,
854
856
  rateLimit,
855
857
  recover: [
856
858
  'resolve_entity { game: "tennis", type: "player", q } for each name',
@@ -862,7 +864,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
862
864
  if (args.limit !== undefined && args.limit !== null)
863
865
  tennisQuery.limit = limit;
864
866
  const res = await fetchJson(ctx, '/tennis/h2h', { query: tennisQuery });
865
- upstreamCalls += 1;
867
+ apiCalls += 1;
866
868
  rateLimit = { ...rateLimit, ...res.headers };
867
869
  if (!res.ok) {
868
870
  return errorEnvelope({
@@ -872,7 +874,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
872
874
  source: 'head_to_head',
873
875
  requestId,
874
876
  tookMs: Date.now() - started,
875
- upstreamCalls,
877
+ apiCalls,
876
878
  httpStatus: res.status,
877
879
  rateLimit,
878
880
  });
@@ -888,7 +890,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
888
890
  game,
889
891
  requestId,
890
892
  tookMs: Date.now() - started,
891
- upstreamCalls,
893
+ apiCalls,
892
894
  rateLimit,
893
895
  data: {
894
896
  sideA: { idOrSlug: tA.id, name: sideAName },
@@ -950,7 +952,7 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
950
952
  game,
951
953
  requestId,
952
954
  tookMs: Date.now() - started,
953
- upstreamCalls,
955
+ apiCalls,
954
956
  rateLimit,
955
957
  warnings: warnings.length ? warnings : undefined,
956
958
  data: {
@@ -970,19 +972,19 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
970
972
  };
971
973
  export const h2hMatrix = {
972
974
  name: 'h2h_matrix',
973
- description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
974
-
975
- When to use:
976
- - Draw/field analysis: how each contender fares against every other (e.g. Alcaraz vs Zverev vs Sinner round-robin records)
977
- - Group-stage or semifinal-field comparisons
978
-
979
- Prefer over: N head_to_head calls for an N-player field; raw matrix via call_api.
980
-
981
- Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits → head_to_head; season stat leaders → leaderboard_*; rankings → standings.
982
-
983
- 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.
984
-
985
- Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
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
+ Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
986
988
  inputSchema: {
987
989
  type: 'object',
988
990
  additionalProperties: false,
@@ -1042,13 +1044,13 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
1042
1044
  recover: ['Pass 2-16 player ids (atp_207989) or names ("Carlos Alcaraz")'],
1043
1045
  });
1044
1046
  }
1045
- let upstreamCalls = 0;
1047
+ let apiCalls = 0;
1046
1048
  let rateLimit = {};
1047
1049
  const resolveTennisSide = async (side) => {
1048
1050
  if (/^(atp|wta)_\d+$/i.test(side))
1049
1051
  return { id: side, name: side };
1050
1052
  const res = await fetchJson(ctx, '/tennis/players/search', { query: { q: side, limit: 3 } });
1051
- upstreamCalls += 1;
1053
+ apiCalls += 1;
1052
1054
  rateLimit = { ...rateLimit, ...res.headers };
1053
1055
  if (!res.ok)
1054
1056
  return null;
@@ -1068,7 +1070,7 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
1068
1070
  source: 'h2h_matrix',
1069
1071
  requestId,
1070
1072
  tookMs: Date.now() - started,
1071
- upstreamCalls,
1073
+ apiCalls,
1072
1074
  rateLimit,
1073
1075
  recover: [
1074
1076
  'resolve_entity { game: "tennis", type: "player", q } for each missing name',
@@ -1085,14 +1087,14 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
1085
1087
  source: 'h2h_matrix',
1086
1088
  requestId,
1087
1089
  tookMs: Date.now() - started,
1088
- upstreamCalls,
1090
+ apiCalls,
1089
1091
  rateLimit,
1090
1092
  });
1091
1093
  }
1092
1094
  const res = await fetchJson(ctx, '/tennis/h2h/matrix', {
1093
1095
  query: { player_ids: ids.join(',') },
1094
1096
  });
1095
- upstreamCalls += 1;
1097
+ apiCalls += 1;
1096
1098
  rateLimit = { ...rateLimit, ...res.headers };
1097
1099
  if (!res.ok) {
1098
1100
  return errorEnvelope({
@@ -1102,7 +1104,7 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
1102
1104
  source: 'h2h_matrix',
1103
1105
  requestId,
1104
1106
  tookMs: Date.now() - started,
1105
- upstreamCalls,
1107
+ apiCalls,
1106
1108
  httpStatus: res.status,
1107
1109
  rateLimit,
1108
1110
  });
@@ -1123,7 +1125,7 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
1123
1125
  game,
1124
1126
  requestId,
1125
1127
  tookMs: Date.now() - started,
1126
- upstreamCalls,
1128
+ apiCalls,
1127
1129
  rateLimit,
1128
1130
  data: {
1129
1131
  players,
@@ -54,10 +54,10 @@ Tennis-only. Levels accept names (Grand Slam, WTA 1000, ATP 500, Challenger,
54
54
  ITF World Tennis Tour, Davis Cup, ...) or raw tier codes (G, M, A, C, D).
55
55
  A level name that starts with ATP or WTA also fixes the tour (and conflicts with
56
56
  a contradicting tour= argument are rejected), so "WTA 1000" cannot return ATP
57
- Masters events even though the upstream filter is not tour-aware.
58
- Note the source does not separate ATP 500 from ATP 250 — both are ATP Tour.
57
+ Masters events.
58
+ The category field reports ATP 500 and ATP 250 both as atp_tour (read tier to tell them apart), and WTA 1000 events as wta_1000.
59
59
 
60
- Parallel-safe: yes. Upstream cost: 1.`,
60
+ Parallel-safe: yes. API calls: 1.`,
61
61
  inputSchema: {
62
62
  type: 'object',
63
63
  additionalProperties: false,
@@ -202,7 +202,7 @@ Parallel-safe: yes. Upstream cost: 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
  });
@@ -232,10 +232,8 @@ Parallel-safe: yes. Upstream cost: 1.`,
232
232
  .slice(0, 5)
233
233
  .map((t) => `${t.id} (${t.tour})`)
234
234
  .join(', ');
235
- warnings.push(`The upstream level filter returned ${contradictions.length} tournament(s) that contradict level="${levelRaw}": ` +
236
- `${sample}. They were dropped. The API's level reverse-map is not tour-aware (WTA 1000 resolves to tier codes ` +
237
- `PM and M), which is a server-side defect; this tool sends tour=${levelTourPrefix} as well, so a repeat of this ` +
238
- `warning means the upstream filter is widening again.`);
235
+ warnings.push(`The level filter returned ${contradictions.length} tournament(s) that contradict level="${levelRaw}": ` +
236
+ `${sample}. They were dropped; this tool also sends tour=${levelTourPrefix}.`);
239
237
  }
240
238
  return successEnvelope({
241
239
  pagination: {
@@ -250,7 +248,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
250
248
  game,
251
249
  requestId,
252
250
  tookMs: Date.now() - started,
253
- upstreamCalls: 1,
251
+ apiCalls: 1,
254
252
  rateLimit: res.headers,
255
253
  warnings: warnings.length ? warnings : undefined,
256
254
  data: {
@@ -156,7 +156,7 @@ export async function runTool(def, args, ctx) {
156
156
  catch (error) {
157
157
  const { errorEnvelope, toMcpResult: toResult } = await import('../envelope.js');
158
158
  return toResult(errorEnvelope({
159
- code: 'UPSTREAM',
159
+ code: 'SERVICE_ERROR',
160
160
  message: error.message || 'Unhandled tool error',
161
161
  game: null,
162
162
  source: def.name,