@claudinho/mcp 0.4.2 → 0.4.3

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 (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +79 -51
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -37,7 +37,7 @@ Developer → Edit Config, then restart. Codex config file: `~/.codex/config.tom
37
37
  | `get_match` | a single match by id |
38
38
  | `get_standings` | group table(s) — one group `A`–`L`, or all |
39
39
  | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) — fully offline |
40
- | `get_market_signal` | read-only prediction-market signal for a match, a team's next fixture, or a date — informational only |
40
+ | `get_market_signal` | read-only prediction-market signal for a match, a team's current-or-next fixture (in-play preferred while live), or a date — informational only |
41
41
  | `get_share_snippet` | a copy-pasteable plain-text match card — hand the returned snippet to the user as-is |
42
42
 
43
43
  All tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
@@ -50,6 +50,7 @@ the prediction-market read).
50
50
 
51
51
  ## Market signals
52
52
 
53
+ Market signals are pre-match and in-play reads — finished matches never show one.
53
54
  `get_today` / `get_match` include a short market line when a reliable market exists
54
55
  (slugs are auto-derived per fixture; matching fails closed). **Read-only and
55
56
  informational only — not betting advice:** market-implied percentages with Polymarket
package/dist/index.js CHANGED
@@ -2614,6 +2614,7 @@ function nextFixtureForTeam(code, opts = {}) {
2614
2614
  (m) => new Date(m.kickoff).getTime() >= from.getTime()
2615
2615
  );
2616
2616
  }
2617
+ var LIVE_WINDOW_MS = 140 * 6e4;
2617
2618
  function groups(fixtures = SCHEDULE) {
2618
2619
  const set = /* @__PURE__ */ new Set();
2619
2620
  for (const m of fixtures) if (m.group) set.add(m.group);
@@ -2871,6 +2872,33 @@ function shiftUtcDate(dateISO, days) {
2871
2872
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
2872
2873
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
2873
2874
  }
2875
+ var EXTRA_TIME_SLACK_MS = 60 * 6e4;
2876
+ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
2877
+ const nowMs = now.getTime();
2878
+ const candidate = fixturesByTeam(code).find((m) => {
2879
+ const k = Date.parse(m.kickoff);
2880
+ return nowMs >= k && nowMs <= k + LIVE_WINDOW_MS + EXTRA_TIME_SLACK_MS;
2881
+ });
2882
+ if (candidate) {
2883
+ const r = await getMatchById(adapter, candidate.id);
2884
+ const m = r.match ?? candidate;
2885
+ if (!isFinished(m.status)) return { ...r, match: m };
2886
+ }
2887
+ const next = nextFixtureForTeam(code, { from: now });
2888
+ return { match: next, degraded: false };
2889
+ }
2890
+ async function getMatchById(adapter, id) {
2891
+ const base = allFixtures().find((m) => m.id === id);
2892
+ if (!base) return { match: void 0, degraded: false };
2893
+ const day = base.kickoff.slice(0, 10);
2894
+ try {
2895
+ const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
2896
+ const hit = live.find((m) => m.id === id);
2897
+ return { match: hit ?? base, degraded: false, source: hit ? adapter.name : void 0 };
2898
+ } catch {
2899
+ return { match: base, degraded: true };
2900
+ }
2901
+ }
2874
2902
  async function getLiveMatches(adapter) {
2875
2903
  try {
2876
2904
  return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
@@ -2879,6 +2907,12 @@ async function getLiveMatches(adapter) {
2879
2907
  }
2880
2908
  }
2881
2909
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
2910
+ function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
2911
+ if (isLive(match.status)) return true;
2912
+ if (isFinished(match.status)) return false;
2913
+ const k = Date.parse(match.kickoff);
2914
+ return !Number.isFinite(k) || now.getTime() <= k + LIVE_WINDOW_MS;
2915
+ }
2882
2916
  function normalizeOutcomes(outcomes) {
2883
2917
  const sum = outcomes.reduce(
2884
2918
  (s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
@@ -3454,13 +3488,19 @@ function resolveMarketProvider(args) {
3454
3488
  function marketDisplayable(sig) {
3455
3489
  return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
3456
3490
  }
3457
- function marketHeader(m) {
3458
- return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
3491
+ function marketHeader(m, args) {
3492
+ const when = formatDate(m.kickoff, { tz: args.tz, locale: args.lang });
3493
+ return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} (${when})`;
3459
3494
  }
