@claudinho/mcp 0.8.18 → 0.8.19

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.
Files changed (2) hide show
  1. package/dist/index.js +82 -21
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -504,6 +504,33 @@ function shiftUtcDate(dateISO, days) {
504
504
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
505
505
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
506
506
  }
507
+ var FEED_TEXT_MAX = 100;
508
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
509
+ let out = "";
510
+ let count = 0;
511
+ for (const ch of String(value)) {
512
+ const cp = ch.codePointAt(0) ?? 0;
513
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
514
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
515
+ if (count >= max) break;
516
+ out += isWhitespaceControl ? " " : ch;
517
+ count++;
518
+ }
519
+ return out;
520
+ }
521
+ var segmenter = new Intl.Segmenter();
522
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
523
+ function displayWidth(s) {
524
+ let w = 0;
525
+ for (const { segment } of segmenter.segment(s)) {
526
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
527
+ }
528
+ return w;
529
+ }
530
+ function padVisible(s, width) {
531
+ const w = displayWidth(s);
532
+ return w >= width ? s : s + " ".repeat(width - w);
533
+ }
507
534
  function outcomeFromScore(home, away) {
508
535
  if (home > away) return "H";
509
536
  if (home < away) return "A";
@@ -2881,6 +2908,7 @@ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2881
2908
  var DEFAULT_COMPETITION = "fifa.world";
2882
2909
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2883
2910
  var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
2911
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2884
2912
  function competitionBase(slug) {
2885
2913
  return `${ESPN_SOCCER}/${slug}`;
2886
2914
  }
@@ -2924,9 +2952,15 @@ function toInt(s) {
2924
2952
  return Number.isFinite(n) ? n : void 0;
2925
2953
  }
2926
2954
  function toTeam(t2) {
2927
- const name = t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD";
2928
- const code = (t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2929
- return { code, name, flag: nationToFlag(t2?.displayName ?? t2?.abbreviation ?? name) };
2955
+ const name = sanitizeFeedText(
2956
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
2957
+ );
2958
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2959
+ return {
2960
+ code,
2961
+ name,
2962
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
2963
+ };
2930
2964
  }
2931
2965
  function mapEspnEvent(ev, ctx = {}) {
2932
2966
  const comp = ev.competitions?.[0];
@@ -2957,9 +2991,9 @@ function mapEspnEvent(ev, ctx = {}) {
2957
2991
  stage,
2958
2992
  group,
2959
2993
  kickoff: ev.date,
2960
- venue: comp?.venue?.fullName ?? "",
2961
- city: comp?.venue?.address?.city || void 0,
2962
- country: comp?.venue?.address?.country || void 0,
2994
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
2995
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
2996
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
2963
2997
  home,
2964
2998
  away,
2965
2999
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3083,11 +3117,18 @@ var EspnAdapter = class {
3083
3117
  try {
3084
3118
  const res = await doFetch(url, {
3085
3119
  signal: controller.signal,
3120
+ // ESPN never legitimately redirects; following one would sidestep the
3121
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3122
+ redirect: "error",
3086
3123
  headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3087
3124
  });
3088
3125
  if (!res.ok) {
3089
3126
  throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3090
3127
  }
3128
+ const length = Number(res.headers?.get?.("content-length"));
3129
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3130
+ throw new Error(`ESPN response too large: ${length} bytes`);
3131
+ }
3091
3132
  return await res.json();
3092
3133
  } finally {
3093
3134
  clearTimeout(timer);
@@ -4229,12 +4270,19 @@ var PolymarketProvider = class {
4229
4270
  const doFetch = this.opts.fetchImpl ?? fetch;
4230
4271
  const res = await doFetch(url, {
4231
4272
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4273
+ // Gamma never legitimately redirects; following one would sidestep the
4274
+ // host allow-list (it only validates the base URL), so reject redirects.
4275
+ redirect: "error",
4232
4276
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4233
4277
  });
4234
4278
  if (res.status === 404) return void 0;
4235
4279
  if (!res.ok) {
4236
4280
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4237
4281
  }
4282
+ const length = Number(res.headers?.get?.("content-length"));
4283
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4284
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4285
+ }
4238
4286
  const data = await res.json();
4239
4287
  const event = Array.isArray(data) ? data[0] : data;
4240
4288
  return event && typeof event === "object" ? event : void 0;
@@ -4677,17 +4725,21 @@ function matchList(matches, empty, opts = {}) {
4677
4725
  }
4678
4726
  function standingsTable(group, rows) {
4679
4727
  const header = `Group ${group}`;
4680
- const cols = "Team P W D L GD Pts";
4681
- const lines = rows.map((r) => {
4682
- const name = `${r.team.flag} ${r.team.name}`.padEnd(24).slice(0, 24);
4683
- const gd2 = (r.goalDiff > 0 ? `+${r.goalDiff}` : `${r.goalDiff}`).padStart(3);
4684
- return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${gd2} ${pad(r.points)}`;
4685
- });
4728
+ const line = (team, p, w, d, l, gd2, pts) => `${padVisible(team, 24)} ${p.padStart(2)} ${w.padStart(2)} ${d.padStart(2)} ${l.padStart(2)} ${gd2.padStart(3)} ${pts.padStart(3)}`;
4729
+ const cols = line("Team", "P", "W", "D", "L", "GD", "Pts");
4730
+ const lines = rows.map(
4731
+ (r) => line(
4732
+ `${r.team.flag} ${r.team.name}`,
4733
+ String(r.played),
4734
+ String(r.won),
4735
+ String(r.drawn),
4736
+ String(r.lost),
4737
+ r.goalDiff > 0 ? `+${r.goalDiff}` : String(r.goalDiff),
4738
+ String(r.points)
4739
+ )
4740
+ );
4686
4741
  return [header, cols, ...lines].join("\n");
4687
4742
  }
4688
- function pad(n) {
4689
- return `${n}`.padStart(2);
4690
- }
4691
4743
  var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated with or endorsed by FIFA or Anthropic.";
4692
4744
 
4693
4745
  // src/tools.ts
@@ -4928,14 +4980,16 @@ async function toolGetNextFixture(args) {
4928
4980
  const msg = degraded ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`;
4929
4981
  return {
4930
4982
  text: withDisclaimer(msg, void 0, args.lang),
4931
- data: { team: code, fixture: null, degraded }
4983
+ data: { team: code, fixture: null, degraded, source: source ?? null }
4932
4984
  };
4933
4985
  }
4934
4986
  const opts = fmtOpts(args);
4935
4987
  return {
4988
+ // `source` in data mirrors the text's "Live data: …" attribution (parity
4989
+ // with CLI `next --json`); null for a static group fixture (no live source).
4936
4990
  text: withDisclaimer(`Next up for ${code}:
4937
4991
  ${matchLine(fixture, opts)}`, source, args.lang),
4938
- data: { team: code, fixture, degraded }
4992
+ data: { team: code, fixture, degraded, source: source ?? null }
4939
4993
  };
4940
4994
  }
4941
4995
  function toolGetTeam(args) {
@@ -5219,7 +5273,7 @@ async function toolGetShareSnippet(args) {
5219
5273
 
5220
5274
  // src/server.ts
5221
5275
  var SERVER_NAME = "claudinho";
5222
- var SERVER_VERSION = "0.8.18";
5276
+ var SERVER_VERSION = "0.8.19";
5223
5277
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5224
5278
  Voice: when relaying scores, narrate with lively, regionally-appropriate football-commentary energy in the user's language. Each match line may end with a short exclamation ("\u2014 \xA1GOOOOL!") \u2014 use it as a tone cue. Keep every fact exact; never invent details and never impersonate or name a real commentator.`;
5225
5279
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -5234,7 +5288,9 @@ var teamArg = z.string().regex(/^[A-Za-z]{3}$/, "a 3-letter team code, e.g. MEX"
5234
5288
  var flavorArg = z.enum(["off", "subtle", "full"]);
5235
5289
  var commonArgs = {
5236
5290
  tz: z.string().optional().describe("IANA timezone for kickoff times, e.g. America/Mexico_City"),
5237
- lang: z.string().optional().describe("Locale for formatting: en, es, pt, fr (other locales fall back to en)"),
5291
+ lang: z.string().optional().describe(
5292
+ "Locale for dates, provider attribution, and commentary: en, es, pt, fr (the summary scaffold stays English; other locales fall back to en)"
5293
+ ),
5238
5294
  flavor: flavorArg.optional().describe("Commentary flair: off, subtle, full (default: full)")
5239
5295
  };
5240
5296
  var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string() }).partial().passthrough();
@@ -5281,7 +5337,12 @@ var bracketOut = {
5281
5337
  standingsDegraded: z.boolean().optional(),
5282
5338
  source: src.optional()
5283
5339
  };
5284
- var nextOut = { team: z.string(), fixture: matchOut.nullable(), degraded: z.boolean() };
5340
+ var nextOut = {
5341
+ team: z.string(),
5342
+ fixture: matchOut.nullable(),
5343
+ degraded: z.boolean(),
5344
+ source: src
5345
+ };
5285
5346
  var marketOut = {
5286
5347
  matchId: z.string().nullable().optional(),
5287
5348
  team: z.string().optional(),
@@ -5333,7 +5394,7 @@ function buildServer() {
5333
5394
  "get_today",
5334
5395
  {
5335
5396
  title: "Today's matches",
5336
- description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes text (en/es/pt/fr); flavor sets commentary tone.",
5397
+ description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes dates, attribution, and commentary (en/es/pt/fr); flavor sets commentary tone.",
5337
5398
  inputSchema: {
5338
5399
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
5339
5400
  ...commonArgs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
4
4
  "mcpName": "io.github.arturogarrido/claudinho",
5
5
  "description": "MCP server for the 2026 men's football tournament — live scores, fixtures, standings, read-only prediction-market signals, and paste-ready match cards. Works with Claude Code, Cursor, Codex, Windsurf, Zed. Not affiliated with FIFA or Anthropic.",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "dist",
29
+ "!dist/*.mcpb",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
@@ -57,7 +58,7 @@
57
58
  "tsup": "^8.0.0",
58
59
  "typescript": "^5.7.0",
59
60
  "vitest": "^4.1.9",
60
- "@claudinho/core": "0.8.18"
61
+ "@claudinho/core": "0.8.19"
61
62
  },
62
63
  "scripts": {
63
64
  "build": "tsup",