@claudinho/mcp 0.8.18 → 0.9.0

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 +214 -50
  2. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -385,7 +385,8 @@ function normalizeLang(lang) {
385
385
  }
386
386
  function t(lang, key, vars) {
387
387
  const dict = CATALOGS[normalizeLang(lang)];
388
- let s = dict[key] ?? EN[key] ?? key;
388
+ const en = EN;
389
+ let s = dict[key] ?? en[key] ?? key;
389
390
  if (vars) {
390
391
  for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
391
392
  }
@@ -504,6 +505,33 @@ function shiftUtcDate(dateISO, days) {
504
505
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
505
506
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
506
507
  }
508
+ var FEED_TEXT_MAX = 100;
509
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
510
+ let out = "";
511
+ let count = 0;
512
+ for (const ch of String(value)) {
513
+ const cp = ch.codePointAt(0) ?? 0;
514
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
515
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
516
+ if (count >= max) break;
517
+ out += isWhitespaceControl ? " " : ch;
518
+ count++;
519
+ }
520
+ return out;
521
+ }
522
+ var segmenter = new Intl.Segmenter();
523
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
524
+ function displayWidth(s) {
525
+ let w = 0;
526
+ for (const { segment } of segmenter.segment(s)) {
527
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
528
+ }
529
+ return w;
530
+ }
531
+ function padVisible(s, width) {
532
+ const w = displayWidth(s);
533
+ return w >= width ? s : s + " ".repeat(width - w);
534
+ }
507
535
  function outcomeFromScore(home, away) {
508
536
  if (home > away) return "H";
509
537
  if (home < away) return "A";
@@ -2880,10 +2908,27 @@ function rosterAtZero(matches) {
2880
2908
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2881
2909
  var DEFAULT_COMPETITION = "fifa.world";
2882
2910
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2883
- var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
2911
+ var USER_AGENT = `claudinho/${"0.9.0"} (+https://github.com/arturogarrido/claudinho)`;
2912
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2884
2913
  function competitionBase(slug) {
2885
2914
  return `${ESPN_SOCCER}/${slug}`;
2886
2915
  }
2916
+ var DEFAULT_TIMEOUT_MS = 6e3;
2917
+ var STANDINGS_SHARE_MS = 3e4;
2918
+ var ProviderError = class extends Error {
2919
+ kind;
2920
+ status;
2921
+ constructor(message, kind, status) {
2922
+ super(message);
2923
+ this.name = "ProviderError";
2924
+ this.kind = kind;
2925
+ this.status = status;
2926
+ }
2927
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
2928
+ get throttled() {
2929
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
2930
+ }
2931
+ };
2887
2932
  function mapStatus(st) {
2888
2933
  const name = (st?.type?.name ?? "").toUpperCase();
2889
2934
  const state = st?.type?.state ?? "";
@@ -2924,9 +2969,15 @@ function toInt(s) {
2924
2969
  return Number.isFinite(n) ? n : void 0;
2925
2970
  }
2926
2971
  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) };
2972
+ const name = sanitizeFeedText(
2973
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
2974
+ );
2975
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2976
+ return {
2977
+ code,
2978
+ name,
2979
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
2980
+ };
2930
2981
  }