3460
- function marketText(m, sig) {
3461
- return `${marketHeader(m)}
3495
+ function marketText(m, sig, args) {
3496
+ return `${marketHeader(m, args)}
3462
3497
  ${marketBlock(sig, m).join("\n")}`;
3463
3498
  }
3499
+ function noSignalText(m, args, now) {
3500
+ if (marketRelevant(m, now)) return `No reliable market signal for ${marketHeader(m, args)}.`;
3501
+ const verb = isFinished(m.status) ? "has finished" : "appears to have finished";
3502
+ return `${marketHeader(m, args)} ${verb} \u2014 market signals are pre-match and in-play reads.`;
3503
+ }
3464
3504
  function marketData(sig) {
3465
3505
  return {
3466
3506
  matchId: sig.matchId,
@@ -3521,10 +3561,12 @@ async function cachedMarketSignals(args, matches) {
3521
3561
  }
3522
3562
  async function reliableMarketData(args, matches) {
3523
3563
  if (!marketsEnabled()) return void 0;
3524
- const signals = await cachedMarketSignals(args, matches);
3525
- const now = /* @__PURE__ */ new Date();
3564
+ const now = args.now ?? /* @__PURE__ */ new Date();
3565
+ const relevant = matches.filter((m) => marketRelevant(m, now));
3566
+ if (relevant.length === 0) return void 0;
3567
+ const signals = await cachedMarketSignals(args, relevant);
3526
3568
  const out = {};
3527
- for (const m of matches) {
3569
+ for (const m of relevant) {
3528
3570
  const s = signals.get(m.id);
3529
3571
  if (s && isReliableMarketSignal(s, { now })) out[m.id] = marketData(s);
3530
3572
  }
@@ -3577,27 +3619,16 @@ ${matchList(matches, "No matches in play right now.", opts)}`;
3577
3619
  };
3578
3620
  }
3579
3621
  async function toolGetMatch(args) {
3580
- let match = allFixtures().find((m) => m.id === args.id);
3581
- let degraded = false;
3582
- let liveSource;
3583
- if (match) {
3584
- try {
3585
- const adapter = resolveAdapter(args);
3586
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
3587
- match = live.find((m) => m.id === args.id) ?? match;
3588
- liveSource = adapter.name;
3589
- } catch {
3590
- degraded = true;
3591
- }
3592
- }
3622
+ const { match, degraded, source: liveSource } = await getMatchById(resolveAdapter(args), args.id);
3593
3623
  if (!match) {
3594
3624
  return { text: withDisclaimer(`No match found with id ${args.id}.`), data: { match: null } };
3595
3625
  }
3596
3626
  const opts = fmtOpts(args);
3627
+ const now = args.now ?? /* @__PURE__ */ new Date();
3597
3628
  let marketSignal;
3598
- if (marketsEnabled()) {
3629
+ if (marketsEnabled() && marketRelevant(match, now)) {
3599
3630
  const s = (await cachedMarketSignals(args, [match])).get(match.id);
3600
- if (s && isReliableMarketSignal(s, { now: /* @__PURE__ */ new Date() })) marketSignal = s;
3631
+ if (s && isReliableMarketSignal(s, { now })) marketSignal = s;
3601
3632
  }
3602
3633
  const base = matchLine(match, opts);
3603
3634
  const text = marketSignal ? `${base}
@@ -3656,11 +3687,13 @@ ${matchLine(fixture, opts)}`),
3656
3687
  }