2931
2982
  function mapEspnEvent(ev, ctx = {}) {
2932
2983
  const comp = ev.competitions?.[0];
@@ -2957,9 +3008,9 @@ function mapEspnEvent(ev, ctx = {}) {
2957
3008
  stage,
2958
3009
  group,
2959
3010
  kickoff: ev.date,
2960
- venue: comp?.venue?.fullName ?? "",
2961
- city: comp?.venue?.address?.city || void 0,
2962
- country: comp?.venue?.address?.country || void 0,
3011
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3012
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3013
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
2963
3014
  home,
2964
3015
  away,
2965
3016
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3019,6 +3070,20 @@ var EspnAdapter = class {
3019
3070
  capabilities = { push: false, latencyHintSec: 45 };
3020
3071
  /** Cached team-code -> group-letter map (built lazily from standings). */
3021
3072
  groupMap;
3073
+ /**
3074
+ * One in-flight/recent standings fetch shared by fetchStandings and
3075
+ * fetchGroupMap, so a command like `bracket` hits the endpoint once instead
3076
+ * of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
3077
+ * fetch clears the slot — a transient failure is never cached.
3078
+ */
3079
+ standingsShared;
3080
+ /**
3081
+ * The most recent request failure (best-effort under concurrency; cleared
3082
+ * when a new request starts). A post-hoc hint for callers whose result path
3083
+ * fails closed to `degraded` booleans but who still need to distinguish a
3084
+ * throttle (persist a backoff) from an ordinary blip.
3085
+ */
3086
+ lastError;
3022
3087
  async fetchByDate(dateISO) {
3023
3088
  return this.fetchScoreboard(toEspnDate(dateISO));
3024
3089
  }
@@ -3040,37 +3105,58 @@ var EspnAdapter = class {
3040
3105
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3041
3106
  return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
3042
3107
  }
3108
+ /** The shared standings fetch (see {@link standingsShared}). */
3109
+ sharedStandings() {
3110
+ const now = Date.now();
3111
+ if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3112
+ return this.standingsShared.promise;
3113
+ }
3114
+ const promise = this.get(this.standingsUrl()).then(
3115
+ (d) => parseStandings(d)
3116
+ );
3117
+ this.standingsShared = { at: now, promise };
3118
+ promise.catch(() => {
3119
+ if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3120
+ });
3121
+ return promise;
3122
+ }
3043
3123
  /**
3044
3124
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3045
3125
  * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3046
3126
  * non-group `children` are filtered out by {@link parseStandings}.
3047
3127
  */
3048
3128
  async fetchStandings() {
3049
- return parseStandings(await this.get(this.standingsUrl()));
3129
+ return this.sharedStandings();
3050
3130
  }
3051
3131
  /**
3052
3132
  * Build (and cache) a team-code -> group-letter map from the standings
3053
- * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
3054
- * same parse as {@link fetchStandings}, so the two never drift.
3133
+ * endpoint. Best-effort: returns {} if standings are unavailable but a
3134
+ * transient failure is NOT cached (only a successful parse pins the map), so
3135
+ * one blip can't silently drop group letters for the adapter's lifetime.
3136
+ * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3137
+ * drift and one command never fetches standings twice.
3055
3138
  */
3056
3139
  async fetchGroupMap(force = false) {
3057
3140
  if (this.groupMap && !force) return this.groupMap;
3058
- const map = {};
3059
3141
  try {
3060
- const tables = parseStandings(await this.get(this.standingsUrl()));
3142
+ const tables = await this.sharedStandings();
3143
+ const map = {};
3061
3144
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3145
+ this.groupMap = map;
3146
+ return map;
3062
3147
  } catch {
3148
+ return {};
3063
3149
  }
3064
- this.groupMap = map;
3065
- return map;
3066
3150
  }
3067
3151
  async fetchScoreboard(dates) {
3068
3152
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3069
3153
  const url = new URL(`${base}/scoreboard`);
3070
3154
  url.searchParams.set("limit", "300");
3071
3155
  if (dates) url.searchParams.set("dates", dates);
3072
- const groupByTeam = this.opts.enrichGroups === false ? {} : await this.fetchGroupMap();
3073
- const data = await this.get(url.toString());
3156
+ const [groupByTeam, data] = await Promise.all([
3157
+ this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3158
+ this.get(url.toString())
3159
+ ]);
3074
3160
  return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3075
3161
  }
3076
3162
  async get(url) {
@@ -3078,17 +3164,51 @@ var EspnAdapter = class {
3078
3164
  const controller = new AbortController();
3079
3165
  const timer = setTimeout(
3080
3166
  () => controller.abort(),
3081
- this.opts.timeoutMs ?? 15e3
3167
+ this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
3082
3168
  );
3169
+ this.lastError = void 0;
3083
3170
  try {
3084
- const res = await doFetch(url, {
3085
- signal: controller.signal,
3086
- headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3087
- });
3171
+ let res;
3172
+ try {
3173
+ res = await doFetch(url, {
3174
+ signal: controller.signal,
3175
+ // ESPN never legitimately redirects; following one would sidestep the
3176
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3177
+ redirect: "error",
3178
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3179
+ });
3180
+ } catch (e) {
3181
+ throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3182
+ }
3088
3183
  if (!res.ok) {
3089
- throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3184
+ throw new ProviderError(
3185
+ `ESPN request failed: ${res.status} ${res.statusText}`,
3186
+ "http",
3187
+ res.status
3188
+ );
3189
+ }
3190
+ const length = Number(res.headers?.get?.("content-length"));
3191
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3192
+ throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3090
3193
  }
3091
- return await res.json();
3194
+ try {
3195
+ return await res.json();
3196
+ } catch (e) {
3197
+ throw new ProviderError(
3198
+ `ESPN response unparseable: ${e?.message ?? e}`,
3199
+ "parse"
3200
+ );
3201
+ }
3202
+ } catch (e) {
3203
+ const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
3204
+ this.lastError = pe;
3205
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
3206
+ process.stderr.write(
3207
+ `claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
3208
+ `
3209
+ );
3210
+ }
3211
+ throw pe;
3092
3212
  } finally {
3093
3213
  clearTimeout(timer);
3094
3214
  }
@@ -3812,13 +3932,18 @@ function resolveCompetition(explicit) {
3812
3932
  }
3813
3933
  return DEFAULT_COMPETITION;
3814
3934
  }
3935
+ var KNOWN_SOURCES = ["espn"];
3815
3936
  function makeAdapter(source = "espn") {
3816
3937
  switch (source) {
3817
- default: {
3938
+ case "espn": {
3818
3939
  const competition = resolveCompetition();
3819
3940
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
3820
3941
  return new EspnAdapter({ baseUrl });
3821
3942
  }
3943
+ default:
3944
+ throw new Error(
3945
+ `Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
3946
+ );
3822
3947
  }
3823
3948
  }
3824
3949
  function mergeLive(base, live) {
@@ -3856,8 +3981,16 @@ async function getStandings(adapter, group) {
3856
3981
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
3857
3982
  return { tables, degraded: true };
3858
3983
  }
3859
- var KNOCKOUT_WINDOW_START = "20260628";
3860
- var KNOCKOUT_WINDOW_END = "20260719";
3984
+ var knockoutWindowMemo;
3985
+ function knockoutWindow() {
3986
+ if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
3987
+ const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
3988
+ knockoutWindowMemo = days.length ? {
3989
+ start: days.reduce((a, b) => a < b ? a : b),
3990
+ end: days.reduce((a, b) => a > b ? a : b)
3991
+ } : null;
3992
+ return knockoutWindowMemo;
3993
+ }
3861
3994
  async function getBracket(adapter, opts = {}) {
3862
3995
  const topology = loadBracketTopology();
3863
3996
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
@@ -3865,7 +3998,8 @@ async function getBracket(adapter, opts = {}) {
3865
3998
  let liveDegraded = true;
3866
3999
  let source;
3867
4000
  try {
3868
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4001
+ const win = knockoutWindow();
4002
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
3869
4003
  matches = mergeLive(base, live);
3870
4004
  liveDegraded = false;
3871
4005
  source = adapter.name;
@@ -3898,8 +4032,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
3898
4032
  let fixtures = allFixtures();
3899
4033
  let overlayFailed = false;
3900
4034
  try {
3901
- if (adapter.fetchWindow) {
3902
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END));
4035
+ const win = knockoutWindow();
4036
+ if (adapter.fetchWindow && win) {
4037
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
3903
4038
  }
3904
4039
  } catch {
3905
4040
  overlayFailed = true;
@@ -3922,7 +4057,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
3922
4057
  let degraded = true;
3923
4058
  let liveById;
3924
4059
  try {
3925
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4060
+ const win = knockoutWindow();
4061
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
3926
4062
  matches = mergeLive(base, live);
3927
4063
  degraded = false;
3928
4064
  liveById = new Set(live.map((m) => m.id));
@@ -4170,7 +4306,7 @@ var mapping_2026_default = {
4170
4306
  var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4171
4307
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4172
4308
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4173
- var DEFAULT_TIMEOUT_MS = 8e3;
4309
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
4174
4310
  var WC_SERIES_SLUG = "soccer-fifwc";
4175
4311
  var WC_SPORT = "fifwc";
4176
4312
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4208,7 +4344,7 @@ var PolymarketProvider = class {
4208
4344
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4209
4345
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4210
4346
  if (slugs.length === 0) return { checked: true };
4211
- const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4347
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4212
4348
  try {
4213
4349
  for (const slug of slugs) {
4214
4350
  const remaining = deadline - Date.now();
@@ -4228,13 +4364,20 @@ var PolymarketProvider = class {
4228
4364
  const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4229
4365
  const doFetch = this.opts.fetchImpl ?? fetch;
4230
4366
  const res = await doFetch(url, {
4231
- signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4367
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
4368
+ // Gamma never legitimately redirects; following one would sidestep the
4369
+ // host allow-list (it only validates the base URL), so reject redirects.
4370
+ redirect: "error",
4232
4371
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4233
4372
  });
4234
4373
  if (res.status === 404) return void 0;
4235
4374
  if (!res.ok) {
4236
4375
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4237
4376
  }
4377
+ const length = Number(res.headers?.get?.("content-length"));
4378
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4379
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4380
+ }
4238
4381
  const data = await res.json();
4239
4382
  const event = Array.isArray(data) ? data[0] : data;
4240
4383
  return event && typeof event === "object" ? event : void 0;
@@ -4677,22 +4820,34 @@ function matchList(matches, empty, opts = {}) {
4677
4820
  }
4678
4821
  function standingsTable(group, rows) {
4679
4822
  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
- });
4823
+ 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)}`;
4824
+ const cols = line("Team", "P", "W", "D", "L", "GD", "Pts");
4825
+ const lines = rows.map(
4826
+ (r) => line(
4827
+ `${r.team.flag} ${r.team.name}`,
4828
+ String(r.played),
4829
+ String(r.won),
4830
+ String(r.drawn),
4831
+ String(r.lost),
4832
+ r.goalDiff > 0 ? `+${r.goalDiff}` : String(r.goalDiff),
4833
+ String(r.points)
4834
+ )
4835
+ );
4686
4836
  return [header, cols, ...lines].join("\n");
4687
4837
  }
4688
- function pad(n) {
4689
- return `${n}`.padStart(2);
4690
- }
4691
4838
  var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated with or endorsed by FIFA or Anthropic.";
4692
4839
 
4693
4840
  // src/tools.ts
4841
+ var adapters = /* @__PURE__ */ new Map();
4694
4842
  function resolveAdapter(args) {
4695
- return args.adapter ?? makeAdapter(args.source);
4843
+ if (args.adapter) return args.adapter;
4844
+ const key = `${args.source ?? "espn"}::${resolveCompetition()}`;
4845
+ let adapter = adapters.get(key);
4846
+ if (!adapter) {
4847
+ adapter = makeAdapter(args.source);
4848
+ adapters.set(key, adapter);
4849
+ }
4850
+ return adapter;
4696
4851
  }
4697
4852
  function resolveMarketProvider(args) {
4698
4853
  return args.marketProvider ?? makeMarketProvider();
@@ -4928,14 +5083,16 @@ async function toolGetNextFixture(args) {
4928
5083
  const msg = degraded ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`;
4929
5084
  return {
4930
5085
  text: withDisclaimer(msg, void 0, args.lang),
4931
- data: { team: code, fixture: null, degraded }
5086
+ data: { team: code, fixture: null, degraded, source: source ?? null }
4932
5087
  };
4933
5088
  }
4934
5089
  const opts = fmtOpts(args);
4935
5090
  return {
5091
+ // `source` in data mirrors the text's "Live data: …" attribution (parity
5092
+ // with CLI `next --json`); null for a static group fixture (no live source).
4936
5093
  text: withDisclaimer(`Next up for ${code}:
4937
5094
  ${matchLine(fixture, opts)}`, source, args.lang),
4938
- data: { team: code, fixture, degraded }
5095
+ data: { team: code, fixture, degraded, source: source ?? null }
4939
5096
  };
4940
5097
  }
4941
5098
  function toolGetTeam(args) {
@@ -5219,7 +5376,7 @@ async function toolGetShareSnippet(args) {
5219
5376
 
5220
5377
  // src/server.ts
5221
5378
  var SERVER_NAME = "claudinho";
5222
- var SERVER_VERSION = "0.8.18";
5379
+ var SERVER_VERSION = "0.9.0";
5223
5380
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5224
5381
  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
5382
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -5234,7 +5391,9 @@ var teamArg = z.string().regex(/^[A-Za-z]{3}$/, "a 3-letter team code, e.g. MEX"
5234
5391
  var flavorArg = z.enum(["off", "subtle", "full"]);
5235
5392
  var commonArgs = {
5236
5393
  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)"),
5394
+ lang: z.string().optional().describe(
5395
+ "Locale for dates, provider attribution, and commentary: en, es, pt, fr (the summary scaffold stays English; other locales fall back to en)"
5396
+ ),
5238
5397
  flavor: flavorArg.optional().describe("Commentary flair: off, subtle, full (default: full)")
5239
5398
  };
5240
5399
  var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string() }).partial().passthrough();
@@ -5281,7 +5440,12 @@ var bracketOut = {
5281
5440
  standingsDegraded: z.boolean().optional(),
5282
5441
  source: src.optional()
5283
5442
  };
5284
- var nextOut = { team: z.string(), fixture: matchOut.nullable(), degraded: z.boolean() };
5443
+ var nextOut = {
5444
+ team: z.string(),
5445
+ fixture: matchOut.nullable(),
5446
+ degraded: z.boolean(),
5447
+ source: src
5448
+ };
5285
5449
  var marketOut = {
5286
5450
  matchId: z.string().nullable().optional(),
5287
5451
  team: z.string().optional(),
@@ -5333,7 +5497,7 @@ function buildServer() {
5333
5497
  "get_today",
5334
5498
  {
5335
5499
  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.",
5500
+ 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
5501
  inputSchema: {
5338
5502
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
5339
5503
  ...commonArgs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.8.18",
3
+ "version": "0.9.0",
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
  ],
@@ -54,10 +55,11 @@
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/node": "^22.20.0",
58
+ "@vitest/coverage-v8": "^4.1.9",
57
59
  "tsup": "^8.0.0",
58
60
  "typescript": "^5.7.0",
59
61
  "vitest": "^4.1.9",
60
- "@claudinho/core": "0.8.18"
62
+ "@claudinho/core": "0.9.0"
61
63
  },
62
64
  "scripts": {
63
65
  "build": "tsup",
@@ -65,6 +67,7 @@
65
67
  "dev": "tsup --watch",
66
68
  "test": "vitest run",
67
69
  "typecheck": "tsc --noEmit",
68
- "start": "node dist/index.js"
70
+ "start": "node dist/index.js",
71
+ "smoke:stdio": "node scripts/stdio-smoke.mjs"
69
72
  }
70
73
  }