3657
3688
  async function toolGetMarketSignal(args) {
3658
3689
  const provider = resolveMarketProvider(args);
3690
+ const now = args.now ?? /* @__PURE__ */ new Date();
3659
3691
  if (args.matchId) {
3660
- const match = allFixtures().find((m) => m.id === args.matchId);
3661
- const sig = match ? await getMarketSignal(provider, match) : void 0;
3692
+ const { match } = await getMatchById(resolveAdapter(args), args.matchId);
3693
+ const relevant = match ? marketRelevant(match, now) : false;
3694
+ const sig = match && relevant ? await getMarketSignal(provider, match) : void 0;
3662
3695
  const shown2 = match && sig && marketDisplayable(sig) ? sig : void 0;
3663
- const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2) : `No reliable market signal for ${marketHeader(match)}.`;
3696
+ const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
3664
3697
  return {
3665
3698
  text: withDisclaimer(text2),
3666
3699
  data: {
@@ -3672,10 +3705,11 @@ async function toolGetMarketSignal(args) {
3672
3705
  }
3673
3706
  if (args.team) {
3674
3707
  const code = args.team.toUpperCase();
3675
- const fixture = nextFixtureForTeam(code);
3676
- const sig = fixture ? await getMarketSignal(provider, fixture) : void 0;
3708
+ const { match: fixture } = await marketFixtureForTeam(resolveAdapter(args), code, now);
3709
+ const relevant = fixture ? marketRelevant(fixture, now) : false;
3710
+ const sig = fixture && relevant ? await getMarketSignal(provider, fixture) : void 0;
3677
3711
  const shown2 = fixture && sig && marketDisplayable(sig) ? sig : void 0;
3678
- const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2) : `No reliable market signal for ${code}'s next fixture.`;
3712
+ const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
3679
3713
  return {
3680
3714
  text: withDisclaimer(text2),
3681
3715
  data: {
@@ -3686,15 +3720,15 @@ async function toolGetMarketSignal(args) {
3686
3720
  }
3687
3721
  };
3688
3722
  }
3689
- const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
3723
+ const date = args.date ?? localDate(now.toISOString(), args.tz);
3690
3724
  const { matches } = await getMatchesForDate(resolveAdapter(args), date);
3691
- const todays = fixturesByDate(date, matches, args.tz);
3725
+ const todays = fixturesByDate(date, matches, args.tz).filter((m) => marketRelevant(m, now));
3692
3726
  const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
3693
3727
  const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
3694
3728
  (r) => !!r.signal && marketDisplayable(r.signal)
3695
3729
  );
3696
3730
  const text = shown.length ? `Market signals on ${date}:
3697
- ${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `No reliable market signals on ${date}.`;
3731
+ ${shown.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : `No reliable market signals on ${date}.`;
3698
3732
  return {
3699
3733
  text: withDisclaimer(text),
3700
3734
  data: {
@@ -3706,10 +3740,12 @@ ${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `
3706
3740
  }
3707
3741
  async function reliableSignalMap(args, matches) {
3708
3742
  if (!marketsEnabled()) return /* @__PURE__ */ new Map();
3709
- const signals = await cachedMarketSignals(args, matches);
3710
- const now = /* @__PURE__ */ new Date();
3743
+ const now = args.now ?? /* @__PURE__ */ new Date();
3744
+ const relevant = matches.filter((m) => marketRelevant(m, now));
3745
+ if (relevant.length === 0) return /* @__PURE__ */ new Map();
3746
+ const signals = await cachedMarketSignals(args, relevant);
3711
3747
  const out = /* @__PURE__ */ new Map();
3712
- for (const m of matches) {
3748
+ for (const m of relevant) {
3713
3749
  const s = signals.get(m.id);
3714
3750
  if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(s)) out.set(m.id, s);
3715
3751
  }
@@ -3771,17 +3807,7 @@ async function toolGetShareSnippet(args) {
3771
3807
  );
3772
3808
  }
3773
3809
  if (args.matchId) {
3774
- let match = allFixtures().find((m) => m.id === args.matchId);
3775
- let source2;
3776
- if (match) {
3777
- try {
3778
- const adapter = resolveAdapter(args);
3779
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
3780
- match = live.find((m) => m.id === args.matchId) ?? match;
3781
- source2 = adapter.name;
3782
- } catch {
3783
- }
3784
- }
3810
+ const { match, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
3785
3811
  const matches = match ? [match] : [];
3786
3812
  return shareResult(
3787
3813
  "match",
@@ -3845,12 +3871,12 @@ async function toolGetShareSnippet(args) {
3845
3871
 
3846
3872
  // src/server.ts
3847
3873
  var SERVER_NAME = "claudinho";
3848
- var SERVER_VERSION = "0.4.2";
3874
+ var SERVER_VERSION = "0.4.3";
3849
3875
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
3850
3876
  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.`;
3851
3877
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
3852
3878
  Use get_live during matches, get_today for a day's schedule, get_next_fixture for a specific team (3-letter code, e.g. MEX), and get_standings for group tables.
3853
- Use get_market_signal for read-only prediction-market odds (a match, a team's next fixture, or a date). Market data is informational only \u2014 relay the percentages factually and never frame it as betting or trading advice.
3879
+ Use get_market_signal for read-only prediction-market signals (a match, a team's current-or-next fixture, or a date). Market data is informational only \u2014 relay the percentages factually and never frame it as betting or trading advice.
3854
3880
  Use get_share_snippet to produce a ready-to-paste match card (for a match, a team's next fixture, a date, or live matches) \u2014 hand the user the returned snippet text verbatim.${VOICE}
3855
3881
  ${DISCLAIMER}`;
3856
3882
  var dateArg = z.string().refine(isValidDate, "must be a real calendar date in YYYY-MM-DD form");
@@ -3926,7 +3952,7 @@ function buildServer() {
3926
3952
  "get_next_fixture",
3927
3953
  {
3928
3954
  title: "Next fixture for a team",
3929
- description: "A team's next scheduled match. Use a 3-letter code, e.g. MEX, BRA, USA.",
3955
+ description: "A team's next scheduled match. Use a 3-letter code, e.g. MEX, BRA, USA. Instant and offline \u2014 answered from the bundled schedule, no network.",
3930
3956
  inputSchema: { team: teamArg.describe("3-letter team code, e.g. MEX"), ...commonArgs },
3931
3957
  // Read-only and served entirely from the bundled static schedule.
3932
3958
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -3937,10 +3963,12 @@ function buildServer() {
3937
3963
  "get_market_signal",
3938
3964
  {
3939
3965
  title: "Prediction-market signal",
3940
- description: "Read-only prediction-market odds for a match (by id), a team's next fixture, or a date (default: today). Returns market-implied percentages with attribution. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
3966
+ description: "Read-only prediction-market signals for a match (by id), a team's current-or-next fixture, or a date (default: today). Returns market-implied percentages with attribution. Shown only before and during a match \u2014 finished matches have no market read. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
3941
3967
  inputSchema: {
3942
3968
  matchId: z.string().optional().describe("Match id (most specific)"),
3943
- team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
3969
+ team: teamArg.optional().describe(
3970
+ "3-letter team code, e.g. MEX \u2014 resolves to the team's in-play match when one is live, else their next fixture"
3971
+ ),
3944
3972
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today) for all that day's signals"),
3945
3973
  ...commonArgs
3946
3974
  },
@@ -4032,7 +4060,7 @@ function buildServer() {
4032
4060
  role: "user",
4033
4061
  content: {
4034
4062
  type: "text",
4035
- text: `Using get_next_fixture, get_standings, and get_market_signal, tell me about ${team}'s next match in the 2026 tournament, their current group standing, and what prediction markets currently say about that match. Treat the market percentages as informational context only \u2014 relay them factually, never as betting or trading advice.`
4063
+ text: `Using get_next_fixture, get_standings, and get_market_signal, tell me about ${team}'s next match in the 2026 tournament, their current group standing, and what prediction markets currently say about that match. Always state each fixture's date so a market read is never mistaken for a different match. Treat the market percentages as informational context only \u2014 relay them factually, never as betting or trading advice.`
4036
4064
  }
4037
4065
  }
4038
4066
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",
@@ -54,7 +54,7 @@
54
54
  "tsup": "^8.0.0",
55
55
  "typescript": "^5.7.0",
56
56
  "vitest": "^4.1.0",
57
- "@claudinho/core": "0.4.2"
57
+ "@claudinho/core": "0.4.3"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsup",